From 4c567e247ea2c2a7906cd8122affcfed56fd1e75 Mon Sep 17 00:00:00 2001 From: Alexander Hesse Date: Wed, 25 Jan 2023 17:35:52 +0100 Subject: [PATCH] INIT: move uptime migration helper to own repository --- .env.sample | 8 ++ .gitignore | 3 + .nvmrc | 1 + README.md | 53 +++++++++++ package.json | 28 ++++++ src/index.ts | 238 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/types.ts | 43 +++++++++ tsconfig.json | 16 ++++ yarn.lock | 191 ++++++++++++++++++++++++++++++++++++++++ 9 files changed, 581 insertions(+) create mode 100644 .env.sample create mode 100644 .gitignore create mode 100644 .nvmrc create mode 100644 README.md create mode 100644 package.json create mode 100644 src/index.ts create mode 100644 src/types.ts create mode 100644 tsconfig.json create mode 100644 yarn.lock diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..9f69425 --- /dev/null +++ b/.env.sample @@ -0,0 +1,8 @@ +# sample configuration for uptimeRobot to kuma migration + +# get from UptimeRobotApi +UPTIME_ROBOT_API_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxx" + +KUMA_URL="http://localhost:3001" +KUMA_USERNAME="admin" +KUMA_PASSWORD="password2" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a8b1485 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +dist/ +node_modules/ +.env diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..59f4a2f --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v18.13.0 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..4b0f932 --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ +# UptimeRobot to Kuma migration + +We migrated from UptimeRobot to UptimeKuma, but there was no fast way to achieve this, so +we wrote our own small migration helper. + +## Getting started + +Copy the `.env.sample` as `.env` and enter your UptimeRobot API key. + +For testing, you can simply start UptimeKuma via Docker: + +```shell +docker run --rm -p 3001:3001 --name uptime-kuma louislam/uptime-kuma:1 +``` + +Ensure you finished the initial setup (simply open [localhost:3001](localhost:3001) in your browser) and +updated the credentials in the `.env` file. + +To start the migration run: + +```bash +# copy all your UptimeRobot monitors to your Kuma installation +yarn copy-monitors + +# disable all UptimeRobot monitors +yarn disable-uptime-robot + +# delete all your monitors from UptimeRobot +# DANGER!!! This is can not be undone +yarn delete-uptime-robot +``` + +## Production Migration + +**Important Node:** This migration helper was writen specially for our use-case. So not all UptimeRobot +scenarios and features are implemented. So no garantie this will work 100% for you. + +**Pro Tipp:** Before migrating, create a default notification that will get used as default. + +## Architecture + +### Fetching from UptimeRobot + +This part was quite easy, because UptimeRobot got a good REST-API to fetch all monitors from + +### Creating the monitors in Kuma + +This was the hard part. Currently, Kuma does not provide any form of API. In the first version of this migration +helper, I tried to hook into the websocket connection of the UI and create monitors that way. This was really instabile +and resulted in many non-deterministic errors. + +For this reason I switched to Playwright. This allows us the remote-control a browser, which will create +the monitors via the Kuma-UI. diff --git a/package.json b/package.json new file mode 100644 index 0000000..2eafb66 --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "kuma-migration", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "build": "tsc", + "copy-monitors": "yarn build && node dist/index.js copy-monitors", + "disable-uptime-robot": "yarn build && node dist/index.js disable-uptime-robot", + "delete-uptime-robot": "yarn build && node dist/index.js delete-uptime-robot", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "MIT", + "devDependencies": { + "@types/node": "^18.11.18", + "playwright": "^1.29.2", + "ts-node": "^10.9.1", + "typescript": "^4.9.4" + }, + "engines": { + "node": "18.13.0" + }, + "dependencies": { + "axios": "^1.2.3", + "dotenv": "^16.0.3" + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..603612b --- /dev/null +++ b/src/index.ts @@ -0,0 +1,238 @@ +import dotenv from "dotenv" + +import { + UptimeRobotMonitor, + UptimeRobotGetMonitorResponse, + UptimeRobotEditMonitorResponse, + UptimeRobotDeleteMonitorResponse +} from "./types"; +import {chromium, Page} from 'playwright'; +import axios from "axios"; + +dotenv.config() + +const UPTIME_ROBOT_API_KEY = process.env.UPTIME_ROBOT_API_KEY + +const KUMA_URL = process.env.KUMA_URL +const KUMA_USERNAME = process.env.KUMA_USERNAME +const KUMA_PASSWORD = process.env.KUMA_PASSWORD + + +if ( + !UPTIME_ROBOT_API_KEY || + !KUMA_URL || + !KUMA_USERNAME || + !KUMA_PASSWORD +) { + console.warn('Required env variables are missing!') + console.warn('Copy ".env.sample" as ".env"') + process.exit(1) +} + +const UPTIME_ROBOT_GET_MONITOR_API_PATH = "https://api.uptimerobot.com/v2/getMonitors?api_key=" + +const loadMonitorsFromUptimeRobot = async () => { + const monitors: UptimeRobotMonitor[] = [] + + let monitorCount = 0 + let currentOffset = 0 + let totalMonitors = 9999999 + + while (monitorCount < totalMonitors) { + const response = await axios.post( + UPTIME_ROBOT_GET_MONITOR_API_PATH + UPTIME_ROBOT_API_KEY, + `offset=${currentOffset}` + ) + + const jsonResponse = response.data as UptimeRobotGetMonitorResponse + + totalMonitors = jsonResponse.pagination.total + currentOffset += 50 + monitorCount += jsonResponse.monitors.length + monitors.push(...jsonResponse.monitors) + + } + + return monitors +} + +const UPTIME_ROBOT_DISABLE_MONITOR_API_PATH = "https://api.uptimerobot.com/v2/editMonitor" + +const disableUptimeRobotMonitor = async (monitor: UptimeRobotMonitor) => { + const response = await axios.post( + UPTIME_ROBOT_DISABLE_MONITOR_API_PATH, + new URLSearchParams({ + 'api_key': UPTIME_ROBOT_API_KEY, + 'format': 'json', + 'id': `${monitor.id}`, + 'status': '0' + }), + { + headers: { + 'Cache-Control': 'no-cache' + } + } + ); + + const responseJson = response.data as UptimeRobotEditMonitorResponse + if (responseJson?.stat === "fail") { + console.error(`Failed to edit monitor '${monitor.friendly_name}' due to '${responseJson.error?.type}'`) + process.exit(1) + } else { + console.log(`Disabled monitor "${monitor.friendly_name}"`) + } +} + +const UPTIME_ROBOT_DELETE_MONITOR_API_PATH = "https://api.uptimerobot.com/v2/deleteMonitor" +const deleteUptimeRobotMonitor = async (monitor: UptimeRobotMonitor) => { + const response = await axios.post( + UPTIME_ROBOT_DELETE_MONITOR_API_PATH, + new URLSearchParams({ + 'api_key': UPTIME_ROBOT_API_KEY, + 'format': 'json', + 'id': `${monitor.id}`, + }), + { + headers: { + 'Cache-Control': 'no-cache' + } + } + ) + const responseJson = response.data as UptimeRobotDeleteMonitorResponse + if (responseJson?.stat === "fail") { + console.error(`Failed to delete monitor '${monitor.friendly_name}' due to '${responseJson.error?.type}'`) + process.exit(1) + } else { + console.log(`Deleted monitor "${monitor.friendly_name}"`) + } +} + +const startPlaywright = async () => chromium.launch({headless: false}) + +const ensureLoggedIn = async (page: Page) => { + const loginButton = await page.getByText("Login") + + if (loginButton == null) { + console.log('already logged in ...') + return + } + + await page.getByPlaceholder('Username').type(KUMA_USERNAME) + await page.getByPlaceholder('Password').type(KUMA_PASSWORD) + + await loginButton.click() + await page.waitForLoadState("domcontentloaded") +} + +const createMonitor = async (page: Page, monitor: UptimeRobotMonitor) => { + await page.waitForLoadState('domcontentloaded') + await page.waitForLoadState('networkidle') + + await page.getByText('Add New Monitor').first().click() + + const createButton = await page.$('a:text(" Add New Monitor")') + await createButton?.click() + + + await page.waitForTimeout(100) + await page.getByLabel('Friendly Name').first().fill(monitor.friendly_name) + + switch (monitor.type) { + case 1: + await createMonitorHTTP(page, monitor) + break + case 2: + await createMonitorKeyword(page, monitor) + break + case 4: + await createMonitorPort(page, monitor) + break + default: + console.log(`Monitor type ${monitor.type} of ${monitor.friendly_name} is not supported`) + break + } + + await page.getByLabel('Heartbeat Interval (Check every').fill(String(monitor.interval)) + + const saveButton = await page.$('button#monitor-submit-btn') + await saveButton?.click() + + await page.waitForLoadState("domcontentloaded") + await page.waitForLoadState("networkidle") + + await page.waitForTimeout(400) + + console.log('Created Monitor ', monitor.friendly_name) +} + +const createMonitorHTTP = async (page: Page, monitor: UptimeRobotMonitor) => { + await page.getByLabel("Monitor Type").selectOption("HTTP(s)") + await page.getByLabel('URL').fill(monitor.url) +} + +const createMonitorKeyword = async (page: Page, monitor: UptimeRobotMonitor) => { + await page.getByLabel("Monitor Type").selectOption("HTTP(s) - Keyword") + await page.getByLabel('URL').fill(monitor.url) + await page.getByLabel('Keyword').fill(monitor.keyword_value) +} + +const createMonitorPort = async (page: Page, monitor: UptimeRobotMonitor) => { + await page.getByLabel("Monitor Type").selectOption("TCP Port") + await page.getByLabel('Hostname').fill(monitor.url) + await page.getByLabel('Port').fill(String(monitor.port)) +} + +const copyMonitors = async () => { + const monitors = await loadMonitorsFromUptimeRobot() + console.log(`Found ${monitors.length} monitors`) + + const browser = await startPlaywright() + const page = await browser.newPage() + await page.goto(KUMA_URL) + + await ensureLoggedIn(page) + + for (let monitor of monitors) { + await createMonitor(page, monitor) + } + + await browser.close() +} + +const disableUptimeRobot = async () => { + const monitors = await loadMonitorsFromUptimeRobot() + console.log(`Found ${monitors.length} monitors`) + + for (let monitor of monitors) { + await disableUptimeRobotMonitor(monitor) + } +} + +const deleteUptimeRobotMonitors = async () => { + const monitors = await loadMonitorsFromUptimeRobot() + console.log(`Found ${monitors.length} monitors`) + + for (let monitor of monitors) { + await deleteUptimeRobotMonitor(monitor) + } +} + +const run = async () => { + const task = process.argv[2] + + switch (task) { + case 'copy-monitors': + await copyMonitors() + break + case 'disable-uptime-robot': + await disableUptimeRobot() + break + case 'delete-uptime-robot': + await deleteUptimeRobotMonitors() + break + default: + console.error(`Task '${task} is not supported ...'`) + } +} + +run() diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..c4389c8 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,43 @@ +export interface UptimeRobotMonitor { + id: number; + friendly_name: string; + url: string; + type: number; + sub_type: string; + keyword_type: null; + keyword_case_type: number; + keyword_value: string; + port: string; + interval: number; + timeout: number; + status: number; + create_datetime: number; +} + +export interface UptimeRobotPagination { + offset: number; + limit: number; + total: number; +} + +export interface UptimeRobotGetMonitorResponse { + stat: 'OK' | 'fail'; + pagination: UptimeRobotPagination; + monitors: UptimeRobotMonitor[]; +} + +export interface UptimeRobotEditMonitorResponse { + stat: 'OK' | 'fail'; + error?: { + type: 'not_authorized', + message: string + } +} + +export interface UptimeRobotDeleteMonitorResponse { + stat: 'OK' | 'fail'; + error?: { + type: 'not_authorized', + message: string + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..f1711bc --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "module": "NodeNext", + "target": "ESNext", + "lib": ["ESNext", "DOM"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "noImplicitAny": true, + "sourceMap": false, + "outDir": "dist", + }, +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..e204abf --- /dev/null +++ b/yarn.lock @@ -0,0 +1,191 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + +"@jridgewell/resolve-uri@^3.0.3": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" + integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== + +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.4.14" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" + integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== + +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@tsconfig/node10@^1.0.7": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" + integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e" + integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== + +"@types/node@^18.11.18": + version "18.11.18" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.18.tgz#8dfb97f0da23c2293e554c5a50d61ef134d7697f" + integrity sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA== + +acorn-walk@^8.1.1: + version "8.2.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" + integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== + +acorn@^8.4.1: + version "8.8.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.1.tgz#0a3f9cbecc4ec3bea6f0a80b66ae8dd2da250b73" + integrity sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA== + +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +axios@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.2.3.tgz#31a3d824c0ebf754a004b585e5f04a5f87e6c4ff" + integrity sha512-pdDkMYJeuXLZ6Xj/Q5J3Phpe+jbGdsSzlQaFVkMQzRUL05+6+tetX8TV3p4HrU4kzuO9bt+io/yGQxuyxA/xcw== + dependencies: + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + +dotenv@^16.0.3: + version "16.0.3" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07" + integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ== + +follow-redirects@^1.15.0: + version "1.15.2" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" + integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== + +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +playwright-core@1.29.2: + version "1.29.2" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.29.2.tgz#2e8347e7e8522409f22b244e600e703b64022406" + integrity sha512-94QXm4PMgFoHAhlCuoWyaBYKb92yOcGVHdQLoxQ7Wjlc7Flg4aC/jbFW7xMR52OfXMVkWicue4WXE7QEegbIRA== + +playwright@^1.29.2: + version "1.29.2" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.29.2.tgz#d6a0a3e8e44f023f7956ed19ffa8af915a042769" + integrity sha512-hKBYJUtdmYzcjdhYDkP9WGtORwwZBBKAW8+Lz7sr0ZMxtJr04ASXVzH5eBWtDkdb0c3LLFsehfPBTRfvlfKJOA== + dependencies: + playwright-core "1.29.2" + +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + +ts-node@^10.9.1: + version "10.9.1" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b" + integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + +typescript@^4.9.4: + version "4.9.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.4.tgz#a2a3d2756c079abda241d75f149df9d561091e78" + integrity sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg== + +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==