diff --git a/examples/.env.example b/examples/.env.example new file mode 100644 index 0000000..ec44747 --- /dev/null +++ b/examples/.env.example @@ -0,0 +1,22 @@ +# ---------------------------------------------------------------------------- +# +# :GUIDE: +# +# Copy `.env.example` to `.env` to run all examples without any +# additional setup. You can also manually export variables +# and run each example separately. +# +# ---------------------------------------------------------------------------- + +# API ------------------------------------------------------------------------ + +# Browser Use API Key +BROWSER_USE_API_KEY="" + +# Webhooks ------------------------------------------------------------------- + +# NOTE: Use something simple in development. In production, Browser Use Cloud +# will give you the production secret. +SECRET_KEY="secret" + +# ---------------------------------------------------------------------------- \ No newline at end of file diff --git a/examples/demo.ts b/examples/demo.ts index 967e863..c8db403 100755 --- a/examples/demo.ts +++ b/examples/demo.ts @@ -1,7 +1,10 @@ #!/usr/bin/env -S npm run tsn -T import { BrowserUse } from 'browser-use-sdk'; -import { spinner } from './utils'; + +import { env, spinner } from './utils'; + +env(); // gets API Key from environment variable BROWSER_USE_API_KEY const browseruse = new BrowserUse(); diff --git a/examples/stream-zod.ts b/examples/stream-zod.ts index 6c2ac21..1e7ed2c 100755 --- a/examples/stream-zod.ts +++ b/examples/stream-zod.ts @@ -3,6 +3,10 @@ import { BrowserUse } from 'browser-use-sdk'; import z from 'zod'; +import { env } from './utils'; + +env(); + const HackerNewsResponse = z.object({ title: z.string(), url: z.string(), diff --git a/examples/stream.ts b/examples/stream.ts index 05fcf94..19d44ae 100755 --- a/examples/stream.ts +++ b/examples/stream.ts @@ -2,6 +2,10 @@ import { BrowserUse } from 'browser-use-sdk'; +import { env } from './utils'; + +env(); + async function main() { // gets API Key from environment variable BROWSER_USE_API_KEY const browseruse = new BrowserUse(); diff --git a/examples/utils.ts b/examples/utils.ts index cc89cad..f65c3a1 100644 --- a/examples/utils.ts +++ b/examples/utils.ts @@ -1,3 +1,5 @@ +import dotenv from '@dotenvx/dotenvx'; + const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; /** @@ -26,3 +28,7 @@ export function spinner(renderText: () => string): () => void { } }; } + +export function env() { + dotenv.config({ path: [__dirname + '/.env', '.env'] }); +} diff --git a/examples/webhook.ts b/examples/webhook.ts new file mode 100755 index 0000000..5c6c5ad --- /dev/null +++ b/examples/webhook.ts @@ -0,0 +1,177 @@ +#!/usr/bin/env -S npm run tsn -T + +import { BrowserUse } from 'browser-use-sdk'; +import { + verifyWebhookEventSignature, + type WebhookAgentTaskStatusUpdatePayload, +} from 'browser-use-sdk/lib/webhooks'; +import { createServer, IncomingMessage, type Server, type ServerResponse } from 'http'; + +import { env } from './utils'; + +env(); + +const PORT = 3000; +const WAIT_FOR_TASK_FINISH_TIMEOUT = 60_000; + +// Environment --------------------------------------------------------------- + +const SECRET_KEY = process.env['SECRET_KEY']; + +// API ----------------------------------------------------------------------- + +// gets API Key from environment variable BROWSER_USE_API_KEY +const browseruse = new BrowserUse(); + +// + +const whServerRef: { current: Server | null } = { current: null }; + +async function main() { + if (!SECRET_KEY) { + console.error('SECRET_KEY is not set'); + process.exit(1); + } + + console.log('Starting Browser Use Webhook Example'); + console.log('Run `browser-use listen --dev http://localhost:3000/webhook`!'); + + // Start a Webhook Server + + const callback: { current: ((event: WebhookAgentTaskStatusUpdatePayload) => Promise) | null } = { + current: null, + }; + + const server = createServer(async (req: IncomingMessage, res: ServerResponse) => { + if (req.method === 'POST' && req.url === '/webhook') { + let body = ''; + + req.on('data', (chunk) => { + body += chunk.toString(); + }); + + req.on('end', async () => { + try { + const signature = req.headers['x-browser-use-signature'] as string; + const timestamp = req.headers['x-browser-use-timestamp'] as string; + + const event = await verifyWebhookEventSignature( + { + evt: body, + signature, + timestamp, + }, + { + secret: SECRET_KEY, + }, + ); + + if (!event.ok) { + console.log('❌ Invalid webhook signature'); + console.log(body); + console.log(signature, 'signature'); + console.log(timestamp, 'timestamp'); + console.log(SECRET_KEY, 'SECRET_KEY'); + + res.writeHead(401, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'Invalid signature' })); + return; + } + + switch (event.event.type) { + case 'agent.task.status_update': + await callback.current?.(event.event.payload); + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ received: true })); + break; + case 'test': + console.log('🧪 Test webhook received'); + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ received: true })); + break; + default: + console.log('🧪 Unknown webhook received'); + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ received: true })); + break; + } + } catch (error) { + console.error(error); + } + }); + } else if (req.method === 'GET' && req.url === '/health') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ status: 'ok', timestamp: new Date().toISOString() })); + } else { + res.writeHead(404, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'Not found' })); + } + }); + + whServerRef.current = server; + + server.listen(PORT, () => { + console.log(`🌐 Webhook server listening on port ${PORT}`); + console.log(`🔗 Health check: http://localhost:${PORT}/health`); + }); + + await new Promise((resolve) => setTimeout(resolve, 1000)); + + // Create Task + console.log('📝 Creating a new task...'); + const task = await browseruse.tasks.create({ + task: "What's the weather like in San Francisco and what's the current temperature?", + }); + + console.log(`🔗 Task created: ${task.id}`); + + await new Promise((resolve, reject) => { + // NOTE: We set a timeout so we can catch it when the task is stuck + // and stop the example. + const interval = setTimeout(() => { + reject(new Error('Task creation timed out')); + }, WAIT_FOR_TASK_FINISH_TIMEOUT); + + // NOTE: We attach the callback to the current reference so we can receive updates from the server. + callback.current = async (payload) => { + if (payload.task_id !== task.id) { + return; + } + + console.log('🔄 Task status updated:', payload.status); + + if (payload.status === 'finished') { + clearTimeout(interval); + resolve(); + } + }; + }).catch((error) => { + console.error(error); + process.exit(1); + }); + + // Fetch final task result + const status = await browseruse.tasks.retrieve(task.id); + + console.log('🎯 Final Task Status'); + console.log('OUTPUT:'); + console.log(status.doneOutput); + + server.close(); +} + +// Handle graceful shutdown +process.on('SIGINT', () => { + console.log('\n👋 Shutting down gracefully...'); + whServerRef.current?.close(); + process.exit(0); +}); + +// + +if (require.main === module) { + main().catch(console.error); +} diff --git a/examples/zod.ts b/examples/zod.ts index 6437269..49eb30c 100755 --- a/examples/zod.ts +++ b/examples/zod.ts @@ -2,7 +2,10 @@ import { BrowserUse } from 'browser-use-sdk'; import { z } from 'zod'; -import { spinner } from './utils'; + +import { env, spinner } from './utils'; + +env(); // gets API Key from environment variable BROWSER_USE_API_KEY const browseruse = new BrowserUse(); diff --git a/package.json b/package.json index d22676e..681576a 100644 --- a/package.json +++ b/package.json @@ -23,18 +23,26 @@ "format": "./scripts/format", "prepare": "if ./scripts/utils/check-is-in-git-install.sh; then ./scripts/build && ./scripts/utils/git-swap.sh; fi", "tsn": "ts-node -r tsconfig-paths/register", + "cli": "ts-node -r tsconfig-paths/register --cwd $PWD ./src/lib/bin/cli.ts", "lint": "./scripts/lint", "fix": "./scripts/format" }, - "dependencies": {}, + "bin": { + "browser-use": "./dist/lib/bin/cli.js" + }, + "dependencies": { + "@dotenvx/dotenvx": "^1.48.4", + "fast-json-stable-stringify": "^2.1.0" + }, "devDependencies": { "@arethetypeswrong/cli": "^0.17.0", "@swc/core": "^1.3.102", "@swc/jest": "^0.2.29", "@types/jest": "^29.4.0", - "@types/node": "^20.17.6", + "@types/node": "^24.3.0", "@typescript-eslint/eslint-plugin": "8.31.1", "@typescript-eslint/parser": "8.31.1", + "commander": "^14.0.0", "eslint": "^9.20.1", "eslint-plugin-prettier": "^5.4.1", "eslint-plugin-unused-imports": "^4.1.4", diff --git a/src/lib/bin/auth.ts b/src/lib/bin/auth.ts new file mode 100644 index 0000000..278723e --- /dev/null +++ b/src/lib/bin/auth.ts @@ -0,0 +1,63 @@ +import * as dotenv from '@dotenvx/dotenvx'; + +import { BrowserUse } from '../../'; + +const API_KEY_ENV_VAR_KEY = 'BROWSER_USE_API_KEY'; + +/** + * Creates a new BrowserUse client with the API key from the environment variable. + */ +export function createBrowserUseClient() { + let apiKey: string | null = null; + + if (process.env[API_KEY_ENV_VAR_KEY]) { + apiKey = process.env[API_KEY_ENV_VAR_KEY]; + } + + if (apiKey == null) { + const env = dotenv.config({ path: '.env' }); + + const envApiKey = env.parsed?.[API_KEY_ENV_VAR_KEY]; + + if (envApiKey) { + apiKey = envApiKey; + } + } + + if (apiKey == null) { + console.error(`Missing ${API_KEY_ENV_VAR_KEY} environment variable!`); + process.exit(1); + } + + return new BrowserUse({ apiKey }); +} + +const SECRET_ENV_VAR_KEY = 'SECRET_KEY'; + +/** + * Loads the Browser Use webhook secret from the environment variable. + */ +export function getBrowserUseWebhookSecret() { + let secret: string | null = null; + + if (process.env[SECRET_ENV_VAR_KEY]) { + secret = process.env[SECRET_ENV_VAR_KEY]; + } + + if (secret == null) { + const env = dotenv.config({ path: '.env' }); + + const envSecret = env.parsed?.[SECRET_ENV_VAR_KEY]; + + if (envSecret) { + secret = envSecret; + } + } + + if (secret == null) { + console.error(`Missing ${SECRET_ENV_VAR_KEY} environment variable!`); + process.exit(1); + } + + return secret; +} diff --git a/src/lib/bin/cli.ts b/src/lib/bin/cli.ts new file mode 100755 index 0000000..1550efb --- /dev/null +++ b/src/lib/bin/cli.ts @@ -0,0 +1,11 @@ +#!/usr/bin/env -S npm run tsn -T + +import { program } from 'commander'; +import { listen } from './commands/listen'; + +program + .name('browser-use') + .description('CLI to some JavaScript string utilities') + .version('0.8.0') + .addCommand(listen) + .parse(process.argv); diff --git a/src/lib/bin/commands/listen.ts b/src/lib/bin/commands/listen.ts new file mode 100644 index 0000000..c9f8f4e --- /dev/null +++ b/src/lib/bin/commands/listen.ts @@ -0,0 +1,163 @@ +import { Command } from 'commander'; + +import { BrowserUse } from '../../../'; +import { createWebhookSignature, Webhook } from '../../webhooks'; +import { createBrowserUseClient, getBrowserUseWebhookSecret } from '../auth'; + +// NOTE: We perform task list refresh to get all running tasks and then +const tickRef: { + timeout: NodeJS.Timeout | null; + abort: AbortController | null; +} = { timeout: null, abort: null }; + +export const listen = new Command('listen') + .description(`Open a local webhook to receive Cloud API updates from the CLI on your local machine.`) + .option('-d, --dev ', 'The endpoint to forward updates to.') + .action(async (options) => { + // Auth + + const client = createBrowserUseClient(); + const secret = getBrowserUseWebhookSecret(); + + // Proxy + + const { dev: localTargetEndpoint } = options; + + if (typeof localTargetEndpoint !== 'string') { + // NOTE: This should never happen because the command is validated by commander. + throw new Error( + 'Something unexpected happened. Please report this issue. https://github.com/browser-use/browser-use-node/issues', + ); + } + + const localTargetURL = new URL(localTargetEndpoint); + + // + + const queue: { current: Webhook[] } = { current: [] }; + const runs: Map = new Map(); + + tickRef.timeout = setInterval(async () => { + // NOTE: On next tick, we abort the current abort controller. + if (tickRef.abort != null) { + tickRef.abort.abort(); + } + + const controller = new AbortController(); + + tickRef.abort = controller; + + console.log(`[polling] ${new Date().toISOString()} `.padEnd(100, '=')); + + const tasks: BrowserUse.Tasks.TaskItemView[] = await client.tasks + .list( + { + includeOutputFiles: false, + includeSteps: false, + includeUserUploadedFiles: false, + pageSize: 10, + }, + { + signal: tickRef.abort.signal, + }, + ) + .then((res) => res.items) + .catch((_) => []); + + for (const task of tasks) { + const currentTaskStatus = runs.get(task.id); + + const timestamp = task.finishedAt ? task.finishedAt : task.startedAt; + + if (currentTaskStatus == null) { + // NOTE: The task is new and the CLI hasn't yet captured it in the current run. + queue.current.push({ + type: 'agent.task.status_update', + timestamp, + payload: { + session_id: task.sessionId, + task_id: task.id, + status: task.status, + metadata: task.metadata, + }, + }); + + runs.set(task.id, task.status); + + continue; + } else { + // NOTE: CLI has registered the task in the registry and we need to compare. + if (task.status !== currentTaskStatus) { + queue.current.push({ + type: 'agent.task.status_update', + timestamp, + payload: { + session_id: task.sessionId, + task_id: task.id, + status: task.status, + metadata: task.metadata, + }, + }); + + runs.set(task.id, task.status); + + continue; + } + } + } + + if (queue.current.length === 0) { + return; + } + + const promises = queue.current.map(async (update) => { + const body = JSON.stringify(update); + + const signature = createWebhookSignature({ + payload: update.payload, + timestamp: update.timestamp, + secret, + }); + + try { + const res = await fetch(localTargetURL, { + method: 'POST', + body, + headers: { + 'Content-Type': 'application/json', + // https://docs.browser-use.com/cloud/webhooks#implementing-webhook-verification + 'X-Browser-Use-Timestamp': update.timestamp, + 'X-Browser-Use-Signature': signature, + }, + + signal: controller.signal, + }); + + console.log(`[update] ${update.timestamp} ${update.type} ${res.status}`); + + return { delivery: 'fulfilled', update, status: res.status }; + } catch (err) { + console.log(`[update] ${update.timestamp} ${update.type} failed`); + + return { delivery: 'rejected', update, error: err }; + } + }); + + const delivery = await Promise.all(promises); + + // NOTE: We preserve the rejected updates so we can retry them. + queue.current = delivery.filter((d) => d.delivery === 'rejected').map((d) => d.update); + }, 1_000); + + console.log(`Listening in dev mode at: ${localTargetEndpoint}`); + }); + +process.on('SIGINT', () => { + if (tickRef.abort != null) { + tickRef.abort.abort(); + } + + if (tickRef.timeout) { + clearInterval(tickRef.timeout); + } +}); diff --git a/src/lib/webhooks.ts b/src/lib/webhooks.ts new file mode 100644 index 0000000..63afa00 --- /dev/null +++ b/src/lib/webhooks.ts @@ -0,0 +1,118 @@ +import { createHmac } from 'crypto'; +import { z } from 'zod'; +import stringify from 'fast-json-stable-stringify'; + +// https://docs.browser-use.com/cloud/webhooks + +// + +export const zWebhookTimestamp = z.iso.datetime({ offset: true, local: true }); + +// test + +export const zWebhookTestPayload = z.object({ + test: z.literal('ok'), +}); + +export type WebhookTestPayload = z.infer; + +export const zWebhookTest = z.object({ + type: z.literal('test'), + timestamp: z.iso.datetime({ offset: true }), + payload: zWebhookTestPayload, +}); + +// agent.task.status_update + +export const zWebhookAgentTaskStatusUpdatePayloadMetadata = z.record(z.string(), z.unknown()).optional(); + +export const zWebhookAgentTaskStatusUpdatePayloadStatus = z.literal([ + 'initializing', + 'started', + 'paused', + 'stopped', + 'finished', +]); + +export const zWebhookAgentTaskStatusUpdatePayload = z.object({ + session_id: z.string(), + task_id: z.string(), + status: zWebhookAgentTaskStatusUpdatePayloadStatus, + metadata: zWebhookAgentTaskStatusUpdatePayloadMetadata, +}); + +export type WebhookAgentTaskStatusUpdatePayload = z.infer; + +export const zWebhookAgentTaskStatusUpdate = z.object({ + type: z.literal('agent.task.status_update'), + timestamp: zWebhookTimestamp, + payload: zWebhookAgentTaskStatusUpdatePayload, +}); + +// + +export const zWebhookSchema = z.discriminatedUnion('type', [ + // + zWebhookTest, + zWebhookAgentTaskStatusUpdate, +]); + +export type Webhook = z.infer; + +// Signature + +/** + * Utility function that validates the received Webhook event/ + */ +export async function verifyWebhookEventSignature( + evt: { + evt: string; + signature: string; + timestamp: string; + }, + cfg: { secret: string }, +): Promise<{ ok: true; event: Webhook } | { ok: false }> { + try { + const event = await zWebhookSchema.safeParseAsync(JSON.parse(evt.evt)); + + if (event.success === false) { + return { ok: false }; + } + + const signature = createWebhookSignature({ + payload: event.data.payload, + timestamp: evt.timestamp, + secret: cfg.secret, + }); + + // Compare signatures using timing-safe comparison + if (evt.signature !== signature) { + return { ok: false }; + } + + return { ok: true, event: event.data }; + } catch (err) { + console.error(err); + return { ok: false }; + } +} + +/** + * Creates a webhook signature for the given payload, timestamp, and secret. + */ +export function createWebhookSignature({ + payload, + timestamp, + secret, +}: { + payload: unknown; + timestamp: string; + secret: string; +}): string { + const dump = stringify(payload); + const message = `${timestamp}.${dump}`; + + const hmac = createHmac('sha256', secret); + hmac.update(message); + return hmac.digest('hex'); +} diff --git a/tests/lib/webhooks.test.ts b/tests/lib/webhooks.test.ts new file mode 100644 index 0000000..92f7bd5 --- /dev/null +++ b/tests/lib/webhooks.test.ts @@ -0,0 +1,104 @@ +import { + createWebhookSignature, + verifyWebhookEventSignature, + zWebhookSchema, + zWebhookTimestamp, +} from '../../src/lib/webhooks'; + +describe('webhooks', () => { + describe('parse', () => { + test('timestamp', () => { + expect(zWebhookTimestamp.parse('2025-05-25T09:22:22.269116+00:00')).toBeDefined(); + expect(zWebhookTimestamp.parse('2025-08-15T18:09:11.881540')).toBeDefined(); + }); + + test('agent.task.status_update', () => { + const MOCK: unknown = { + type: 'agent.task.status_update', + timestamp: '2025-05-25T09:22:22.269116+00:00', + payload: { + session_id: 'cd9cc7bf-e3af-4181-80a2-73f083bc94b4', + task_id: '5b73fb3f-a3cb-4912-be40-17ce9e9e1a45', + status: 'finished', + metadata: { + campaign: 'q4-automation', + team: 'marketing', + }, + }, + }; + + const response = zWebhookSchema.parse(MOCK); + + expect(response).toBeDefined(); + }); + + test('test', () => { + const MOCK: unknown = { + type: 'test', + timestamp: '2025-05-25T09:22:22.269116+00:00', + payload: { test: 'ok' }, + }; + + const response = zWebhookSchema.parse(MOCK); + + expect(response).toBeDefined(); + }); + + test('invalid', () => { + const MOCK: unknown = { + type: 'invalid', + timestamp: '2025-05-25T09:22:22.269116+00:00', + payload: { test: 'ok' }, + }; + + expect(() => zWebhookSchema.parse(MOCK)).toThrow(); + }); + }); + + describe('verify', () => { + test('correctly calculates signature', async () => { + const timestamp = '2025-05-26:22:22.269116+00:00'; + + const MOCK = { + type: 'agent.task.status_update', + timestamp: '2025-05-25T09:22:22.269116+00:00', + payload: { + session_id: 'cd9cc7bf-e3af-4181-80a2-73f083bc94b4', + task_id: '5b73fb3f-a3cb-4912-be40-17ce9e9e1a45', + status: 'finished', + metadata: { + campaign: 'q4-automation', + team: 'marketing', + }, + }, + }; + + const signature = createWebhookSignature({ + payload: MOCK.payload, + secret: 'secret', + timestamp, + }); + + const valid = await verifyWebhookEventSignature( + { + evt: JSON.stringify(MOCK), + signature: signature, + timestamp, + }, + { secret: 'secret' }, + ); + + const invalid = await verifyWebhookEventSignature( + { + evt: JSON.stringify(MOCK), + signature: 'invalid', + timestamp, + }, + { secret: 'secret' }, + ); + + expect(valid.ok).toBe(true); + expect(invalid.ok).toBe(false); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 43b21d6..3e4ade1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -350,6 +350,26 @@ dependencies: "@cspotcode/source-map-consumer" "0.8.0" +"@dotenvx/dotenvx@^1.48.4": + version "1.48.4" + resolved "https://registry.yarnpkg.com/@dotenvx/dotenvx/-/dotenvx-1.48.4.tgz#697ca86d3cda1e6a7bde73bf129b9905149916d8" + integrity sha512-GpJWpGVI5JGhNzFlWOjCD3KMiN3xU1US4oLKQ7SiiGru4LvR7sUf3pDMpfjtlgzHStL5ydq4ekfZcRxWpHaJkA== + dependencies: + commander "^11.1.0" + dotenv "^17.2.1" + eciesjs "^0.4.10" + execa "^5.1.1" + fdir "^6.2.0" + ignore "^5.3.0" + object-treeify "1.1.33" + picomatch "^4.0.2" + which "^4.0.0" + +"@ecies/ciphers@^0.2.3": + version "0.2.4" + resolved "https://registry.yarnpkg.com/@ecies/ciphers/-/ciphers-0.2.4.tgz#20a4e51f61d521e5e311eb49385d93d91087de51" + integrity sha512-t+iX+Wf5nRKyNzk8dviW3Ikb/280+aEJAnw9YXvCp2tYGPSkMki+NRY+8aNLmVFv3eNtMdvViPNOPxS8SZNP+w== + "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" @@ -693,6 +713,23 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@noble/ciphers@^1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@noble/ciphers/-/ciphers-1.3.0.tgz#f64b8ff886c240e644e5573c097f86e5b43676dc" + integrity sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw== + +"@noble/curves@^1.9.1": + version "1.9.7" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.9.7.tgz#79d04b4758a43e4bca2cbdc62e7771352fa6b951" + integrity sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw== + dependencies: + "@noble/hashes" "1.8.0" + +"@noble/hashes@1.8.0", "@noble/hashes@^1.8.0": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.8.0.tgz#cee43d801fcef9644b11b8194857695acd5f815a" + integrity sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -937,12 +974,12 @@ dependencies: undici-types "~5.26.4" -"@types/node@^20.17.6": - version "20.17.6" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.17.6.tgz#6e4073230c180d3579e8c60141f99efdf5df0081" - integrity sha512-VEI7OdvK2wP7XHnsuXbAJnEpEkF6NjSN45QJlL4VGqZSXsnicpesdTWsg9RISeSdYd3yeRj/y3k5KGjUXYnFwQ== +"@types/node@^24.3.0": + version "24.3.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-24.3.0.tgz#89b09f45cb9a8ee69466f18ee5864e4c3eb84dec" + integrity sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow== dependencies: - undici-types "~6.19.2" + undici-types "~7.10.0" "@types/stack-utils@^2.0.0": version "2.0.3" @@ -1414,6 +1451,16 @@ commander@^10.0.1: resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== +commander@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-11.1.0.tgz#62fdce76006a68e5c1ab3314dc92e800eb83d906" + integrity sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ== + +commander@^14.0.0: + version "14.0.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-14.0.0.tgz#f244fc74a92343514e56229f16ef5c5e22ced5e9" + integrity sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA== + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -1495,6 +1542,21 @@ diff@^4.0.1: resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== +dotenv@^17.2.1: + version "17.2.1" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-17.2.1.tgz#6f32e10faf014883515538dc922a0fb8765d9b32" + integrity sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ== + +eciesjs@^0.4.10: + version "0.4.15" + resolved "https://registry.yarnpkg.com/eciesjs/-/eciesjs-0.4.15.tgz#8c7191ce425c54627ee5c65328ab54eaa6ed4556" + integrity sha512-r6kEJXDKecVOCj2nLMuXK/FCPeurW33+3JRpfXVbjLja3XUYFfD9I/JBreH6sUyzcm3G/YQboBjMla6poKeSdA== + dependencies: + "@ecies/ciphers" "^0.2.3" + "@noble/ciphers" "^1.3.0" + "@noble/curves" "^1.9.1" + "@noble/hashes" "^1.8.0" + electron-to-chromium@^1.4.601: version "1.4.614" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.614.tgz#2fe789d61fa09cb875569f37c309d0c2701f91c0" @@ -1656,7 +1718,7 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== -execa@^5.0.0: +execa@^5.0.0, execa@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== @@ -1732,6 +1794,11 @@ fb-watchman@^2.0.0: dependencies: bser "2.1.1" +fdir@^6.2.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350" + integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg== + fflate@^0.8.2: version "0.8.2" resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.2.tgz#fc8631f5347812ad6028bbe4a2308b2792aa1dea" @@ -1923,7 +1990,7 @@ ignore-walk@^5.0.1: dependencies: minimatch "^5.0.1" -ignore@^5.2.0, ignore@^5.3.1: +ignore@^5.2.0, ignore@^5.3.0, ignore@^5.3.1: version "5.3.2" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== @@ -2016,6 +2083,11 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== +isexe@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-3.1.1.tgz#4a407e2bd78ddfb14bea0c27c6f7072dde775f0d" + integrity sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ== + istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz#2d166c4b0644d43a39f04bf6c2edd1e585f31756" @@ -2729,6 +2801,11 @@ object-assign@^4.0.1: resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== +object-treeify@1.1.33: + version "1.1.33" + resolved "https://registry.yarnpkg.com/object-treeify/-/object-treeify-1.1.33.tgz#f06fece986830a3cba78ddd32d4c11d1f76cdf40" + integrity sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A== + once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -2871,6 +2948,11 @@ picomatch@^2.0.4, picomatch@^2.2.3, picomatch@^2.3.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== +picomatch@^4.0.2: + version "4.0.3" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042" + integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q== + pirates@^4.0.4: version "4.0.6" resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9" @@ -3353,10 +3435,10 @@ undici-types@~5.26.4: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== -undici-types@~6.19.2: - version "6.19.8" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" - integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== +undici-types@~7.10.0: + version "7.10.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.10.0.tgz#4ac2e058ce56b462b056e629cc6a02393d3ff350" + integrity sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag== unicode-emoji-modifier-base@^1.0.0: version "1.0.0" @@ -3416,6 +3498,13 @@ which@^2.0.1: dependencies: isexe "^2.0.0" +which@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/which/-/which-4.0.0.tgz#cd60b5e74503a3fbcfbf6cd6b4138a8bae644c1a" + integrity sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg== + dependencies: + isexe "^3.1.1" + wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"