Skip to content

Commit 376720f

Browse files
committed
Merge branch 'main' into dan/hdx-2622-refactor-tasks
2 parents 3001978 + 2162a69 commit 376720f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+3025
-417
lines changed

.changeset/angry-scissors-flow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@hyperdx/app": patch
3+
---
4+
5+
fix: memoize inputs to fix text input performance

.changeset/fluffy-mails-sparkle.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@hyperdx/common-utils": patch
3+
"@hyperdx/app": patch
4+
---
5+
6+
feat: Optimize and fix filtering on toStartOfX primary key expressions

.changeset/happy-spies-tickle.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@hyperdx/app": patch
3+
---
4+
5+
improve drawer a11y

.changeset/kind-hotels-arrive.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@hyperdx/api": patch
3+
"@hyperdx/app": patch
4+
---
5+
6+
feat: allow specifying webhook request headers

.changeset/soft-donkeys-fetch.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@hyperdx/app": patch
3+
---
4+
5+
feat: Implement query chunking for charts

.changeset/sweet-vans-pump.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@hyperdx/common-utils": patch
3+
---
4+
5+
perf: Improve getKeyValues query performance for JSON keys

.changeset/twelve-beers-buy.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@hyperdx/app": patch
3+
---
4+
5+
feat: Include displayed timestamp in default order by

.changeset/wise-horses-swim.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@hyperdx/app": patch
3+
---
4+
5+
avoid hydration errors when app loads if nav is collapsed

Makefile

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,16 @@ dev-int:
4545
npx nx run @hyperdx/api:dev:int $(FILE)
4646
docker compose -p int -f ./docker-compose.ci.yml down
4747

48+
.PHONY: dev-int-common-utils
49+
dev-int-common-utils:
50+
docker compose -p int -f ./docker-compose.ci.yml up -d
51+
npx nx run @hyperdx/common-utils:dev:int $(FILE)
52+
docker compose -p int -f ./docker-compose.ci.yml down
53+
4854
.PHONY: ci-int
4955
ci-int:
5056
docker compose -p int -f ./docker-compose.ci.yml up -d
51-
npx nx run @hyperdx/api:ci:int
57+
npx nx run-many -t ci:int --parallel=false
5258
docker compose -p int -f ./docker-compose.ci.yml down
5359

5460
.PHONY: dev-unit

