Skip to content

Commit f9bf0a1

Browse files
committed
add webhooks cli
1 parent 2314ae0 commit f9bf0a1

File tree

8 files changed

+725
-13
lines changed

8 files changed

+725
-13
lines changed

examples/webhook.ts

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

package.json

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,22 @@
2626
"lint": "./scripts/lint",
2727
"fix": "./scripts/format"
2828
},
29-
"dependencies": {},
29+
"bin": {
30+
"browser-use": "./dist/lib/bin/cli.js"
31+
},
32+
"dependencies": {
33+
"@dotenvx/dotenvx": "^1.48.4",
34+
"fast-json-stable-stringify": "^2.1.0"
35+
},
3036
"devDependencies": {
3137
"@arethetypeswrong/cli": "^0.17.0",
3238
"@swc/core": "^1.3.102",
3339
"@swc/jest": "^0.2.29",
3440
"@types/jest": "^29.4.0",
35-
"@types/node": "^20.17.6",
41+
"@types/node": "^24.3.0",
3642
"@typescript-eslint/eslint-plugin": "8.31.1",
3743
"@typescript-eslint/parser": "8.31.1",
44+
"commander": "^14.0.0",
3845
"eslint": "^9.20.1",
3946
"eslint-plugin-prettier": "^5.4.1",
4047
"eslint-plugin-unused-imports": "^4.1.4",

src/lib/bin/auth.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import * as dotenv from '@dotenvx/dotenvx';
2+
3+
import { BrowserUse } from '../../';
4+
5+
const 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[ENV_VAR_KEY]) {
14+
apiKey = process.env[ENV_VAR_KEY];
15+
}
16+
17+
if (apiKey == null) {
18+
const env = dotenv.config({ path: '.env' });
19+
20+
const envApiKey = env.parsed?.[ENV_VAR_KEY];
21+
22+
if (envApiKey) {
23+
apiKey = envApiKey;
24+
}
25+
}
26+
27+
if (apiKey == null) {
28+
console.error(`Missing BROWSER_USE_API_KEY environment variable!`);
29+
process.exit(1);
30+
}
31+
32+
return new BrowserUse({ apiKey });
33+
}

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)