Skip to content

Commit 7f44f73

Browse files
authored
Merge pull request #12 from browser-use/feat/webhooks
feat: Add Webhooks CLI and Signature Verification Utils
2 parents 2314ae0 + 7afd50c commit 7f44f73

File tree

14 files changed

+790
-15
lines changed

14 files changed

+790
-15
lines changed

examples/.env.example

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# ----------------------------------------------------------------------------
2+
#
3+
# :GUIDE:
4+
#
5+
# Copy `.env.example` to `.env` to run all examples without any
6+
# additional setup. You can also manually export variables
7+
# and run each example separately.
8+
#
9+
# ----------------------------------------------------------------------------
10+
11+
# API ------------------------------------------------------------------------
12+
13+
# Browser Use API Key
14+
BROWSER_USE_API_KEY=""
15+
16+
# Webhooks -------------------------------------------------------------------
17+
18+
# NOTE: Use something simple in development. In production, Browser Use Cloud
19+
# will give you the production secret.
20+
SECRET_KEY="secret"
21+
22+
# ----------------------------------------------------------------------------

examples/demo.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
#!/usr/bin/env -S npm run tsn -T
22

33
import { BrowserUse } from 'browser-use-sdk';
4-
import { spinner } from './utils';
4+
5+
import { env, spinner } from './utils';
6+
7+
env();
58

69
// gets API Key from environment variable BROWSER_USE_API_KEY
710
const browseruse = new BrowserUse();

examples/stream-zod.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
import { BrowserUse } from 'browser-use-sdk';
44
import z from 'zod';
55