packages/api/src/routers/api/__tests__/webhooks.test.ts

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,4 +180,226 @@ describe('webhooks router', () => {
180180

181181
await agent.delete('/webhooks/invalid-id').expect(400);
182182
});
183+
184+
describe('Header validation', () => {
185+
it('POST / - accepts valid header names', async () => {
186+
const { agent } = await getLoggedInAgent(server);
187+
188+
const validHeaders = {
189+
'Content-Type': 'application/json',
190+
Authorization: 'Bearer token',
191+
'X-Custom-Header': 'value',
192+
'User-Agent': 'test',
193+
'x-api-key': 'secret',
194+
'custom!header#test': 'value',
195+
};
196+
197+
const response = await agent
198+
.post('/webhooks')
199+
.send({
200+
...MOCK_WEBHOOK,
201+
url: 'https://example.com/valid-headers',
202+
headers: validHeaders,
203+
})
204+
.expect(200);
205+
206+
expect(response.body.data.headers).toMatchObject(validHeaders);
207+
});
208+
209+
it('POST / - rejects header names starting with numbers', async () => {
210+
const { agent } = await getLoggedInAgent(server);
211+
212+
const response = await agent
213+
.post('/webhooks')
214+
.send({
215+
...MOCK_WEBHOOK,
216+
url: 'https://example.com/invalid-header-name',
217+
headers: {
218+
'123Invalid': 'value',
219+
},
220+
})
221+
.expect(400);
222+
223+
expect(Array.isArray(response.body)).toBe(true);
224+
expect(response.body[0].type).toBe('Body');
225+
expect(response.body[0].errors).toBeDefined();
226+
});
227+
228+
it('POST / - rejects empty header names', async () => {
229+
const { agent } = await getLoggedInAgent(server);
230+
231+
const response = await agent
232+
.post('/webhooks')
233+
.send({
234+
...MOCK_WEBHOOK,
235+
url: 'https://example.com/empty-header-name',
236+
headers: {
237+
'': 'value',
238+
},
239+
})
240+
.expect(400);
241+
242+
expect(Array.isArray(response.body)).toBe(true);
243+
expect(response.body[0].type).toBe('Body');
244+
expect(response.body[0].errors).toBeDefined();
245+
});
246+
247+
it('POST / - rejects header names with invalid characters', async () => {
248+
const { agent } = await getLoggedInAgent(server);
249+
250+
const invalidHeaderNames = [
251+
{ 'Header Name': 'value' }, // space
252+
{ 'Header\nName': 'value' }, // newline
253+
{ 'Header\rName': 'value' }, // carriage return
254+
{ 'Header\tName': 'value' }, // tab
255+
{ 'Header@Name': 'value' }, // @ not allowed
256+
{ 'Header[Name]': 'value' }, // brackets not allowed
257+
];
258+
259+
for (const headers of invalidHeaderNames) {
260+
const response = await agent
261+
.post('/webhooks')
262+
.send({
263+
...MOCK_WEBHOOK,
264+
url: `https://example.com/invalid-header-${Math.random()}`,
265+
headers,
266+
})
267+
.expect(400);
268+
269+
expect(Array.isArray(response.body)).toBe(true);
270+
expect(response.body[0].type).toBe('Body');
271+
expect(response.body[0].errors).toBeDefined();
272+
}
273+
});
274+
275+
it('POST / - accepts valid header values', async () => {
276+
const { agent } = await getLoggedInAgent(server);
277+
278+
const validHeaders = {
279+
Authorization: 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9',
280+
'Content-Type': 'application/json; charset=utf-8',
281+
'X-Api-Key': 'abc123-def456-ghi789',
282+
'User-Agent': 'Mozilla/5.0 (compatible; TestBot/1.0)',
283+
'Custom-Header': 'value with spaces and special chars: !@#$%^&*()',
284+
};
285+
286+
const response = await agent
287+
.post('/webhooks')
288+
.send({
289+
...MOCK_WEBHOOK,
290+
url: 'https://example.com/valid-header-values',
291+
headers: validHeaders,
292+
})
293+
.expect(200);
294+
295+
expect(response.body.data.headers).toMatchObject(validHeaders);
296+
});
297+
298+
it('POST / - rejects header values with CRLF injection', async () => {
299+
const { agent } = await getLoggedInAgent(server);
300+
301+
const response = await agent
302+
.post('/webhooks')
303+
.send({
304+
...MOCK_WEBHOOK,
305+
url: 'https://example.com/crlf-injection',
306+
headers: {
307+
'X-Custom-Header': 'value\r\nX-Injected-Header: malicious',
308+
},
309+
})
310+
.expect(400);
311+
312+
expect(Array.isArray(response.body)).toBe(true);
313+
expect(response.body[0].type).toBe('Body');
314+
expect(response.body[0].errors).toBeDefined();
315+
});
316+
317+
it('POST / - rejects header values with tab characters', async () => {
318+
const { agent } = await getLoggedInAgent(server);
319+
320+
const response = await agent
321+
.post('/webhooks')
322+
.send({
323+
...MOCK_WEBHOOK,
324+
url: 'https://example.com/tab-injection',
325+
headers: {
326+
'X-Custom-Header': 'value\twith\ttabs',
327+
},
328+
})
329+
.expect(400);
330+
331+
expect(Array.isArray(response.body)).toBe(true);
332+
expect(response.body[0].type).toBe('Body');
333+
expect(response.body[0].errors).toBeDefined();
334+
});
335+
336+
it('POST / - rejects header values with control characters', async () => {
337+
const { agent } = await getLoggedInAgent(server);
338+
339+
// Test various control characters
340+
const controlCharTests = [
341+
'\x00', // null
342+
'\x01', // start of heading
343+
'\x0B', // vertical tab
344+
'\x0C', // form feed
345+
'\x1F', // unit separator
346+
'\x7F', // delete
347+
];
348+
349+
for (const controlChar of controlCharTests) {
350+
const response = await agent
351+
.post('/webhooks')
352+
.send({
353+
...MOCK_WEBHOOK,
354+
url: `https://example.com/control-char-${Math.random()}`,
355+
headers: {
356+
'X-Custom-Header': `value${controlChar}test`,
357+
},
358+
})
359+
.expect(400);
360+
361+
expect(Array.isArray(response.body)).toBe(true);
362+
expect(response.body[0].type).toBe('Body');
363+
expect(response.body[0].errors).toBeDefined();
364+
}
365+
});
366+
367+
it('POST / - rejects header values with newline characters', async () => {
368+
const { agent } = await getLoggedInAgent(server);
369+
370+
const response = await agent
371+
.post('/webhooks')
372+
.send({
373+
...MOCK_WEBHOOK,
374+
url: 'https://example.com/newline-injection',
375+
headers: {
376+
'X-Custom-Header': 'value\nwith\nnewlines',
377+
},
378+
})
379+
.expect(400);
380+
381+
expect(Array.isArray(response.body)).toBe(true);
382+
expect(response.body[0].type).toBe('Body');
383+
expect(response.body[0].errors).toBeDefined();
384+
});
385+
386+
it('POST / - rejects header values with carriage return characters', async () => {
387+
const { agent } = await getLoggedInAgent(server);
388+
389+
const response = await agent
390+
.post('/webhooks')
391+
.send({
392+
...MOCK_WEBHOOK,
393+
url: 'https://example.com/carriage-return-injection',
394+
headers: {
395+
'X-Custom-Header': 'value\rwith\rcarriage\rreturns',
396+
},
397+
})
398+
.expect(400);
399+
400+
expect(Array.isArray(response.body)).toBe(true);
401+
expect(response.body[0].type).toBe('Body');
402+
expect(response.body[0].errors).toBeDefined();
403+
});
404+
});
183405
});

0 commit comments

Comments
 (0)