Skip to content

Commit 562a043

Browse files
committed
fixes
1 parent b7729fe commit 562a043

File tree

7 files changed

+288
-11
lines changed

7 files changed

+288
-11
lines changed

.fernignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
# Specify files that shouldn't be modified by Fern
22
src/wrapper/
3+
test/wrapper/
34
src/index.ts
45
examples/
56
.vscode/
67

7-
.gitignore
8+
.gitignore
9+
jest.config.mjs

README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,43 @@ for await (const update of task.watch()) {
9797
}
9898
```
9999

100+
## Webhook Verification
101+
102+
> We encourage you to use the SDK functions that verify and parse webhook events.
103+
104+
```ts
105+
import { verifyWebhookEventSignature, type WebhookAgentTaskStatusUpdatePayload } from "browser-use-sdk";
106+
107+
export async function POST(req: Request) {
108+
const signature = req.headers["x-browser-use-signature"] as string;
109+
const timestamp = req.headers["x-browser-use-timestamp"] as string;
110+
111+
const event = await verifyWebhookEventSignature(
112+
{
113+
body,
114+
signature,
115+
timestamp,
116+
},
117+
{
118+
secret: SECRET_KEY,
119+
},
120+
);
121+
122+
if (!event.ok) {
123+
return;
124+
}
125+
126+
switch (event.event.type) {
127+
case "agent.task.status_update":
128+
break;
129+
case "test":
130+
break;
131+
default:
132+
break;
133+
}
134+
}
135+
```
136+
100137
## Advanced Usage
101138

102139
## Handling errors

examples/stream.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,6 @@ async function basic() {
5555
console.log(result.output);
5656
}
5757

58-
basic().catch(console.error);
59-
6058
// Structured ----------------------------------------------------------------
6159

6260
// Define Structured Output Schema
@@ -92,6 +90,6 @@ async function structured() {
9290
}
9391
}
9492

95-
// basic()
96-
// .then(() => structured())
97-
// .catch(console.error);
93+
basic()
94+
.then(() => structured())
95+
.catch(console.error);

jest.config.mjs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export default {
1111
"^(\.{1,2}/.*)\.js$": "$1",
1212
},
1313
roots: ["<rootDir>/tests"],
14-
testPathIgnorePatterns: ["\.browser\.(spec|test)\.[jt]sx?$", "/tests/wire/"],
14+
testPathIgnorePatterns: ["\.browser\.(spec|test)\.[jt]sx?$", "/tests/wire/", "/tests/wrapper"],
1515
setupFilesAfterEnv: [],
1616
},
1717
{
@@ -25,7 +25,6 @@ export default {
2525
testMatch: ["<rootDir>/tests/unit/**/?(*.)+(browser).(spec|test).[jt]s?(x)"],
2626
setupFilesAfterEnv: [],
2727
},
28-
,
2928
{
3029
displayName: "wire",
3130
preset: "ts-jest",
@@ -36,6 +35,13 @@ export default {
3635
roots: ["<rootDir>/tests/wire"],
3736
setupFilesAfterEnv: ["<rootDir>/tests/mock-server/setup.ts"],
3837
},
38+
{
39+
displayName: "wrapper",
40+
preset: "ts-jest",
41+
testEnvironment: "node",
42+
moduleNameMapper: {},
43+
roots: ["<rootDir>/tests/wrapper"],
44+
},
3945
],
4046
workerThreads: false,
4147
passWithNoTests: true,

src/index.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export * as BrowserUse from "./api/index.js";
2-
export { BrowserUseError, BrowserUseTimeoutError } from "./errors/index.js";
32
export { BrowserUseEnvironment } from "./environments.js";
3+
export { BrowserUseError, BrowserUseTimeoutError } from "./errors/index.js";
44
export { BrowserUseClient } from "./wrapper/BrowserUseClient.js";
5-
export type { WrappedTaskFnsWithoutSchema } from "./wrapper/lib/parse.js";
6-
export type { WrappedTaskFnsWithSchema } from "./wrapper/lib/parse.js";
5+
export type { WrappedTaskFnsWithoutSchema, WrappedTaskFnsWithSchema } from "./wrapper/lib/parse.js";
6+
export { createWebhookSignature, verifyWebhookEventSignature } from "./wrapper/lib/webhooks.js";
7+
export type { Webhook, WebhookAgentTaskStatusUpdatePayload, WebhookTestPayload } from "./wrapper/lib/webhooks.js";

src/wrapper/lib/webhooks.ts

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { createHmac } from "crypto";
2+
import stringify from "fast-json-stable-stringify";
3+
import { z } from "zod";
4+
5+
// https://docs.browser-use.com/cloud/webhooks
6+
7+
//
8+
9+
export const zWebhookTimestamp = z.iso.datetime({ offset: true, local: true });
10+
11+
// test
12+
13+
export const zWebhookTestPayload = z.object({
14+
test: z.literal("ok"),
15+
});
16+
17+
export type WebhookTestPayload = z.infer<typeof zWebhookTestPayload>;
18+
19+
export const zWebhookTest = z.object({
20+
type: z.literal("test"),
21+
timestamp: zWebhookTimestamp,
22+
payload: zWebhookTestPayload,
23+
});
24+
25+
// agent.task.status_update
26+
27+
export const zWebhookAgentTaskStatusUpdatePayloadMetadata = z.record(z.string(), z.unknown()).optional();
28+
29+
export const zWebhookAgentTaskStatusUpdatePayloadStatus = z.literal([
30+
"initializing",
31+
"started",
32+
"paused",
33+
"stopped",
34+
"finished",
35+
]);
36+
37+
export const zWebhookAgentTaskStatusUpdatePayload = z.object({
38+
session_id: z.string(),
39+
task_id: z.string(),
40+
status: zWebhookAgentTaskStatusUpdatePayloadStatus,
41+
metadata: zWebhookAgentTaskStatusUpdatePayloadMetadata,
42+
});
43+
44+
export type WebhookAgentTaskStatusUpdatePayload = z.infer<typeof zWebhookAgentTaskStatusUpdatePayload>;
45+
46+
export const zWebhookAgentTaskStatusUpdate = z.object({
47+
type: z.literal("agent.task.status_update"),
48+
timestamp: zWebhookTimestamp,
49+
payload: zWebhookAgentTaskStatusUpdatePayload,
50+
});
51+
52+
//
53+
54+
export const zWebhookSchema = z.discriminatedUnion("type", [
55+
//
56+
zWebhookTest,
57+
zWebhookAgentTaskStatusUpdate,
58+
]);
59+
60+
export type Webhook = z.infer<typeof zWebhookSchema>;
61+
62+
// Signature
63+
64+
/**
65+
* Utility function that validates the received Webhook event/
66+
*/
67+
export async function verifyWebhookEventSignature(
68+
evt: {
69+
body: string | object;
70+
signature: string;
71+
timestamp: string;
72+
},
73+
cfg: { secret: string },
74+
): Promise<{ ok: true; event: Webhook } | { ok: false }> {
75+
try {
76+
const json = typeof evt.body === "string" ? JSON.parse(evt.body) : evt.body;
77+
const event = await zWebhookSchema.safeParseAsync(json);
78+
79+
if (event.success === false) {
80+
return { ok: false };
81+
}
82+
83+
const signature = createWebhookSignature({
84+
payload: event.data.payload,
85+
timestamp: evt.timestamp,
86+
secret: cfg.secret,
87+
});
88+
89+
// Compare signatures using timing-safe comparison
90+
if (evt.signature !== signature) {
91+
return { ok: false };
92+
}
93+
94+
return { ok: true, event: event.data };
95+
} catch (err) {
96+
console.error(err);
97+
return { ok: false };
98+
}
99+
}
100+
101+
/**
102+
* Creates a webhook signature for the given payload, timestamp, and secret.
103+
*/
104+
export function createWebhookSignature({
105+
payload,
106+
timestamp,
107+
secret,
108+
}: {
109+
payload: unknown;
110+
timestamp: string;
111+
secret: string;
112+
}): string {
113+
const dump = stringify(payload);
114+
const message = `${timestamp}.${dump}`;
115+
116+
const hmac = createHmac("sha256", secret);
117+
hmac.update(message);
118+
return hmac.digest("hex");
119+
}

tests/wrapper/webhooks.test.ts

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import {
2+
createWebhookSignature,
3+
verifyWebhookEventSignature,
4+
zWebhookSchema,
5+
zWebhookTimestamp,
6+
} from "../../src/wrapper/lib/webhooks";
7+
8+
describe("webhooks", () => {
9+
describe("parse", () => {
10+
test("timestamp", () => {
11+
expect(zWebhookTimestamp.parse("2025-05-25T09:22:22.269116+00:00")).toBeDefined();
12+
expect(zWebhookTimestamp.parse("2025-08-15T18:09:11.881540")).toBeDefined();
13+
});
14+
15+
test("agent.task.status_update", () => {
16+
const MOCK: unknown = {
17+
type: "agent.task.status_update",
18+
timestamp: "2025-05-25T09:22:22.269116+00:00",
19+
payload: {
20+
session_id: "cd9cc7bf-e3af-4181-80a2-73f083bc94b4",
21+
task_id: "5b73fb3f-a3cb-4912-be40-17ce9e9e1a45",
22+
status: "finished",
23+
metadata: {
24+
campaign: "q4-automation",
25+
team: "marketing",
26+
},
27+
},
28+
};
29+
30+
const response = zWebhookSchema.parse(MOCK);
31+
32+
expect(response).toBeDefined();
33+
});
34+
35+
test("test", () => {
36+
const MOCK: unknown = {
37+
type: "test",
38+
timestamp: "2025-05-25T09:22:22.269116+00:00",
39+
payload: { test: "ok" },
40+
};
41+
42+
const response = zWebhookSchema.parse(MOCK);
43+
44+
expect(response).toBeDefined();
45+
});
46+
47+
test("invalid", () => {
48+
const MOCK: unknown = {
49+
type: "invalid",
50+
timestamp: "2025-05-25T09:22:22.269116+00:00",
51+
payload: { test: "ok" },
52+
};
53+
54+
expect(() => zWebhookSchema.parse(MOCK)).toThrow();
55+
});
56+
});
57+
58+
describe("verify", () => {
59+
test("correctly calculates signature", async () => {
60+
const timestamp = "2025-05-26:22:22.269116+00:00";
61+
62+
const MOCK = {
63+
type: "agent.task.status_update",
64+
timestamp: "2025-05-25T09:22:22.269116+00:00",
65+
payload: {
66+
session_id: "cd9cc7bf-e3af-4181-80a2-73f083bc94b4",
67+
task_id: "5b73fb3f-a3cb-4912-be40-17ce9e9e1a45",
68+
status: "finished",
69+
metadata: {
70+
campaign: "q4-automation",
71+
team: "marketing",
72+
},
73+
},
74+
};
75+
76+
const signature = createWebhookSignature({
77+
payload: MOCK.payload,
78+
secret: "secret",
79+
timestamp,
80+
});
81+
82+
const validJSON = await verifyWebhookEventSignature(
83+
{
84+
body: MOCK,
85+
signature: signature,
86+
timestamp,
87+
},
88+
{ secret: "secret" },
89+
);
90+
91+
const validString = await verifyWebhookEventSignature(
92+
{
93+
body: JSON.stringify(MOCK),
94+
signature: signature,
95+
timestamp,
96+
},
97+
{ secret: "secret" },
98+
);
99+
100+
const invalid = await verifyWebhookEventSignature(
101+
{
102+
body: JSON.stringify(MOCK),
103+
signature: "invalid",
104+
timestamp,
105+
},
106+
{ secret: "secret" },
107+
);
108+
109+
expect(validJSON.ok).toBe(true);
110+
expect(validString.ok).toBe(true);
111+
expect(invalid.ok).toBe(false);
112+
});
113+
});
114+
});

0 commit comments

Comments
 (0)