6+
import { env } from './utils';
7+
8+
env();
9+
610
const HackerNewsResponse = z.object({
711
title: z.string(),
812
url: z.string(),

examples/stream.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
import { BrowserUse } from 'browser-use-sdk';
44

5+
import { env } from './utils';
6+
7+
env();
8+
59
async function main() {
610
// gets API Key from environment variable BROWSER_USE_API_KEY
711
const browseruse = new BrowserUse();

examples/utils.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import dotenv from '@dotenvx/dotenvx';
2+
13
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
24

35
/**
@@ -26,3 +28,7 @@ export function spinner(renderText: () => string): () => void {
2628
}
2729
};
2830
}
31+
32+
export function env() {
33+
dotenv.config({ path: [__dirname + '/.env', '.env'] });
34+
}

examples/webhook.ts

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
#!/usr/bin/env -S npm run tsn -T
2+
3+
import { BrowserUse } from 'browser-use-sdk';
4+
import {
5+
verifyWebhookEventSignature,
6+
type WebhookAgentTaskStatusUpdatePayload,
7+
} from 'browser-use-sdk/lib/webhooks';
8+
import { createServer, IncomingMessage, type Server, type ServerResponse } from 'http';
9+
10+
import { env } from './utils';
11+
12+
env();
13+
14+
const PORT = 3000;
15+
const WAIT_FOR_TASK_FINISH_TIMEOUT = 60_000;
16+
17+
// Environment ---------------------------------------------------------------
18+
19+
const SECRET_KEY = process.env['SECRET_KEY'];
20+
21+
// API -----------------------------------------------------------------------
22+
23+
// gets API Key from environment variable BROWSER_USE_API_KEY
24+
const browseruse = new BrowserUse();
25+
26+
//
27+
28+
const whServerRef: { current: Server | null } = { current: null };
29+
30+
async function main() {
31+
if (!SECRET_KEY) {
32+
console.error('SECRET_KEY is not set');
33+
process.exit(1);
34+
}
35+
36+
console.log('Starting Browser Use Webhook Example');
37+
console.log('Run `browser-use listen --dev http://localhost:3000/webhook`!');
38+
39+
// Start a Webhook Server
40+
41+
const callback: { current: ((event: WebhookAgentTaskStatusUpdatePayload) => Promise<void>) | null } = {
42+
current: null,
43+
};
44+
45+
const server = createServer(async (req: IncomingMessage, res: ServerResponse) => {
46+
if (req.method === 'POST' && req.url === '/webhook') {
47+
let body = '';
48+
49+
req.on('data', (chunk) => {
50+
body += chunk.toString();
51+
});
52+
53+
req.on('end', async () => {
54+
try {
55+
const signature = req.headers['x-browser-use-signature'] as string;
56+
const timestamp = req.headers['x-browser-use-timestamp'] as string;
57+
58+
const event = await verifyWebhookEventSignature(
59+
{
60+
evt: body,
61+
signature,
62+
timestamp,
63+
},
64+
{
65+
secret: SECRET_KEY,
66+
},
67+
);
68+
69+
if (!event.ok) {
70+
console.log('❌ Invalid webhook signature');
71+
console.log(body);
72+
console.log(signature, 'signature');
73+
console.log(timestamp, 'timestamp');
74+
console.log(SECRET_KEY, 'SECRET_KEY');
75+
76+
res.writeHead(401, { 'Content-Type': 'application/json' });
77+
res.end(JSON.stringify({ error: 'Invalid signature' }));
78+
return;
79+
}
80+
81+
switch (event.event.type) {
82+
case 'agent.task.status_update':
83+
await callback.current?.(event.event.payload);
84+
85+
res.writeHead(200, { 'Content-Type': 'application/json' });
86+
res.end(JSON.stringify({ received: true }));
87+
break;
88+
case 'test':
89+
console.log('🧪 Test webhook received');
90+
91+
res.writeHead(200, { 'Content-Type': 'application/json' });
92+
res.end(JSON.stringify({ received: true }));
93+
break;
94+
default:
95+
console.log('🧪 Unknown webhook received');
96+
97+
res.writeHead(200, { 'Content-Type': 'application/json' });
98+
res.end(JSON.stringify({ received: true }));
99+
break;
100+
}
101+
} catch (error) {
102+
console.error(error);
103+
}
104+
});
105+
} else if (req.method === 'GET' && req.url === '/health') {
106+
res.writeHead(200, { 'Content-Type': 'application/json' });
107+
res.end(JSON.stringify({ status: 'ok', timestamp: new Date().toISOString() }));
108+
} else {
109+
res.writeHead(404, { 'Content-Type': 'application/json' });
110+
res.end(JSON.stringify({ error: 'Not found' }));
111+
}
112+
});
113+
114+
whServerRef.current = server;
115+
116+
server.listen(PORT, () => {
117+
console.log(`🌐 Webhook server listening on port ${PORT}`);
118+
console.log(`🔗 Health check: http://localhost:${PORT}/health`);
119+
});
120+
121+
await new Promise((resolve) => setTimeout(resolve, 1000));
122+
123+
// Create Task
124+
console.log('📝 Creating a new task...');
125+
const task = await browseruse.tasks.create({
126+
task: "What's the weather like in San Francisco and what's the current temperature?",
127+
});
128+
129+
console.log(`🔗 Task created: ${task.id}`);
130+
131+
await new Promise<void>((resolve, reject) => {
132+
// NOTE: We set a timeout so we can catch it when the task is stuck
133+
// and stop the example.
134+
const interval = setTimeout(() => {
135+
reject(new Error('Task creation timed out'));
136+
}, WAIT_FOR_TASK_FINISH_TIMEOUT);
137+
138+
// NOTE: We attach the callback to the current reference so we can receive updates from the server.
139+
callback.current = async (payload) => {
140+
if (payload.task_id !== task.id) {
141+
return;
142+
}
143+
144+
console.log('🔄 Task status updated:', payload.status);
145+
146+
if (payload.status === 'finished') {
147+
clearTimeout(interval);
148+
resolve();
149+
}
150+
};
151+
}).catch((error) => {
152+
console.error(error);
153+
process.exit(1);
154+
});
155+
156+
// Fetch final task result
157+
const status = await browseruse.tasks.retrieve(task.id);
158+
159+
console.log('🎯 Final Task Status');
160+
console.log('OUTPUT:');
161+
console.log(status.doneOutput);
162+
163+
server.close();
164+
}
165+
166+
// Handle graceful shutdown
167+
process.on('SIGINT', () => {
168+
console.log('\n👋 Shutting down gracefully...');
169+
whServerRef.current?.close();
170+
process.exit(0);
171+
});
172+
173+
//
174+
175+
if (require.main === module) {
176+
main().catch(console.error);
177+
}

examples/zod.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22

33
import { BrowserUse } from 'browser-use-sdk';
44
import { z } from 'zod';
5-
import { spinner } from './utils';
5+
6+
import { env, spinner } from './utils';
7+
8+
env();
69

710
// gets API Key from environment variable BROWSER_USE_API_KEY
811
const browseruse = new BrowserUse();

package.json

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,26 @@
2323
"format": "./scripts/format",
2424
"prepare": "if ./scripts/utils/check-is-in-git-install.sh; then ./scripts/build && ./scripts/utils/git-swap.sh; fi",
2525
"tsn": "ts-node -r tsconfig-paths/register",
26+
"cli": "ts-node -r tsconfig-paths/register --cwd $PWD ./src/lib/bin/cli.ts",
2627
"lint": "./scripts/lint",
2728
"fix": "./scripts/format"
2829
},
29-
"dependencies": {},
30+
"bin": {
31+
"browser-use": "./dist/lib/bin/cli.js"
32+
},
33+
"dependencies": {
34+
"@dotenvx/dotenvx": "^1.48.4",
35+
"fast-json-stable-stringify": "^2.1.0"
36+
},
3037
"devDependencies": {
3138
"@arethetypeswrong/cli": "^0.17.0",
3239
"@swc/core": "^1.3.102",
3340
"@swc/jest": "^0.2.29",
3441
"@types/jest": "^29.4.0",
35-
"@types/node": "^20.17.6",
42+
"@types/node": "^24.3.0",
3643
"@typescript-eslint/eslint-plugin": "8.31.1",
3744
"@typescript-eslint/parser": "8.31.1",
45+
"commander": "^14.0.0",
3846
"eslint": "^9.20.1",
3947
"eslint-plugin-prettier": "^5.4.1",
4048
"eslint-plugin-unused-imports": "^4.1.4",

src/lib/bin/auth.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import * as dotenv from '@dotenvx/dotenvx';
2+
3+
import { BrowserUse } from '../../';
4+
5+
const API_KEY_ENV_VAR_KEY = 'BROWSER_USE_API_KEY';
6+
7+
/**
8+
* Creates a new BrowserUse client with the API key from the environment variable.
9+
*/
10+
export function createBrowserUseClient() {
11+
let apiKey: string | null = null;
12+
13+
if (process.env[API_KEY_ENV_VAR_KEY]) {
14+
apiKey = process.env[API_KEY_ENV_VAR_KEY];
15+
}
16+
17+
if (apiKey == null) {
18+
const env = dotenv.config({ path: '.env' });
19+
20+
const envApiKey = env.parsed?.[API_KEY_ENV_VAR_KEY];
21+
22+
if (envApiKey) {
23+
apiKey = envApiKey;
24+
}
25+
}
26+
27+
if (apiKey == null) {
28+
console.error(`Missing ${API_KEY_ENV_VAR_KEY} environment variable!`);
29+
process.exit(1);
30+
}
31+
32+
return new BrowserUse({ apiKey });
33+
}
34+
35+
const SECRET_ENV_VAR_KEY = 'SECRET_KEY';
36+
37+
/**
38+
* Loads the Browser Use webhook secret from the environment variable.
39+
*/
40+
export function getBrowserUseWebhookSecret() {
41+
let secret: string | null = null;
42+
43+
if (process.env[SECRET_ENV_VAR_KEY]) {
44+
secret = process.env[SECRET_ENV_VAR_KEY];
45+
}
46+
47+
if (secret == null) {
48+
const env = dotenv.config({ path: '.env' });
49+
50+
const envSecret = env.parsed?.[SECRET_ENV_VAR_KEY];
51+
52+
if (envSecret) {
53+
secret = envSecret;
54+
}
55+
}
56+
57+
if (secret == null) {
58+
console.error(`Missing ${SECRET_ENV_VAR_KEY} environment variable!`);
59+
process.exit(1);
60+
}
61+
62+
return secret;
63+
}

src/lib/bin/cli.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#!/usr/bin/env -S npm run tsn -T
2+
3+
import { program } from 'commander';
4+
import { listen } from './commands/listen';
5+
6+
program
7+
.name('browser-use')
8+
.description('CLI to some JavaScript string utilities')
9+
.version('0.8.0')
10+
.addCommand(listen)
11+
.parse(process.argv);

0 commit comments

Comments
 (0)