diff --git a/examples/basic/vpn.ts b/examples/basic/vpn.ts new file mode 100644 index 000000000..75de44101 --- /dev/null +++ b/examples/basic/vpn.ts @@ -0,0 +1,54 @@ +import { DemandSpec, GolemNetwork } from "@golem-sdk/golem-js"; +import { pinoPrettyLogger } from "@golem-sdk/pino-logger"; + +(async () => { + const glm = new GolemNetwork({ + logger: pinoPrettyLogger({ + level: "info", + }), + }); + + try { + await glm.connect(); + const network = await glm.createNetwork({ ip: "192.168.7.0/24" }); + const demand: DemandSpec = { + demand: { + activity: { imageTag: "golem/alpine:latest" }, + }, + market: { + maxAgreements: 2, + rentHours: 0.5, + pricing: { + model: "linear", + maxStartPrice: 0.5, + maxCpuPerHourPrice: 1.0, + maxEnvPerHourPrice: 0.5, + }, + }, + network, + }; + // create a pool that can grow up to 3 leases at the same time + const pool = await glm.manyOf({ + concurrency: 2, + demand, + }); + const lease1 = await pool.acquire(); + const lease2 = await pool.acquire(); + const exe1 = await lease1.getExeUnit(); + const exe2 = await lease2.getExeUnit(); + await exe1 + .run(`ping ${exe2.getIp()} -c 4`) + .then((res) => console.log(`Response from provider: ${exe1.provider.name} (ip: ${exe1.getIp()})`, res.stdout)); + await exe2 + .run(`ping ${exe1.getIp()} -c 4`) + .then((res) => console.log(`Response from provider: ${exe2.provider.name} (ip: ${exe2.getIp()})`, res.stdout)); + await pool.destroy(lease1); + await pool.destroy(lease2); + + await glm.destroyNetwork(network); + } catch (err) { + console.error("Failed to run the example", err); + } finally { + await glm.disconnect(); + } +})().catch(console.error); diff --git a/examples/experimental/deployment/new-api.ts b/examples/experimental/deployment/new-api.ts index 7e204cfe9..c10ea5861 100644 --- a/examples/experimental/deployment/new-api.ts +++ b/examples/experimental/deployment/new-api.ts @@ -16,7 +16,7 @@ async function main() { builder .createNetwork("basic", { - networkOwnerId: "test", + ip: "192.168.7.0/24", }) .createActivityPool("app", { demand: { diff --git a/examples/package-lock.json b/examples/package-lock.json deleted file mode 100644 index 7d5c3b88c..000000000 --- a/examples/package-lock.json +++ /dev/null @@ -1,1624 +0,0 @@ -{ - "name": "golem-js-examples", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "golem-js-examples", - "version": "0.0.0", - "license": "LGPL-3.0", - "dependencies": { - "@golem-sdk/golem-js": "file:..", - "@golem-sdk/pino-logger": "^1.0.1", - "commander": "^12.0.0", - "express": "^4.18.2", - "tsx": "^4.7.1" - }, - "devDependencies": { - "@types/node": "20", - "typescript": "^5.3.3" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "..": { - "name": "@golem-sdk/golem-js", - "version": "0.8.0", - "license": "LGPL-3.0", - "workspaces": [ - "examples/" - ], - "dependencies": { - "async-lock": "^1.4.1", - "async-retry": "^1.3.3", - "axios": "^1.6.7", - "bottleneck": "^2.19.5", - "debug": "^4.3.4", - "decimal.js-light": "^2.5.1", - "eventemitter3": "^5.0.1", - "eventsource": "^2.0.2", - "flatbuffers": "^24.3.7", - "generic-pool": "^3.9.0", - "ip-num": "^1.5.1", - "js-sha3": "^0.9.3", - "rxjs": "^7.8.1", - "semver": "^7.5.4", - "tmp": "^0.2.2", - "uuid": "^9.0.1", - "ws": "^8.16.0", - "ya-ts-client": "^1.1.1-beta.1" - }, - "devDependencies": { - "@commitlint/cli": "^19.0.3", - "@commitlint/config-conventional": "^19.0.3", - "@johanblumenberg/ts-mockito": "^1.0.41", - "@rollup/plugin-alias": "^5.1.0", - "@rollup/plugin-commonjs": "^25.0.7", - "@rollup/plugin-json": "^6.1.0", - "@rollup/plugin-node-resolve": "^15.2.3", - "@rollup/plugin-terser": "^0.4.4", - "@rollup/plugin-typescript": "^11.1.6", - "@types/async-lock": "^1.4.2", - "@types/async-retry": "^1.4.8", - "@types/debug": "^4.1.12", - "@types/eventsource": "^1.1.15", - "@types/express": "^4.17.21", - "@types/jest": "^29.5.12", - "@types/node": "^20.11.20", - "@types/semver": "^7.5.8", - "@types/supertest": "^6.0.2", - "@types/tmp": "^0.2.6", - "@types/uuid": "^9.0.8", - "@types/ws": "^8.5.10", - "@typescript-eslint/eslint-plugin": "^7.1.0", - "@typescript-eslint/parser": "^7.1.0", - "buffer": "^6.0.3", - "cross-env": "^7.0.3", - "cypress": "^13.6.6", - "cypress-log-to-output": "^1.1.2", - "eslint": "^8.57.0", - "eslint-config-prettier": "^9.1.0", - "express": "^4.18.2", - "husky": "^9.0.11", - "jest": "^29.7.0", - "jest-junit": "^16.0.0", - "prettier": "^3.2.5", - "rollup": "^4.12.0", - "rollup-plugin-filesize": "^10.0.0", - "rollup-plugin-ignore": "^1.0.10", - "rollup-plugin-polyfill-node": "^0.13.0", - "rollup-plugin-visualizer": "^5.12.0", - "semantic-release": "^23.0.2", - "stream-browserify": "^3.0.0", - "supertest": "^6.3.4", - "ts-jest": "^29.1.2", - "ts-loader": "^9.5.1", - "tsconfig-paths": "^4.2.0", - "tslint-config-prettier": "^1.18.0", - "tsx": "^4.7.1", - "typedoc": "^0.25.9", - "typedoc-plugin-markdown": "^3.17.1", - "typedoc-plugin-merge-modules": "^5.1.0", - "typedoc-theme-hierarchy": "4.1.2", - "typescript": "^5.3.3" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-darwin-x64": "^4", - "@rollup/rollup-win32-arm64-msvc": "^4", - "@rollup/rollup-win32-x64-msvc": "^4" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", - "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", - "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", - "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", - "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", - "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", - "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", - "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", - "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", - "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", - "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", - "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", - "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", - "cpu": [ - "loong64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", - "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", - "cpu": [ - "mips64el" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", - "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", - "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", - "cpu": [ - "riscv64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", - "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", - "cpu": [ - "s390x" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", - "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", - "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", - "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", - "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", - "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", - "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", - "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@golem-sdk/golem-js": { - "resolved": "..", - "link": true - }, - "node_modules/@golem-sdk/pino-logger": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@golem-sdk/pino-logger/-/pino-logger-1.0.1.tgz", - "integrity": "sha512-GysE4ju++ONBSw+VAz25YMcS05jpLpiDninEL6sLZ4Ia3VQJ6eonT8k+NNEcgqcEFf0j9PcWf07fj6uJn4gXXw==", - "dependencies": { - "pino": "^8.20.0", - "pino-pretty": "^11.0.0" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "@golem-sdk/golem-js": "< 4" - } - }, - "node_modules/@types/node": { - "version": "20.12.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz", - "integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==", - "dev": true, - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" - }, - "node_modules/atomic-sleep": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", - "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==" - }, - "node_modules/commander": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.0.0.tgz", - "integrity": "sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA==", - "engines": { - "node": ">=18" - } - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" - }, - "node_modules/dateformat": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", - "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", - "engines": { - "node": "*" - } - }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" - }, - "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/esbuild": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", - "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.19.12", - "@esbuild/android-arm": "0.19.12", - "@esbuild/android-arm64": "0.19.12", - "@esbuild/android-x64": "0.19.12", - "@esbuild/darwin-arm64": "0.19.12", - "@esbuild/darwin-x64": "0.19.12", - "@esbuild/freebsd-arm64": "0.19.12", - "@esbuild/freebsd-x64": "0.19.12", - "@esbuild/linux-arm": "0.19.12", - "@esbuild/linux-arm64": "0.19.12", - "@esbuild/linux-ia32": "0.19.12", - "@esbuild/linux-loong64": "0.19.12", - "@esbuild/linux-mips64el": "0.19.12", - "@esbuild/linux-ppc64": "0.19.12", - "@esbuild/linux-riscv64": "0.19.12", - "@esbuild/linux-s390x": "0.19.12", - "@esbuild/linux-x64": "0.19.12", - "@esbuild/netbsd-x64": "0.19.12", - "@esbuild/openbsd-x64": "0.19.12", - "@esbuild/sunos-x64": "0.19.12", - "@esbuild/win32-arm64": "0.19.12", - "@esbuild/win32-ia32": "0.19.12", - "@esbuild/win32-x64": "0.19.12" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.2", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.6.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.2.0", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.7", - "qs": "6.11.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/fast-copy": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz", - "integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==" - }, - "node_modules/fast-redact": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", - "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", - "engines": { - "node": ">=6" - } - }, - "node_modules/fast-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" - }, - "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-tsconfig": { - "version": "4.7.3", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.3.tgz", - "integrity": "sha512-ZvkrzoUA0PQZM6fy6+/Hce561s+faD1rsNwhnO5FelNjyy7EMGJ3Rz1AQ8GYDWjhRs/7dBLOEJvhK8MiEJOAFg==", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, - "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dependencies": { - "get-intrinsic": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/help-me": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", - "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==" - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/joycon": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", - "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", - "engines": { - "node": ">=10" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-exit-leak-free": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", - "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" - }, - "node_modules/pino": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-8.21.0.tgz", - "integrity": "sha512-ip4qdzjkAyDDZklUaZkcRFb2iA118H9SgRh8yzTkSQK8HilsOJF7rSY8HoW5+I0M46AZgX/pxbprf2vvzQCE0Q==", - "dependencies": { - "atomic-sleep": "^1.0.0", - "fast-redact": "^3.1.1", - "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "^1.2.0", - "pino-std-serializers": "^6.0.0", - "process-warning": "^3.0.0", - "quick-format-unescaped": "^4.0.3", - "real-require": "^0.2.0", - "safe-stable-stringify": "^2.3.1", - "sonic-boom": "^3.7.0", - "thread-stream": "^2.6.0" - }, - "bin": { - "pino": "bin.js" - } - }, - "node_modules/pino-abstract-transport": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz", - "integrity": "sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==", - "dependencies": { - "readable-stream": "^4.0.0", - "split2": "^4.0.0" - } - }, - "node_modules/pino-pretty": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-11.0.0.tgz", - "integrity": "sha512-YFJZqw59mHIY72wBnBs7XhLGG6qpJMa4pEQTRgEPEbjIYbng2LXEZZF1DoyDg9CfejEy8uZCyzpcBXXG0oOCwQ==", - "dependencies": { - "colorette": "^2.0.7", - "dateformat": "^4.6.3", - "fast-copy": "^3.0.0", - "fast-safe-stringify": "^2.1.1", - "help-me": "^5.0.0", - "joycon": "^3.1.1", - "minimist": "^1.2.6", - "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "^1.0.0", - "pump": "^3.0.0", - "readable-stream": "^4.0.0", - "secure-json-parse": "^2.4.0", - "sonic-boom": "^3.0.0", - "strip-json-comments": "^3.1.1" - }, - "bin": { - "pino-pretty": "bin.js" - } - }, - "node_modules/pino-std-serializers": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz", - "integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==" - }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/process-warning": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", - "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==" - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/quick-format-unescaped": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", - "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/readable-stream": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", - "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", - "dependencies": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10", - "string_decoder": "^1.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/real-require": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", - "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", - "engines": { - "node": ">= 12.13.0" - } - }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/safe-stable-stringify": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", - "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", - "engines": { - "node": ">=10" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "node_modules/secure-json-parse": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", - "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==" - }, - "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", - "dependencies": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.18.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", - "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/sonic-boom": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.8.1.tgz", - "integrity": "sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg==", - "dependencies": { - "atomic-sleep": "^1.0.0" - } - }, - "node_modules/split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", - "engines": { - "node": ">= 10.x" - } - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/thread-stream": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.7.0.tgz", - "integrity": "sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw==", - "dependencies": { - "real-require": "^0.2.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/tsx": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.7.2.tgz", - "integrity": "sha512-BCNd4kz6fz12fyrgCTEdZHGJ9fWTGeUzXmQysh0RVocDY3h4frk05ZNCXSy4kIenF7y/QnrdiVpTsyNRn6vlAw==", - "dependencies": { - "esbuild": "~0.19.10", - "get-tsconfig": "^4.7.2" - }, - "bin": { - "tsx": "dist/cli.mjs" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typescript": { - "version": "5.4.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", - "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - } - } -} diff --git a/examples/package.json b/examples/package.json index 9f289809e..3c0f0b8f9 100644 --- a/examples/package.json +++ b/examples/package.json @@ -7,6 +7,7 @@ "scripts": { "basic-one-of": "tsx basic/one-of.ts", "basic-many-of": "tsx basic/many-of.ts", + "basic-vpn": "tsx basic/vpn.ts", "advanced-hello-world": "tsx advanced/hello-world.ts", "advanced-manual-pools": "tsx advanced/manual-pools.ts", "local-image": "tsx advanced/local-image/serveLocalGvmi.ts", diff --git a/src/activity/activity.module.ts b/src/activity/activity.module.ts index dd0c41aea..99a47df12 100644 --- a/src/activity/activity.module.ts +++ b/src/activity/activity.module.ts @@ -121,6 +121,7 @@ export class ActivityModuleImpl implements ActivityModule { this.services.yagna.activity.control, this.services.yagna.activity.exec, activity, + this.services.networkApi, options, ); diff --git a/src/activity/work/work.ts b/src/activity/work/work.ts index c476a5f12..f8365d630 100644 --- a/src/activity/work/work.ts +++ b/src/activity/work/work.ts @@ -25,6 +25,7 @@ import { AgreementDTO } from "../../market/agreement/service"; import { ActivityApi } from "ya-ts-client"; import { YagnaExeScriptObserver } from "../../shared/yagna"; import { ExeScriptExecutor } from "../exe-script-executor"; +import { INetworkApi } from "../../network/api"; export type Worker = (ctx: WorkContext) => Promise; @@ -75,6 +76,7 @@ export class WorkContext { public readonly activityControl: ActivityApi.RequestorControlService, public readonly execObserver: YagnaExeScriptObserver, public readonly activity: Activity, + private readonly networkApi: INetworkApi, private options?: WorkOptions, executionOptions?: ExecutionConfig, ) { @@ -359,7 +361,7 @@ export class WorkContext { this.activity.getProviderInfo(), ); - return this.networkNode.getWebsocketUri(port); + return this.networkApi.getWebsocketUri(this.networkNode, port); } getIp(): string { @@ -371,7 +373,7 @@ export class WorkContext { this.activity, this.activity.getProviderInfo(), ); - return this.networkNode.ip.toString(); + return this.networkNode.ip; } /** diff --git a/src/experimental/deployment/builder.test.ts b/src/experimental/deployment/builder.test.ts index 9f6529f6f..b0a2fc165 100644 --- a/src/experimental/deployment/builder.test.ts +++ b/src/experimental/deployment/builder.test.ts @@ -57,10 +57,10 @@ describe("Deployment builder", () => { expect(() => { builder .createNetwork("my-network", { - networkOwnerId: "test", + id: "test", }) .createNetwork("my-network", { - networkOwnerId: "test", + id: "test", }); }).toThrow(new GolemConfigError(`Network with name my-network already exists`)); }); @@ -69,7 +69,7 @@ describe("Deployment builder", () => { expect(() => { builder .createNetwork("existing-network", { - networkOwnerId: "test", + id: "test", }) .createActivityPool("my-pool", { demand: { diff --git a/src/experimental/deployment/builder.ts b/src/experimental/deployment/builder.ts index 74257e170..3069d8099 100644 --- a/src/experimental/deployment/builder.ts +++ b/src/experimental/deployment/builder.ts @@ -64,6 +64,7 @@ export class GolemDeploymentBuilder { payment: this.glm.payment, market: this.glm.market, activity: this.glm.activity, + network: this.glm.network, }, { dataTransferProtocol: this.glm.options.dataTransferProtocol ?? "gftp", diff --git a/src/experimental/deployment/deployment.ts b/src/experimental/deployment/deployment.ts index 53c054f25..3d8c138cd 100644 --- a/src/experimental/deployment/deployment.ts +++ b/src/experimental/deployment/deployment.ts @@ -2,7 +2,7 @@ import { GolemAbortError, GolemUserError } from "../../shared/error/golem-error" import { defaultLogger, Logger, YagnaApi } from "../../shared/utils"; import { EventEmitter } from "eventemitter3"; import { ActivityModule } from "../../activity"; -import { Network, NetworkOptions } from "../../network"; +import { Network, NetworkOptions, NetworkModule } from "../../network"; import { GftpStorageProvider, StorageProvider, WebSocketBrowserStorageProvider } from "../../shared/storage"; import { validateDeployment } from "./validate-deployment"; import { DemandBuildParams, DraftOfferProposalPool, MarketModule } from "../../market"; @@ -82,6 +82,7 @@ export class Deployment { market: MarketModule; activity: ActivityModule; payment: PaymentModule; + network: NetworkModule; }; constructor( @@ -92,6 +93,7 @@ export class Deployment { market: MarketModule; activity: ActivityModule; payment: PaymentModule; + network: NetworkModule; }, options: DeploymentOptions, ) { @@ -143,7 +145,7 @@ export class Deployment { await this.dataTransferProtocol.init(); for (const network of this.components.networks) { - const networkInstance = await Network.create(this.yagnaApi, network.options); + const networkInstance = await this.modules.network.createNetwork(network.options); this.networks.set(network.name, networkInstance); } @@ -153,7 +155,6 @@ export class Deployment { expirationSec: 30 * 60, // 30 minutes }); - // TODO: add pool to network // TODO: pass dataTransferProtocol to pool for (const pool of this.components.activityPools) { const { demandBuildOptions, leaseProcessPoolOptions } = this.prepareParams(pool.options); @@ -206,7 +207,9 @@ export class Deployment { ); await Promise.allSettled(stopPools); - const stopNetworks: Promise[] = Array.from(this.networks.values()).map((network) => network.remove()); + const stopNetworks: Promise[] = Array.from(this.networks.values()).map((network) => + this.modules.network.removeNetwork(network), + ); await Promise.allSettled(stopNetworks); this.state = DeploymentState.STOPPED; @@ -252,6 +255,7 @@ export class Deployment { : typeof options.deployment?.replicas === "object" ? options.deployment?.replicas : { min: 1, max: 1 }; + const network = options.deployment?.network ? this.networks.get(options.deployment?.network) : undefined; return { demandBuildOptions: { demand: options.demand, @@ -260,6 +264,7 @@ export class Deployment { leaseProcessPoolOptions: { agreementOptions: { invoiceFilter: options.payment?.invoiceFilter }, replicas, + network, }, }; } diff --git a/src/experimental/job/job.test.ts b/src/experimental/job/job.test.ts index 0fc03c646..e57f84c92 100644 --- a/src/experimental/job/job.test.ts +++ b/src/experimental/job/job.test.ts @@ -1,7 +1,6 @@ import { Job } from "./job"; import { Agreement, AgreementPoolService } from "../../market/agreement"; import { WorkContext } from "../../activity/work"; -import { NetworkNode, NetworkService } from "../../network"; import { Activity, IActivityApi } from "../../activity"; import { anything, imock, instance, mock, verify, when } from "@johanblumenberg/ts-mockito"; import { Logger } from "../../shared/utils"; @@ -22,10 +21,8 @@ describe.skip("Job", () => { describe("cancel()", () => { it("stops the activity and releases the agreement when canceled", async () => { jest.spyOn(AgreementPoolService.prototype, "run").mockResolvedValue(); - jest.spyOn(NetworkService.prototype, "run").mockResolvedValue(); jest.spyOn(WorkContext.prototype, "before").mockResolvedValue(); jest.spyOn(AgreementPoolService.prototype, "releaseAgreement").mockResolvedValue(); - jest.spyOn(NetworkService.prototype, "addNode").mockResolvedValue({} as unknown as NetworkNode); const mockAgreement = { id: "test_agreement_id", diff --git a/src/experimental/job/job.ts b/src/experimental/job/job.ts index 1f08ed343..d7afd6f31 100644 --- a/src/experimental/job/job.ts +++ b/src/experimental/job/job.ts @@ -1,13 +1,13 @@ import { Worker, WorkOptions } from "../../activity/work"; import { LegacyAgreementServiceOptions } from "../../market/agreement"; import { DemandSpec } from "../../market"; -import { NetworkOptions } from "../../network"; import { PaymentModuleOptions } from "../../payment"; import { EventEmitter } from "eventemitter3"; import { GolemAbortError, GolemUserError } from "../../shared/error/golem-error"; import { GolemNetwork } from "../../golem-network/golem-network"; import { Logger } from "../../shared/utils"; import { ActivityDemandDirectorConfigOptions } from "../../market/demand/options"; +import { NetworkOptions } from "../../network/network.module"; export enum JobState { New = "new", diff --git a/src/golem-network/golem-network.test.ts b/src/golem-network/golem-network.test.ts index 2de8f4bcc..6b26f0632 100644 --- a/src/golem-network/golem-network.test.ts +++ b/src/golem-network/golem-network.test.ts @@ -88,7 +88,7 @@ describe("Golem Network", () => { const mockLease = { finalize: jest.fn().mockImplementation(() => Promise.resolve()) as LeaseProcess["finalize"], } as LeaseProcess; - when(mockMarket.createLease(_, _)).thenReturn(mockLease); + when(mockMarket.createLease(_, _, _)).thenReturn(mockLease); const mockSubscription = mockStartCollectingProposals(); const mockAllocation = mockPaymentCreateAllocation(); jest.spyOn(DraftOfferProposalPool.prototype, "acquire").mockResolvedValue({} as OfferProposal); diff --git a/src/golem-network/golem-network.ts b/src/golem-network/golem-network.ts index dbf27b6bb..a3b898d45 100644 --- a/src/golem-network/golem-network.ts +++ b/src/golem-network/golem-network.ts @@ -10,7 +10,7 @@ import { } from "../market"; import { IPaymentApi, PaymentModule, PaymentModuleImpl, PaymentModuleOptions } from "../payment"; import { ActivityModule, ActivityModuleImpl, IActivityApi, IFileServer } from "../activity"; -import { NetworkModule, NetworkModuleImpl } from "../network/network.module"; +import { Network, NetworkModule, NetworkModuleImpl, NetworkOptions, INetworkApi } from "../network"; import { EventEmitter } from "eventemitter3"; import { LeaseProcess, LeaseProcessPool, LeaseProcessPoolOptions } from "../lease-process"; import { DebitNoteRepository, InvoiceRepository, MarketApiAdapter, PaymentApiAdapter } from "../shared/yagna"; @@ -32,6 +32,7 @@ import { WebSocketBrowserStorageProvider, } from "../shared/storage"; import { DataTransferProtocol } from "../shared/types"; +import { NetworkApiAdapter } from "../shared/yagna/adapters/network-api-adapter"; export interface GolemNetworkOptions { /** @@ -100,6 +101,7 @@ export type GolemServices = { activityApi: IActivityApi; agreementApi: IAgreementApi; marketApi: MarketApi; + networkApi: INetworkApi; proposalCache: CacheService; proposalRepository: IProposalRepository; demandRepository: IDemandRepository; @@ -200,13 +202,14 @@ export class GolemNetwork { this.options.override?.agreementApi || new AgreementApiAdapter(this.yagna.appSessionId, this.yagna.market, agreementRepository, this.logger), marketApi: this.options.override?.marketApi || new MarketApiAdapter(this.yagna, this.logger), + networkApi: this.options.override?.networkApi || new NetworkApiAdapter(this.yagna, this.logger), fileServer: this.options.override?.fileServer || new GftpServerAdapter(this.storageProvider), }; - - this.market = this.options.override?.market || new MarketModuleImpl(this.services); + this.network = this.options.override?.network || new NetworkModuleImpl(this.services); + this.market = + this.options.override?.market || new MarketModuleImpl({ ...this.services, networkModule: this.network }); this.payment = this.options.override?.payment || new PaymentModuleImpl(this.services, this.options.payment); this.activity = this.options.override?.activity || new ActivityModuleImpl(this.services); - this.network = this.options.override?.network || new NetworkModuleImpl(); } catch (err) { this.events.emit("error", err); throw err; @@ -286,7 +289,11 @@ export class GolemNetwork { const agreement = await this.market.proposeAgreement(draftProposal); - const lease = this.market.createLease(agreement, allocation); + const networkNode = demand.network + ? await this.network.createNetworkNode(demand.network, agreement.getProviderInfo().id) + : undefined; + + const lease = this.market.createLease(agreement, allocation, networkNode); // We managed to create the activity, no need to look for more agreement candidates proposalSubscription.unsubscribe(); @@ -295,6 +302,11 @@ export class GolemNetwork { // First finalize the lease (which will wait for all payments to be processed) // and only then release the allocation await lease.finalize().catch((err) => this.logger.error("Error while finalizing lease", err)); + if (demand?.network && networkNode) { + await this.network + .removeNetworkNode(demand.network, networkNode) + .catch((err) => this.logger.error("Error while removing network node", err)); + } await this.payment .releaseAllocation(allocation) .catch((err) => this.logger.error("Error while releasing allocation", err)); @@ -357,6 +369,7 @@ export class GolemNetwork { const leaseProcessPool = this.market.createLeaseProcessPool(proposalPool, allocation, { replicas: concurrency, + network: demand.network, }); this.cleanupTasks.push(() => subscription.unsubscribe()); this.cleanupTasks.push(async () => { @@ -377,6 +390,26 @@ export class GolemNetwork { return this.hasConnection; } + /** + * Creates a new logical network within the Golem VPN infrastructure. + * Allows communication between network nodes using standard network mechanisms, + * but requires specific implementation in the ExeUnit/runtime, + * which must be capable of providing a standard Unix-socket interface to their payloads + * and marshaling the logical network traffic through the Golem Net transport layer + * @param options + */ + async createNetwork(options?: NetworkOptions): Promise { + return await this.network.createNetwork(options); + } + + /** + * Removes an existing network from the Golem VPN infrastructure. + * @param network + */ + async destroyNetwork(network: Network): Promise { + return await this.network.removeNetwork(network); + } + private createStorageProvider(): StorageProvider { if (typeof this.options.dataTransferProtocol === "string") { switch (this.options.dataTransferProtocol) { diff --git a/src/lease-process/lease-process-pool.test.ts b/src/lease-process/lease-process-pool.test.ts index 658f89fcd..935d2d54c 100644 --- a/src/lease-process/lease-process-pool.test.ts +++ b/src/lease-process/lease-process-pool.test.ts @@ -6,12 +6,14 @@ import type { MarketModule, OfferProposal } from "../market"; import { DraftOfferProposalPool } from "../market"; import { LeaseProcessPool } from "./lease-process-pool"; import { type RequireAtLeastOne } from "../shared/utils/types"; +import { NetworkModule } from "../network"; const agreementApi = imock(); const paymentApi = imock(); const allocation = mock(Allocation); const proposalPool = mock(DraftOfferProposalPool); const marketModule = imock(); +const networkModule = imock(); function getMockLeaseProcess() { return { @@ -28,6 +30,8 @@ function getLeasePool(replicas: RequireAtLeastOne<{ min: number; max: number }>) allocation: instance(allocation), proposalPool: instance(proposalPool), marketModule: instance(marketModule), + networkModule: instance(networkModule), + network: undefined, replicas, }); } @@ -49,7 +53,7 @@ describe("LeaseProcessPool", () => { getDto: () => ({}), } as Agreement); when(proposalPool.remove(_)).thenResolve(); - when(marketModule.createLease(_, _)).thenCall(() => ({}) as LeaseProcess); + when(marketModule.createLease(_, _, _)).thenCall(() => ({}) as LeaseProcess); const pool = getLeasePool({ min: 5, max: 10 }); @@ -61,7 +65,7 @@ describe("LeaseProcessPool", () => { it("retries on error", async () => { when(proposalPool.acquire()).thenResolve({} as OfferProposal); when(proposalPool.remove(_)).thenResolve(); - when(marketModule.createLease(_, _)).thenCall(() => ({}) as LeaseProcess); + when(marketModule.createLease(_, _, _)).thenCall(() => ({}) as LeaseProcess); const fakeAgreement = { getDto: () => ({}) } as Agreement; when(agreementApi.proposeAgreement(_)) diff --git a/src/lease-process/lease-process-pool.ts b/src/lease-process/lease-process-pool.ts index 60b297c53..4c6b32b26 100644 --- a/src/lease-process/lease-process-pool.ts +++ b/src/lease-process/lease-process-pool.ts @@ -8,6 +8,7 @@ import { EventEmitter } from "eventemitter3"; import type { RequireAtLeastOne } from "../shared/utils/types"; import type { Allocation, IPaymentApi } from "../payment"; import type { LeaseProcess } from "./lease-process"; +import { Network, NetworkModule } from "../network"; export interface LeaseProcessPoolDependencies { agreementApi: IAgreementApi; @@ -15,12 +16,14 @@ export interface LeaseProcessPoolDependencies { allocation: Allocation; proposalPool: DraftOfferProposalPool; marketModule: MarketModule; + networkModule: NetworkModule; logger?: Logger; } export interface LeaseProcessPoolOptions { replicas?: number | RequireAtLeastOne<{ min: number; max: number }>; agreementOptions?: LegacyAgreementServiceOptions; + network?: Network; } export interface LeaseProcessPoolEvents { @@ -55,9 +58,11 @@ export class LeaseProcessPool { private logger: Logger; private allocation: Allocation; + private network?: Network; private agreementApi: IAgreementApi; private proposalPool: DraftOfferProposalPool; private marketModule: MarketModule; + private networkModule: NetworkModule; private readonly minPoolSize: number; private readonly maxPoolSize: number; @@ -66,6 +71,8 @@ export class LeaseProcessPool { this.allocation = options.allocation; this.proposalPool = options.proposalPool; this.marketModule = options.marketModule; + this.networkModule = options.networkModule; + this.network = options.network; this.logger = this.logger = options?.logger || defaultLogger("lease-process-pool"); @@ -94,7 +101,10 @@ export class LeaseProcessPool { const agreement = await this.agreementApi.proposeAgreement(proposal); // After reaching an agreement, the proposal is useless await this.proposalPool.remove(proposal); - const leaseProcess = this.marketModule.createLease(agreement, this.allocation); + const networkNode = this.network + ? await this.networkModule.createNetworkNode(this.network, agreement.getProviderInfo().id) + : undefined; + const leaseProcess = this.marketModule.createLease(agreement, this.allocation, networkNode); this.events.emit("created", agreement.getDto()); return leaseProcess; } catch (error) { @@ -210,7 +220,7 @@ export class LeaseProcessPool { try { this.borrowed.delete(leaseProcess); this.logger.debug("Destroying lease process from the pool", { agreementId: leaseProcess.agreement.id }); - await leaseProcess.finalize(); + await Promise.all([leaseProcess.finalize(), this.removeNetworkNode(leaseProcess)]); this.events.emit("destroyed", leaseProcess.agreement.getDto()); } catch (error) { this.events.emit( @@ -315,6 +325,16 @@ export class LeaseProcessPool { this.events.emit("ready"); } + private async removeNetworkNode(leaseProcess: LeaseProcess) { + if (this.network && leaseProcess.networkNode) { + this.logger.debug("Removing a node from the network", { + network: this.network.getNetworkInfo().ip, + nodeIp: leaseProcess.networkNode.ip, + }); + await this.networkModule.removeNetworkNode(this.network, leaseProcess.networkNode); + } + } + /** * Acquire a lease process from the pool and release it after the callback is done * @example diff --git a/src/lease-process/lease-process.ts b/src/lease-process/lease-process.ts index 65ce1658c..bb0ca9c64 100644 --- a/src/lease-process/lease-process.ts +++ b/src/lease-process/lease-process.ts @@ -8,6 +8,8 @@ import { waitForCondition } from "../shared/utils/waitForCondition"; import { Activity, IActivityApi, WorkContext } from "../activity"; import { StorageProvider } from "../shared/storage"; import { EventEmitter } from "eventemitter3"; +import { INetworkApi } from "../network/api"; +import { NetworkNode } from "../network"; export interface LeaseProcessEvents { /** @@ -32,10 +34,12 @@ export class LeaseProcess { private readonly paymentApi: IPaymentApi, private readonly activityApi: IActivityApi, private readonly agreementApi: IAgreementApi, + private readonly networkApi: INetworkApi, private readonly logger: Logger, /** @deprecated This will be removed, we want to have a nice adapter here */ private readonly yagna: YagnaApi, private readonly storageProvider: StorageProvider, + public readonly networkNode?: NetworkNode, private readonly leaseOptions?: { paymentOptions: { invoiceFilter: InvoiceFilter; @@ -110,8 +114,10 @@ export class LeaseProcess { this.yagna.activity.control, this.yagna.activity.exec, this.currentActivity, + this.networkApi, { storageProvider: this.storageProvider, + networkNode: this.networkNode, }, ); } @@ -120,9 +126,17 @@ export class LeaseProcess { this.currentActivity = activity; // Access your work context to perform operations - const ctx = new WorkContext(this.activityApi, this.yagna.activity.control, this.yagna.activity.exec, activity, { - storageProvider: this.storageProvider, - }); + const ctx = new WorkContext( + this.activityApi, + this.yagna.activity.control, + this.yagna.activity.exec, + activity, + this.networkApi, + { + storageProvider: this.storageProvider, + networkNode: this.networkNode, + }, + ); await ctx.before(); diff --git a/src/market/market.module.test.ts b/src/market/market.module.test.ts index 23bea79bb..c020e99c8 100644 --- a/src/market/market.module.test.ts +++ b/src/market/market.module.test.ts @@ -11,6 +11,8 @@ import { IActivityApi, IFileServer } from "../activity"; import { StorageProvider } from "../shared/storage"; import { GolemMarketError } from "./error"; import { Allocation, IPaymentApi } from "../payment"; +import { INetworkApi } from "../network/api"; +import { NetworkModule } from "../network"; const mockMarketApiAdapter = mock(MarketApiAdapter); const mockYagna = mock(YagnaApi); @@ -24,6 +26,7 @@ beforeEach(() => { activityApi: instance(imock()), paymentApi: instance(imock()), agreementApi: instance(imock()), + networkApi: instance(imock()), proposalRepository: instance(imock()), demandRepository: instance(imock()), yagna: instance(mockYagna), @@ -31,6 +34,7 @@ beforeEach(() => { marketApi: instance(mockMarketApiAdapter), fileServer: instance(imock()), storageProvider: instance(imock()), + networkModule: instance(imock()), }); }); diff --git a/src/market/market.module.ts b/src/market/market.module.ts index a4500a1e2..ca7fcee47 100644 --- a/src/market/market.module.ts +++ b/src/market/market.module.ts @@ -27,6 +27,7 @@ import { ActivityDemandDirectorConfigOptions } from "./demand/options"; import { BasicDemandDirectorConfig } from "./demand/directors/basic-demand-director-config"; import { PaymentDemandDirectorConfig } from "./demand/directors/payment-demand-director-config"; import { GolemUserError } from "../shared/error/golem-error"; +import { Network, NetworkModule, NetworkNode, INetworkApi } from "../network"; import { LeaseProcess, LeaseProcessPool, LeaseProcessPoolOptions } from "../lease-process"; export interface MarketEvents {} @@ -47,6 +48,7 @@ export type DemandEngine = "vm" | "vm-nvidia" | "wasmtime"; export interface DemandSpec { demand: BuildDemandOptions; market: MarketOptions; + network?: Network; } export interface MarketOptions { @@ -150,7 +152,7 @@ export interface MarketModule { bufferSize?: number; }): Observable; - createLease(agreement: Agreement, allocation: Allocation): LeaseProcess; + createLease(agreement: Agreement, allocation: Allocation, networkNode?: NetworkNode): LeaseProcess; /** * Factory that creates new lease process pool that's fully configured @@ -188,6 +190,7 @@ export class MarketModuleImpl implements MarketModule { private readonly yagnaApi: YagnaApi; private readonly logger = defaultLogger("market"); private readonly agreementApi: IAgreementApi; + private readonly networkModule: NetworkModule; private readonly proposalRepo: IProposalRepository; private readonly demandRepo: IDemandRepository; private fileServer: IFileServer; @@ -202,6 +205,8 @@ export class MarketModuleImpl implements MarketModule { paymentApi: IPaymentApi; activityApi: IActivityApi; marketApi: MarketApi; + networkApi: INetworkApi; + networkModule: NetworkModule; fileServer: IFileServer; storageProvider: StorageProvider; }, @@ -209,6 +214,7 @@ export class MarketModuleImpl implements MarketModule { this.logger = deps.logger; this.yagnaApi = deps.yagna; this.agreementApi = deps.agreementApi; + this.networkModule = deps.networkModule; this.proposalRepo = deps.proposalRepository; this.demandRepo = deps.demandRepository; this.fileServer = deps.fileServer; @@ -397,7 +403,7 @@ export class MarketModuleImpl implements MarketModule { ); } - createLease(agreement: Agreement, allocation: Allocation) { + createLease(agreement: Agreement, allocation: Allocation, networkNode?: NetworkNode) { // TODO Accept the filters return new LeaseProcess( agreement, @@ -405,9 +411,11 @@ export class MarketModuleImpl implements MarketModule { this.deps.paymentApi, this.deps.activityApi, this.agreementApi, + this.deps.networkApi, this.deps.logger, this.yagnaApi, // TODO: Remove this dependency this.deps.storageProvider, + networkNode, ); } @@ -422,6 +430,7 @@ export class MarketModuleImpl implements MarketModule { allocation, proposalPool: draftPool, marketModule: this, + networkModule: this.deps.networkModule, logger: this.logger.child("lease-process-pool"), ...options, }); diff --git a/src/network/api.ts b/src/network/api.ts new file mode 100644 index 000000000..122d1dda3 --- /dev/null +++ b/src/network/api.ts @@ -0,0 +1,45 @@ +import { Network } from "./network"; +import { NetworkNode } from "./node"; +import { NetworkOptions } from "./network.module"; + +export interface INetworkApi { + /** + * Creates a new network with the specified options. + * @param options NetworkOptions + */ + createNetwork(options: NetworkOptions): Promise; + + /** + * Removes an existing network. + * @param network - The network to be removed. + */ + removeNetwork(network: Network): Promise; + + /** + * Creates a new node within a specified network. + * @param network - The network to which the node will be added. + * @param nodeId - The ID of the node to be created. + * @param nodeIp - Optional IP address for the node. If not provided, the first available IP address will be assigned. + */ + + createNetworkNode(network: Network, nodeId: string, nodeIp?: string): Promise; + + /** + * Removes an existing node from a specified network. + * @param network - The network from which the node will be removed. + * @param node - The node to be removed. + */ + removeNetworkNode(network: Network, node: NetworkNode): Promise; + + /** + * Returns the identifier of the requesor + */ + getIdentity(): Promise; + + /** + * Retrieves the WebSocket URI for a specified network node and port. + * @param networkNode - The network node for which the WebSocket URI is retrieved. + * @param port - The port number for the WebSocket connection. + */ + getWebsocketUri(networkNode: NetworkNode, port: number): string; +} diff --git a/src/network/config.ts b/src/network/config.ts deleted file mode 100644 index 8075216e9..000000000 --- a/src/network/config.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { NetworkOptions } from "./network"; -import { Logger, defaultLogger } from "../shared/utils"; - -const DEFAULTS = { - networkIp: "192.168.0.0/24", -}; - -/** - * @internal - */ -export class NetworkConfig { - public readonly mask?: string; - public readonly ip: string; - public readonly ownerId: string; - public readonly ownerIp?: string; - public readonly gateway?: string; - public readonly logger: Logger; - - constructor(options: NetworkOptions) { - this.ip = options?.networkIp || DEFAULTS.networkIp; - this.mask = options?.networkMask; - this.ownerId = options.networkOwnerId; - this.ownerIp = options?.networkOwnerIp; - this.gateway = options?.networkGateway; - this.logger = options?.logger || defaultLogger("network"); - } -} diff --git a/src/network/error.ts b/src/network/error.ts index 62cbbed3b..288bc8f73 100644 --- a/src/network/error.ts +++ b/src/network/error.ts @@ -11,6 +11,8 @@ export enum NetworkErrorCode { NodeAddingFailed = "NodeAddingFailed", NodeRemovalFailed = "NodeRemovalFailed", NetworkRemovalFailed = "NetworkRemovalFailed", + GettingIdentityFailed = "GettingIdentityFailed", + NetworkRemoved = "NetworkRemoved", } export class GolemNetworkError extends GolemModuleError { diff --git a/src/network/index.ts b/src/network/index.ts index bf280437f..1c0844695 100644 --- a/src/network/index.ts +++ b/src/network/index.ts @@ -1,4 +1,5 @@ -export { Network, NetworkOptions } from "./network"; -export { NetworkNode } from "./node"; -export { NetworkService, NetworkServiceOptions } from "./service"; -export { GolemNetworkError, NetworkErrorCode } from "./error"; +export * from "./network"; +export * from "./node"; +export * from "./network.module"; +export * from "./error"; +export * from "./api"; diff --git a/src/network/network.module.test.ts b/src/network/network.module.test.ts new file mode 100644 index 000000000..0fdcdfd49 --- /dev/null +++ b/src/network/network.module.test.ts @@ -0,0 +1,206 @@ +import { + GolemNetworkError, + Logger, + Network, + NetworkErrorCode, + NetworkInfo, + NetworkModuleImpl, + NetworkNode, +} from "../index"; +import { mock, anything, capture, imock, instance, reset, verify, when } from "@johanblumenberg/ts-mockito"; +import { INetworkApi } from "./api"; +import { IPv4 } from "ip-num"; + +const mockNetworkApi = imock(); +const mockNetwork = mock(Network); + +let networkModule: NetworkModuleImpl; + +describe("Network", () => { + beforeEach(() => { + reset(mockNetworkApi); + reset(mockNetwork); + networkModule = new NetworkModuleImpl({ + networkApi: instance(mockNetworkApi), + logger: instance(imock()), + }); + when(mockNetworkApi.createNetwork(anything())).thenResolve(instance(mockNetwork)); + when(mockNetwork.getNetworkInfo()).thenReturn({ + id: "test-id-1", + ip: "192.168.0.0", + mask: "255.255.255.0", + } as NetworkInfo); + when(mockNetwork.isIpInNetwork(anything())).thenReturn(true); + when(mockNetwork.isNodeIpUnique(anything())).thenReturn(true); + when(mockNetwork.isNodeIdUnique(anything())).thenReturn(true); + when(mockNetwork.getFirstAvailableIpAddress()).thenReturn(IPv4.fromString("192.168.0.1")); + }); + + describe("Creating", () => { + it("should create network with default ip and mask", async () => { + await networkModule.createNetwork(); + expect(capture(mockNetworkApi.createNetwork).last()).toEqual([ + { + ip: "192.168.0.0", + mask: "255.255.255.0", + }, + ]); + }); + + it("should create network with 16 bit mask", async () => { + await networkModule.createNetwork({ id: "1", ip: "192.168.7.0/16" }); + expect(capture(mockNetworkApi.createNetwork).last()).toEqual([ + { + id: "1", + ip: "192.168.0.0", + mask: "255.255.0.0", + }, + ]); + }); + + it("should create network with 24 bit mask", async () => { + await networkModule.createNetwork({ id: "1", ip: "192.168.7.0/24" }); + expect(capture(mockNetworkApi.createNetwork).last()).toEqual([ + { + id: "1", + ip: "192.168.7.0", + mask: "255.255.255.0", + }, + ]); + }); + + it("should create network with 8 bit mask", async () => { + await networkModule.createNetwork({ id: "1", ip: "192.168.7.0/8" }); + expect(capture(mockNetworkApi.createNetwork).last()).toEqual([ + { + id: "1", + ip: "192.0.0.0", + mask: "255.0.0.0", + }, + ]); + }); + + it("should not create network with invalid ip", async () => { + const shouldFail = networkModule.createNetwork({ id: "1", ip: "123.1.2" }); + await expect(shouldFail).rejects.toMatchError( + new GolemNetworkError( + "Unable to create network. Error: An IP4 number cannot have less or greater than 4 octets", + NetworkErrorCode.NetworkCreationFailed, + undefined, + new Error("An IP4 number cannot have less or greater than 4 octets"), + ), + ); + }); + + it("should create network with custom gateway", async () => { + await networkModule.createNetwork({ + ip: "192.168.0.1/27", + id: "owner_1", + gateway: "192.168.0.2", + }); + expect(capture(mockNetworkApi.createNetwork).last()).toEqual([ + { + id: "owner_1", + ip: "192.168.0.0", + mask: "255.255.255.224", + gateway: "192.168.0.2", + }, + ]); + }); + }); + describe("Nodes", () => { + it("should create a new node", async () => { + const network = instance(mockNetwork); + await networkModule.createNetworkNode(network, "7", "192.168.0.7"); + expect(capture(mockNetworkApi.createNetworkNode).last()).toEqual([network, "7", "192.168.0.7"]); + }); + + it("should create a few nodes", async () => { + const network = instance(mockNetwork); + await networkModule.createNetworkNode(network, "2", "192.168.0.3"); + await networkModule.createNetworkNode(network, "3", "192.168.0.7"); + expect(capture(mockNetworkApi.createNetworkNode).first()).toEqual([network, "2", "192.168.0.3"]); + expect(capture(mockNetworkApi.createNetworkNode).last()).toEqual([network, "3", "192.168.0.7"]); + }); + + it("should not create a node with an existing ID", async () => { + const network = instance(mockNetwork); + when(mockNetwork.isNodeIdUnique("2")).thenReturn(false); + await expect(networkModule.createNetworkNode(network, "2", "192.168.0.7")).rejects.toMatchError( + new GolemNetworkError( + `Network ID '2' has already been assigned in this network.`, + NetworkErrorCode.AddressAlreadyAssigned, + ), + ); + }); + + it("should not create a node with an existing IP", async () => { + when(mockNetwork.isNodeIpUnique(anything())).thenReturn(false); + const network = instance(mockNetwork); + await expect(networkModule.createNetworkNode(network, "3", "192.168.0.3")).rejects.toMatchError( + new GolemNetworkError( + "IP '192.168.0.3' has already been assigned in this network.", + NetworkErrorCode.AddressAlreadyAssigned, + ), + ); + }); + + it("should not add a node with address outside the network range", async () => { + const network = instance(mockNetwork); + when(mockNetwork.isIpInNetwork(anything())).thenReturn(false); + await expect(networkModule.createNetworkNode(network, "3", "192.168.2.2")).rejects.toMatchError( + new GolemNetworkError( + "The given IP ('192.168.2.2') address must belong to the network ('192.168.0.0').", + NetworkErrorCode.AddressOutOfRange, + ), + ); + }); + + it("should not add too many nodes", async () => { + const network = instance(mockNetwork); + const mockError = new GolemNetworkError( + "No more addresses available in 192.168.0.0/30", + NetworkErrorCode.NoAddressesAvailable, + ); + when(mockNetwork.getFirstAvailableIpAddress()).thenThrow(mockError); + await expect(networkModule.createNetworkNode(network, "next-id")).rejects.toMatchError(mockError); + }); + + it("should not remove node from the network if it does not belong to the network", async () => { + const network = instance(mockNetwork); + const mockNode = mock(NetworkNode); + const node = instance(mockNode); + when(mockNode.id).thenReturn("88"); + when(mockNetwork.hasNode(node)).thenReturn(false); + await expect(networkModule.removeNetworkNode(network, node)).rejects.toMatchError( + new GolemNetworkError(`The network node 88 does not belong to the network`, NetworkErrorCode.NodeRemovalFailed), + ); + }); + + it("should ignore the removal of the node if the network has been removed", async () => { + const network = instance(mockNetwork); + const mockNode = mock(NetworkNode); + const node = instance(mockNode); + when(mockNode.id).thenReturn("88"); + when(mockNetwork.hasNode(node)).thenReturn(true); + when(mockNetwork.isRemoved()).thenReturn(true); + await networkModule.removeNetworkNode(network, node); + verify(mockNetworkApi.removeNetworkNode(anything(), anything())).never(); + }); + }); + + describe("Removing", () => { + it("should remove network", async () => { + const network = instance(mockNetwork); + await networkModule.removeNetwork(network); + verify(mockNetworkApi.removeNetwork(network)).once(); + }); + + it("should not remove network that doesn't exist", async () => { + const network = instance(mockNetwork); + const mockError = new Error("404"); + when(mockNetworkApi.removeNetwork(network)).thenReject(mockError); + await expect(networkModule.removeNetwork(network)).rejects.toMatchError(mockError); + }); + }); +}); diff --git a/src/network/network.module.ts b/src/network/network.module.ts index 017bfb94a..9d933ab58 100644 --- a/src/network/network.module.ts +++ b/src/network/network.module.ts @@ -1,11 +1,208 @@ import { EventEmitter } from "eventemitter3"; +import { Network } from "./network"; +import { GolemNetworkError, NetworkErrorCode } from "./error"; +import { Logger } from "../shared/utils"; +import { INetworkApi } from "./api"; +import { NetworkNode } from "./node"; +import { IPv4, IPv4CidrRange, IPv4Mask } from "ip-num"; +import AsyncLock from "async-lock"; export interface NetworkEvents {} +export interface NetworkOptions { + /** + * The ID of the network. + * This is an optional field that can be used to specify a unique identifier for the network. + * If not provided, it will be generated automatically. + */ + id?: string; + + /** + * The IP address of the network. May contain netmask, e.g. "192.168.0.0/24". + * This field can include the netmask directly in CIDR notation. + */ + ip?: string; + + /** + * The desired IP address of the requestor node within the newly-created network. + * This field is optional and if not provided, the first available IP address will be assigned. + */ + ownerIp?: string; + + /** + * Optional network mask given in dotted decimal notation. + * If the ip address was provided in Cidr notation this mask will override the mask from the Cidr notation + */ + mask?: string; + + /** + * Optional gateway address for the network. + * This field can be used to specify a gateway IP address for the network. + */ + gateway?: string; +} + export interface NetworkModule { events: EventEmitter; + + /** + * Creates a new network with the specified options. + * @param options NetworkOptions + */ + createNetwork(options?: NetworkOptions): Promise; + + /** + * Removes an existing network. + * @param network - The network to be removed. + */ + removeNetwork(network: Network): Promise; + + /** + * Creates a new node within a specified network. + * @param network - The network to which the node will be added. + * @param nodeId - The ID of the node to be created. + * @param nodeIp - Optional IP address for the node. If not provided, the first available IP address will be assigned. + */ + createNetworkNode(network: Network, nodeId: string, nodeIp?: string): Promise; + + /** + * Removes an existing node from a specified network. + * @param network - The network from which the node will be removed. + * @param node - The node to be removed. + */ + removeNetworkNode(network: Network, node: NetworkNode): Promise; + + /** + * Retrieves the WebSocket URI for a specified network node and port. + * @param networkNode - The network node for which the WebSocket URI is retrieved. + * @param port - The port number for the WebSocket connection. + */ + getWebsocketUri(networkNode: NetworkNode, port: number): string; } export class NetworkModuleImpl implements NetworkModule { events: EventEmitter = new EventEmitter(); + private lock: AsyncLock = new AsyncLock(); + + constructor( + private readonly deps: { + logger: Logger; + networkApi: INetworkApi; + }, + ) {} + + async createNetwork(options?: NetworkOptions): Promise { + try { + const ipDecimalDottedString = options?.ip?.split("/")?.[0] || "192.168.0.0"; + const maskBinaryNotation = parseInt(options?.ip?.split("/")?.[1] || "24"); + const maskPrefix = options?.mask ? IPv4Mask.fromDecimalDottedString(options.mask).prefix : maskBinaryNotation; + const ipRange = IPv4CidrRange.fromCidr(`${IPv4.fromString(ipDecimalDottedString)}/${maskPrefix}`); + const ip = ipRange.getFirst(); + const mask = ipRange.getPrefix().toMask(); + const gateway = options?.gateway ? new IPv4(options.gateway) : undefined; + const network = await this.deps.networkApi.createNetwork({ + id: options?.id, + ip: ip.toString(), + mask: mask?.toString(), + gateway: gateway?.toString(), + }); + // add Requestor as network node + const requestorId = await this.deps.networkApi.getIdentity(); + await this.createNetworkNode(network, requestorId, options?.ownerIp); + this.deps.logger.info(`Network created`, network.getNetworkInfo()); + return network; + } catch (error) { + if (error instanceof GolemNetworkError) { + throw error; + } + throw new GolemNetworkError( + `Unable to create network. ${error?.response?.data?.message || error}`, + NetworkErrorCode.NetworkCreationFailed, + undefined, + error, + ); + } + } + async removeNetwork(network: Network): Promise { + await this.lock.acquire(`net-${network.id}`, async () => { + await this.deps.networkApi.removeNetwork(network); + network.markAsRemoved(); + this.deps.logger.info(`Network removed`, network.getNetworkInfo()); + }); + } + + async createNetworkNode(network: Network, nodeId: string, nodeIp?: string): Promise { + return await this.lock.acquire(`net-${network.id}`, async () => { + if (!network.isNodeIdUnique(nodeId)) { + throw new GolemNetworkError( + `Network ID '${nodeId}' has already been assigned in this network.`, + NetworkErrorCode.AddressAlreadyAssigned, + network.getNetworkInfo(), + ); + } + if (network.isRemoved()) { + throw new GolemNetworkError( + `Unable to create network node ${nodeId}. Network has already been removed`, + NetworkErrorCode.NetworkRemoved, + network.getNetworkInfo(), + ); + } + const ipv4 = this.getFreeIpInNetwork(network, nodeIp); + const node = await this.deps.networkApi.createNetworkNode(network, nodeId, ipv4.toString()); + network.addNode(node); + this.deps.logger.info(`Node has been added to the network.`, { id: nodeId, ip: ipv4.toString() }); + return node; + }); + } + + async removeNetworkNode(network: Network, node: NetworkNode): Promise { + return await this.lock.acquire(`net-${network.id}`, async () => { + if (!network.hasNode(node)) { + throw new GolemNetworkError( + `The network node ${node.id} does not belong to the network`, + NetworkErrorCode.NodeRemovalFailed, + network.getNetworkInfo(), + ); + } + if (network.isRemoved()) { + this.deps.logger.debug(`Unable to remove network node ${node.id}. Network has already been removed`, { + network, + node, + }); + return; + } + await this.deps.networkApi.removeNetworkNode(network, node); + network.removeNode(node); + this.deps.logger.info(`Node has been removed from the network.`, { + network: network.getNetworkInfo().ip, + nodeIp: node.ip, + }); + }); + } + + getWebsocketUri(networkNode: NetworkNode, port: number): string { + return this.deps.networkApi.getWebsocketUri(networkNode, port); + } + + private getFreeIpInNetwork(network: Network, targetIp?: string): IPv4 { + if (!targetIp) { + return network.getFirstAvailableIpAddress(); + } + const ipv4 = IPv4.fromString(targetIp); + if (!network.isIpInNetwork(ipv4)) { + throw new GolemNetworkError( + `The given IP ('${targetIp}') address must belong to the network ('${network.getNetworkInfo().ip}').`, + NetworkErrorCode.AddressOutOfRange, + network.getNetworkInfo(), + ); + } + if (!network.isNodeIpUnique(ipv4)) { + throw new GolemNetworkError( + `IP '${targetIp.toString()}' has already been assigned in this network.`, + NetworkErrorCode.AddressAlreadyAssigned, + network.getNetworkInfo(), + ); + } + return ipv4; + } } diff --git a/src/network/network.test.ts b/src/network/network.test.ts new file mode 100644 index 000000000..28f3fcae2 --- /dev/null +++ b/src/network/network.test.ts @@ -0,0 +1,123 @@ +import { Network } from "./network"; +import { instance, mock, when } from "@johanblumenberg/ts-mockito"; +import { NetworkNode } from "./node"; +import { GolemNetworkError, NetworkErrorCode } from "./error"; +import { IPv4 } from "ip-num"; + +const mockNetworkNode = mock(NetworkNode); +when(mockNetworkNode.id).thenReturn("network-node-id"); +when(mockNetworkNode.ip).thenReturn("192.168.0.2"); + +describe("Network", () => { + describe("Creating", () => { + test("should create a network from the ip given in Cidr notation", () => { + const network = new Network("network-id", "192.168.0.0/24"); + const networkInfo = network.getNetworkInfo(); + expect(networkInfo.id).toEqual("network-id"); + expect(networkInfo.ip).toEqual("192.168.0.0"); + expect(networkInfo.mask).toEqual("255.255.255.0"); + }); + test("should create a network from the ip and mask given in decimal dotted noatation", () => { + const network = new Network("network-id", "192.168.0.0", "255.255.255.0"); + const networkInfo = network.getNetworkInfo(); + expect(networkInfo.id).toEqual("network-id"); + expect(networkInfo.ip).toEqual("192.168.0.0"); + expect(networkInfo.mask).toEqual("255.255.255.0"); + }); + test("should not create a network with invalid ip", () => { + expect(() => new Network("network-id", "192.168.0")).toThrow( + new Error("Cidr notation should be in the form [ip number]/[range]"), + ); + }); + test("should not create a network with invalid mask", () => { + expect(() => new Network("network-id", "192.168.0.0", "255.0")).toThrow( + new Error("An IP4 number cannot have less or greater than 4 octets"), + ); + }); + test("should not create a network with invalid gatewey", () => { + expect(() => new Network("network-id", "192.168.0.0", "255.255.255.0", "234")).toThrow( + new Error("An IP4 number cannot have less or greater than 4 octets"), + ); + }); + }); + describe("Adding nodes", () => { + test("should add a node to the network", () => { + const network = new Network("network-id", "192.168.0.0/24"); + network.addNode(instance(mockNetworkNode)); + const networkInfo = network.getNetworkInfo(); + expect(networkInfo.nodes).toEqual({ "192.168.0.2": "network-node-id" }); + }); + test("should not add a node with the existing id", () => { + const network = new Network("network-id", "192.168.0.0/24"); + network.addNode(instance(mockNetworkNode)); + expect(() => network.addNode(instance(mockNetworkNode))).toThrow( + new GolemNetworkError( + `Node network-node-id has already been added to this network`, + NetworkErrorCode.AddressAlreadyAssigned, + ), + ); + }); + test("should not add a node to removed network", () => { + const network = new Network("network-id", "192.168.0.0/24"); + network.markAsRemoved(); + expect(() => network.addNode(instance(mockNetworkNode))).toThrow( + new GolemNetworkError(`Unable to add node network-node-id to removed network`, NetworkErrorCode.NetworkRemoved), + ); + }); + test("should get first avialble ip adrress", () => { + const network = new Network("network-id", "192.168.0.0/24"); + expect(network.getFirstAvailableIpAddress().toString()).toEqual("192.168.0.1"); + }); + }); + describe("Remove nodes", () => { + test("should remove a node from the network", () => { + const network = new Network("network-id", "192.168.0.0/24"); + const networkNode = instance(mockNetworkNode); + network.addNode(networkNode); + network.removeNode(networkNode); + expect(network.getNetworkInfo().nodes).toEqual({}); + }); + test("should not remove a node if it does not belong to the network", () => { + const network = new Network("network-id", "192.168.0.0/24"); + const networkNode = instance(mockNetworkNode); + network.addNode(networkNode); + when(mockNetworkNode.id).thenReturn("test-id-2"); + expect(() => network.removeNode(instance(mockNetworkNode))).toThrow( + new GolemNetworkError(`There is no node test-id-2 in the network`, NetworkErrorCode.NodeRemovalFailed), + ); + }); + test("should not remove a node if network has been removed", () => { + const network = new Network("network-id", "192.168.0.0/24"); + const networkNode = instance(mockNetworkNode); + when(mockNetworkNode.id).thenReturn("test-id-1"); + network.addNode(networkNode); + network.markAsRemoved(); + expect(() => network.removeNode(networkNode)).toThrow( + new GolemNetworkError(`Unable to remove node test-id-1 from removed network`, NetworkErrorCode.NetworkRemoved), + ); + }); + }); + describe("Validating", () => { + test("should check if node id is unique", () => { + const network = new Network("network-id", "192.168.0.0/24"); + when(mockNetworkNode.id).thenReturn("test-id"); + network.addNode(instance(mockNetworkNode)); + expect(network.isNodeIdUnique("test-id")).toBe(false); + expect(network.isNodeIdUnique("test-id-2")).toBe(true); + }); + test("should check if node ip is unique", () => { + const network = new Network("network-id", "192.168.0.0/24"); + when(mockNetworkNode.ip).thenReturn("192.168.0.2"); + network.addNode(instance(mockNetworkNode)); + expect(network.isNodeIpUnique(IPv4.fromDecimalDottedString("192.168.0.2"))).toBe(false); + expect(network.isNodeIpUnique(IPv4.fromDecimalDottedString("192.168.0.3"))).toBe(true); + }); + test("should check if node ip belongs to the network", () => { + const network = new Network("network-id", "192.168.0.0/24"); + expect(network.isIpInNetwork(IPv4.fromDecimalDottedString("192.168.0.2"))).toBe(true); + expect(network.isIpInNetwork(IPv4.fromDecimalDottedString("195.168.0.3"))).toBe(false); + expect(network.isIpInNetwork(IPv4.fromDecimalDottedString("192.169.0.3"))).toBe(false); + expect(network.isIpInNetwork(IPv4.fromDecimalDottedString("192.168.1.3"))).toBe(false); + }); + }); +}); diff --git a/src/network/network.ts b/src/network/network.ts index d60b1ed15..01ba6eb99 100644 --- a/src/network/network.ts +++ b/src/network/network.ts @@ -1,212 +1,115 @@ import { AbstractIPNum, IPv4, IPv4CidrRange, IPv4Mask, IPv4Prefix } from "ip-num"; -import { Logger, YagnaApi, YagnaOptions } from "../shared/utils"; -import { NetworkConfig } from "./config"; import { NetworkNode } from "./node"; import { GolemNetworkError, NetworkErrorCode } from "./error"; -/** - * @hidden - */ -export interface NetworkOptions { - /** the node ID of the owner of this VPN (the requestor) */ - networkOwnerId: string; - /** {@link YagnaOptions} */ - yagnaOptions?: YagnaOptions; - /** the IP address of the network. May contain netmask, e.g. "192.168.0.0/24" */ - networkIp?: string; - /** the desired IP address of the requestor node within the newly-created network */ - networkOwnerIp?: string; - /** optional netmask (only if not provided within the `ip` argument) */ - networkMask?: string; - /** optional gateway address for the network */ - networkGateway?: string; - /** optional custom logger module */ - logger?: Logger; -} - export interface NetworkInfo { id: string; ip: string; mask: string; + gateway?: string; nodes: { [ip: string]: string }; } -/** - * Network module - an object represents VPN created between the requestor and the provider nodes within Golem Network. - * @hidden - */ +export enum NetworkState { + Active = "Active", + Removed = "Removed", +} + export class Network { private readonly ip: IPv4; private readonly ipRange: IPv4CidrRange; private ipIterator: Iterator; private mask: IPv4Mask; - private ownerId: string; - private ownerIp: IPv4; private gateway?: IPv4; private nodes = new Map(); - private logger: Logger; + private state: NetworkState = NetworkState.Active; - /** - * Create a new VPN. - * - * @param yagnaApi - {@link YagnaApi} - * @param options - {@link NetworkOptions} - */ - static async create(yagnaApi: YagnaApi, options: NetworkOptions): Promise { - const config = new NetworkConfig(options); - try { - const { id, ip, mask } = await yagnaApi.net.createNetwork({ - id: config.ownerId, - ip: config.ip, - mask: config.mask, - gateway: config.gateway, - }); - const network = new Network(id!, yagnaApi, config); - await network.addNode(network.ownerId, network.ownerIp.toString()).catch(async (e) => { - await yagnaApi.net.removeNetwork(id as string); - throw e; - }); - config.logger.info(`Network created`, { id, ip, mask }); - return network; - } catch (error) { - if (error instanceof GolemNetworkError) { - throw error; - } - throw new GolemNetworkError( - `Unable to create network. ${error?.response?.data?.message || error}`, - NetworkErrorCode.NetworkCreationFailed, - undefined, - error, - ); - } - } - - /** - * @param id - * @param yagnaApi - * @param config - * @private - * @hidden - */ - private constructor( + constructor( public readonly id: string, - private readonly yagnaApi: YagnaApi, - public readonly config: NetworkConfig, + ip: string, + mask?: string, + gateway?: string, ) { - this.ipRange = IPv4CidrRange.fromCidr(config.mask ? `${config.ip}/${config.mask}` : config.ip); + this.ipRange = IPv4CidrRange.fromCidr( + mask ? `${ip.split("/")[0]}/${IPv4Mask.fromDecimalDottedString(mask).prefix}` : ip, + ); this.ipIterator = this.ipRange[Symbol.iterator](); - this.ip = this.nextAddress(); + this.ip = this.getFirstAvailableIpAddress(); this.mask = this.ipRange.getPrefix().toMask(); - this.ownerId = config.ownerId; - this.ownerIp = config.ownerIp ? new IPv4(config.ownerIp) : this.nextAddress(); - this.gateway = config.gateway ? new IPv4(config.gateway) : undefined; - this.logger = config.logger; + this.gateway = gateway ? new IPv4(gateway) : undefined; } /** - * Get Network Information - * @return NetworkInfo + * Returns information about the network. */ - getNetworkInfo(): NetworkInfo { + public getNetworkInfo(): NetworkInfo { return { id: this.id, ip: this.ip.toString(), mask: this.mask.toString(), - nodes: Object.fromEntries(Array.from(this.nodes).map(([id, node]) => [node.ip.toString(), id])), + gateway: this.gateway?.toString?.(), + nodes: Object.fromEntries(Array.from(this.nodes).map(([id, node]) => [node.ip, id])), }; } /** - * Add a new node to the network. - * - * @param nodeId Node ID within the Golem network of this VPN node - * @param ip IP address to assign to this node + * Adds a node to the network. + * @param node - The network node to be added. */ - async addNode(nodeId: string, ip?: string): Promise { - try { - this.ensureIdUnique(nodeId); - let ipv4: IPv4; - if (ip) { - ipv4 = IPv4.fromString(ip); - this.ensureIpInNetwork(ipv4); - this.ensureIpUnique(ipv4); - } else { - while (true) { - ipv4 = this.nextAddress(); - if (this.isIpUnique(ipv4)) break; - } - } - const node = new NetworkNode(nodeId, ipv4, this.getNetworkInfo.bind(this), this.getUrl()); - this.nodes.set(nodeId, node); - await this.yagnaApi.net.addNode(this.id, { id: nodeId, ip: ipv4.toString() }); - this.logger.debug(`Node has added to the network.`, { id: nodeId, ip: ipv4.toString() }); - return node; - } catch (error) { - if (error instanceof GolemNetworkError) { - throw error; - } + public addNode(node: NetworkNode) { + if (this.isRemoved()) { throw new GolemNetworkError( - `Unable to add node to network. ${error?.data?.message || error.toString()}`, - NetworkErrorCode.NodeAddingFailed, + `Unable to add node ${node.id} to removed network`, + NetworkErrorCode.NetworkRemoved, this.getNetworkInfo(), - error, ); } - } - - /** - * Remove the node from the network - * @param nodeId - */ - async removeNode(nodeId: string): Promise { - const node = this.nodes.get(nodeId); - if (!node) { - throw new GolemNetworkError( - `Unable to remove node ${nodeId}. There is no such node in the network`, - NetworkErrorCode.NodeRemovalFailed, - this.getNetworkInfo(), - ); - } - try { - await this.yagnaApi.net.removeNode(this.id, nodeId); - this.nodes.delete(nodeId); - this.logger.debug(`Node has removed from the network.`, { id: nodeId, ip: node.ip.toString() }); - } catch (error) { + if (this.hasNode(node)) { throw new GolemNetworkError( - `Unable to remove node ${nodeId}. ${error}`, - NetworkErrorCode.NetworkRemovalFailed, - this.getNetworkInfo(), - error, + `Node ${node.id} has already been added to this network`, + NetworkErrorCode.AddressAlreadyAssigned, ); } + this.nodes.set(node.id, node); } /** - * Checks whether the node belongs to the network - * @param nodeId + * Checks whether the node belongs to the network. + * @param node - The network node to check. */ - hasNode(nodeId: string): boolean { - return this.nodes.has(nodeId); + public hasNode(node: NetworkNode): boolean { + return this.nodes.has(node.id); } /** - * Remove this network, terminating any connections it provides + * Removes a node from the network. + * @param node - The network node to be removed. */ - async remove(): Promise { - try { - await this.yagnaApi.net.removeNetwork(this.id); - this.logger.info(`Network has removed:`, { id: this.id, ip: this.ip.toString() }); - } catch (error) { + public removeNode(node: NetworkNode) { + if (this.isRemoved()) { throw new GolemNetworkError( - `Unable to remove network. ${error?.data?.message || error.toString()}`, - NetworkErrorCode.NetworkRemovalFailed, + `Unable to remove node ${node.id} from removed network`, + NetworkErrorCode.NetworkRemoved, this.getNetworkInfo(), - error, ); } + if (!this.hasNode(node)) { + throw new GolemNetworkError(`There is no node ${node.id} in the network`, NetworkErrorCode.NodeRemovalFailed); + } + this.nodes.delete(node.id); } - private nextAddress(): IPv4 { + public markAsRemoved() { + if (this.state === NetworkState.Removed) { + throw new GolemNetworkError("Network already removed", NetworkErrorCode.NetworkRemoved, this.getNetworkInfo()); + } + this.state = NetworkState.Removed; + } + + /** + * Returns the first available IP address in the network. + */ + public getFirstAvailableIpAddress(): IPv4 { const ip = this.ipIterator.next().value; if (!ip) throw new GolemNetworkError( @@ -217,42 +120,33 @@ export class Network { return ip; } - private ensureIpInNetwork(ip: IPv4): boolean { - if (!this.ipRange.contains(new IPv4CidrRange(ip, new IPv4Prefix(BigInt(this.mask.prefix))))) - throw new GolemNetworkError( - `The given IP ('${ip.toString()}') address must belong to the network ('${this.ipRange.toCidrString()}').`, - NetworkErrorCode.AddressOutOfRange, - this.getNetworkInfo(), - ); - return true; - } - - private ensureIpUnique(ip: IPv4) { - if (!this.isIpUnique(ip)) - throw new GolemNetworkError( - `IP '${ip.toString()}' has already been assigned in this network.`, - NetworkErrorCode.AddressAlreadyAssigned, - this.getNetworkInfo(), - ); + /** + * Checks if a given IP address is within the network range. + * @param ip - The IPv4 address to check. + */ + public isIpInNetwork(ip: IPv4): boolean { + return this.ipRange.contains(new IPv4CidrRange(ip, new IPv4Prefix(BigInt(this.mask.prefix)))); } - private ensureIdUnique(id: string) { - if (this.nodes.has(id)) - throw new GolemNetworkError( - `Network ID '${id}' has already been assigned in this network.`, - NetworkErrorCode.AddressAlreadyAssigned, - this.getNetworkInfo(), - ); + /** + * Checks if a given node ID is unique within the network. + * @param id - The node ID to check. + */ + public isNodeIdUnique(id: string): boolean { + return !this.nodes.has(id); } - private isIpUnique(ip: IPv4): boolean { + /** + * Checks if a given IP address is unique within the network. + */ + public isNodeIpUnique(ip: IPv4): boolean { for (const node of this.nodes.values()) { - if (node.ip.isEquals(ip)) return false; + if (new IPv4(node.ip).isEquals(ip)) return false; } return true; } - private getUrl() { - return this.yagnaApi.net.httpRequest.config.BASE; + public isRemoved() { + return this.state === NetworkState.Removed; } } diff --git a/src/network/node.ts b/src/network/node.ts index 61ccec853..8705132a3 100644 --- a/src/network/node.ts +++ b/src/network/node.ts @@ -1,4 +1,3 @@ -import { IPv4 } from "ip-num"; import { NetworkInfo } from "./network"; /** @@ -7,9 +6,8 @@ import { NetworkInfo } from "./network"; export class NetworkNode { constructor( public readonly id: string, - public readonly ip: IPv4, - private getNetworkInfo: () => NetworkInfo, - private apiUrl: string, + public readonly ip: string, + public getNetworkInfo: () => NetworkInfo, ) {} /** @@ -22,20 +20,9 @@ export class NetworkNode { net: [ { ...this.getNetworkInfo(), - nodeIp: this.ip.toString(), + nodeIp: this.ip, }, ], }; } - - /** - * Get the websocket URI corresponding with a specific TCP port on this Node. - * @param port TCP port of the service within the runtime - * @return the url - */ - getWebsocketUri(port: number) { - const url = new URL(this.apiUrl); - url.protocol = "ws"; - return `${url.href}/net/${this.getNetworkInfo().id}/tcp/${this.ip}/${port}`; - } } diff --git a/src/network/service.ts b/src/network/service.ts deleted file mode 100644 index 94876794d..000000000 --- a/src/network/service.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { defaultLogger, Logger, YagnaApi } from "../shared/utils"; -import { Network, NetworkOptions } from "./network"; -import { NetworkNode } from "./node"; -import { GolemNetworkError, NetworkErrorCode } from "./error"; - -export type NetworkServiceOptions = Omit; - -/** - * Network Service - * @description Service used in {@link TaskExecutor} - * @internal - */ -export class NetworkService { - private network?: Network; - private logger: Logger; - - constructor( - private readonly yagnaApi: YagnaApi, - private readonly options?: NetworkServiceOptions, - ) { - this.logger = options?.logger || defaultLogger("network"); - } - - async run(networkOwnerId?: string) { - if (!networkOwnerId) { - const data = await this.yagnaApi.identity.getIdentity(); - networkOwnerId = data.identity; - } - - this.network = await Network.create(this.yagnaApi, { ...this.options, networkOwnerId }); - this.logger.info("Network Service has started"); - } - - public async addNode(nodeId: string, ip?: string): Promise { - if (!this.network) - throw new GolemNetworkError( - "The service is not started and the network does not exist", - NetworkErrorCode.NetworkSetupMissing, - ); - return this.network.addNode(nodeId, ip); - } - - public async removeNode(nodeId: string): Promise { - if (!this.network) - throw new GolemNetworkError( - "The service is not started and the network does not exist", - NetworkErrorCode.ServiceNotInitialized, - ); - return this.network.removeNode(nodeId); - } - - public hasNode(nodeId: string) { - if (!this.network) - throw new GolemNetworkError( - "The service is not started and the network does not exist", - NetworkErrorCode.ServiceNotInitialized, - ); - return this.network.hasNode(nodeId); - } - - async end() { - await this.network?.remove(); - this.logger.info("Network Service has been stopped"); - } -} diff --git a/src/shared/yagna/adapters/network-api-adapter.ts b/src/shared/yagna/adapters/network-api-adapter.ts new file mode 100644 index 000000000..d3316d5eb --- /dev/null +++ b/src/shared/yagna/adapters/network-api-adapter.ts @@ -0,0 +1,90 @@ +import { YagnaApi } from "../yagnaApi"; +import { Logger } from "../../utils"; +import { INetworkApi } from "../../../network/api"; +import { GolemNetworkError, Network, NetworkErrorCode, NetworkNode } from "../../../network"; +import { getMessageFromApiError } from "../../utils/apiErrorMessage"; + +export class NetworkApiAdapter implements INetworkApi { + constructor( + private readonly yagnaApi: YagnaApi, + private readonly logger: Logger, + ) {} + + async createNetwork(options: { id: string; ip: string; mask?: string; gateway?: string }): Promise { + try { + const { id, ip, mask, gateway } = await this.yagnaApi.net.createNetwork(options); + // @ts-expect-error TODO: Can we create a network without an id or is this just a bug in ya-clinet spec? + return new Network(id, ip, mask, gateway); + } catch (error) { + const message = getMessageFromApiError(error); + throw new GolemNetworkError( + `Unable to create network. ${message}`, + NetworkErrorCode.NetworkCreationFailed, + undefined, + error, + ); + } + } + async removeNetwork(network: Network): Promise { + try { + await this.yagnaApi.net.removeNetwork(network.id); + } catch (error) { + const message = getMessageFromApiError(error); + throw new GolemNetworkError( + `Unable to remove network. ${message}`, + NetworkErrorCode.NetworkRemovalFailed, + network.getNetworkInfo(), + error, + ); + } + } + async createNetworkNode(network: Network, nodeId: string, nodeIp: string): Promise { + try { + await this.yagnaApi.net.addNode(network.id, { id: nodeId, ip: nodeIp }); + const networkNode = new NetworkNode(nodeId, nodeIp, network.getNetworkInfo.bind(network)); + + return networkNode; + } catch (error) { + const message = getMessageFromApiError(error); + throw new GolemNetworkError( + `Unable to add node to network. ${message}`, + NetworkErrorCode.NodeAddingFailed, + network.getNetworkInfo(), + error, + ); + } + } + async removeNetworkNode(network: Network, node: NetworkNode): Promise { + try { + await this.yagnaApi.net.removeNode(network.id, node.id); + } catch (error) { + const message = getMessageFromApiError(error); + throw new GolemNetworkError( + `Unable to remove network node. ${message}`, + NetworkErrorCode.NodeRemovalFailed, + network.getNetworkInfo(), + error, + ); + } + } + + async getIdentity() { + try { + return await this.yagnaApi.identity.getIdentity().then((res) => res.identity); + } catch (error) { + const message = getMessageFromApiError(error); + throw new GolemNetworkError( + `Unable to get requestor identity. ${message}`, + NetworkErrorCode.GettingIdentityFailed, + undefined, + error, + ); + } + } + + getWebsocketUri(networkNode: NetworkNode, port: number) { + const url = new URL(this.yagnaApi.basePath); + url.protocol = "ws"; + return `${url.href}/net/${networkNode.getNetworkInfo().id}/tcp/${networkNode.ip}/${port}`; + } +} diff --git a/tests/e2e/leaseProcessPool.spec.ts b/tests/e2e/leaseProcessPool.spec.ts index 8b56d6132..ae01ec75d 100644 --- a/tests/e2e/leaseProcessPool.spec.ts +++ b/tests/e2e/leaseProcessPool.spec.ts @@ -7,6 +7,7 @@ describe("LeaseProcessPool", () => { market: glm.market, activity: glm.activity, payment: glm.payment, + network: glm.network, }; let proposalPool: DraftOfferProposalPool; let allocation: Allocation; @@ -135,4 +136,25 @@ describe("LeaseProcessPool", () => { [activity1.activity.agreement.id, activity2.activity.agreement.id].sort(), ); }); + + it("should establish a connection between two activities from pool via vpn", async () => { + const network = await modules.network.createNetwork(); + const pool = modules.market.createLeaseProcessPool(proposalPool, allocation, { replicas: 2, network }); + pool.events.on("error", (error) => { + throw error; + }); + const leaseProcess1 = await pool.acquire(); + const leaseProcess2 = await pool.acquire(); + const exe1 = await leaseProcess1.getExeUnit(); + const exe2 = await leaseProcess2.getExeUnit(); + const result1 = await exe1.run(`ping ${exe2.getIp()} -c 4`); + const result2 = await exe2.run(`ping ${exe1.getIp()} -c 4`); + expect(result1.stdout?.toString().trim()).toMatch("4 packets transmitted, 4 packets received, 0% packet loss"); + expect(result2.stdout?.toString().trim()).toMatch("4 packets transmitted, 4 packets received, 0% packet loss"); + expect(Object.keys(network.getNetworkInfo().nodes)).toEqual(["192.168.0.1", "192.168.0.2", "192.168.0.3"]); + await pool.destroy(leaseProcess1); + await pool.destroy(leaseProcess2); + await pool.drainAndClear(); + await modules.network.removeNetwork(network); + }); }); diff --git a/tests/examples/examples.json b/tests/examples/examples.json index 804f919f9..d81827926 100644 --- a/tests/examples/examples.json +++ b/tests/examples/examples.json @@ -1,6 +1,7 @@ [ { "cmd": "tsx", "path": "examples/basic/many-of.ts" }, { "cmd": "tsx", "path": "examples/basic/one-of.ts" }, + { "cmd": "tsx", "path": "examples/basic/vpn.ts" }, { "cmd": "tsx", "path": "examples/advanced/hello-world.ts" }, { "cmd": "tsx", "path": "examples/advanced/manual-pools.ts" }, { "cmd": "tsx", "path": "examples/experimental/deployment/new-api.ts" } diff --git a/tests/unit/network.test.ts b/tests/unit/network.test.ts deleted file mode 100644 index d5de63728..000000000 --- a/tests/unit/network.test.ts +++ /dev/null @@ -1,260 +0,0 @@ -import { GolemNetworkError, Network, NetworkErrorCode, YagnaApi } from "../../src"; -import { anything, instance, mock, reset, verify, when } from "@johanblumenberg/ts-mockito"; -import { NetApi } from "ya-ts-client"; - -const mockYagna = mock(YagnaApi); -const mockNet = mock(NetApi.RequestorService); -const mockHttpRequest = mock(NetApi.BaseHttpRequest); -const yagnaApi = instance(mockYagna); - -describe("Network", () => { - beforeEach(() => { - reset(mockYagna); - reset(mockNet); - - when(mockYagna.net).thenReturn(instance(mockNet)); - when(mockNet.httpRequest).thenReturn(instance(mockHttpRequest)); - when(mockHttpRequest.config).thenReturn({ - BASE: "http://localhost/net-api/v1", - CREDENTIALS: "same-origin", - WITH_CREDENTIALS: true, - VERSION: "v1", - }); - - when(mockNet.createNetwork(anything())).thenCall((body) => - Promise.resolve({ - id: "network-id", - ip: "192.168.0.0", - mask: "255.255.255.0", - gateway: body.gateway, - }), - ); - }); - describe("Creating", () => { - it("should create network", async () => { - const network = await Network.create(yagnaApi, { networkOwnerId: "test_owner_id" }); - const { ip, mask, nodes } = network.getNetworkInfo(); - expect(nodes["192.168.0.1"]).toEqual("test_owner_id"); - expect(Object.keys(nodes).length).toEqual(1); - expect(ip).toEqual("192.168.0.0"); - expect(mask).toEqual("255.255.255.0"); - }); - - it("should create network with 16 bit mask", async () => { - const network = await Network.create(yagnaApi, { networkOwnerId: "1", networkIp: "192.168.7.0/16" }); - const { ip, mask } = network.getNetworkInfo(); - expect({ ip, mask }).toEqual({ ip: "192.168.0.0", mask: "255.255.0.0" }); - }); - - it("should create network with 24 bit mask", async () => { - const network = await Network.create(yagnaApi, { networkOwnerId: "1", networkIp: "192.168.7.0/24" }); - const { ip, mask } = network.getNetworkInfo(); - expect({ ip, mask }).toEqual({ ip: "192.168.7.0", mask: "255.255.255.0" }); - }); - - it("should create network with 8 bit mask", async () => { - const network = await Network.create(yagnaApi, { networkOwnerId: "1", networkIp: "192.168.7.0/8" }); - const { ip, mask } = network.getNetworkInfo(); - expect({ ip, mask }).toEqual({ ip: "192.0.0.0", mask: "255.0.0.0" }); - }); - - it("should not create network with invalid ip", async () => { - const shouldFail = Network.create(yagnaApi, { networkOwnerId: "1", networkIp: "123.1.2" }); - await expect(shouldFail).rejects.toMatchError( - new GolemNetworkError( - "Unable to create network. Error: Cidr notation should be in the form [ip number]/[range]", - NetworkErrorCode.NetworkCreationFailed, - undefined, - new Error("Cidr notation should be in the form [ip number]/[range]"), - ), - ); - }); - - it("should not create network without mask", async () => { - const shouldFail = Network.create(yagnaApi, { networkOwnerId: "1", networkIp: "1.1.1.1" }); - await expect(shouldFail).rejects.toMatchError( - new GolemNetworkError( - "Unable to create network. Error: Cidr notation should be in the form [ip number]/[range]", - NetworkErrorCode.NetworkCreationFailed, - undefined, - new Error("Cidr notation should be in the form [ip number]/[range]"), - ), - ); - }); - - it("should create network with custom options", async () => { - const network = await Network.create(yagnaApi, { - networkIp: "192.168.0.1", - networkOwnerId: "owner_1", - networkOwnerIp: "192.168.0.7", - networkMask: "27", - networkGateway: "192.168.0.2", - }); - const { ip, mask, nodes } = network.getNetworkInfo(); - expect({ ip, mask }).toEqual({ ip: "192.168.0.0", mask: "255.255.255.224" }); - expect(nodes["192.168.0.7"]).toEqual("owner_1"); - }); - }); - - describe("Nodes", () => { - it("should add node", async () => { - const network = await Network.create(yagnaApi, { networkOwnerId: "1", networkIp: "192.168.0.0/24" }); - const { id, ip } = await network.addNode("7", "192.168.0.7"); - expect({ id, ip: ip.toString() }).toEqual({ id: "7", ip: "192.168.0.7" }); - }); - - it("should add a few nodes", async () => { - const network = await Network.create(yagnaApi, { networkOwnerId: "1", networkIp: "192.168.0.0/24" }); - const node2 = await network.addNode("2", "192.168.0.3"); - const node3 = await network.addNode("3"); - const node4 = await network.addNode("4"); - expect({ id: node2.id, ip: node2.ip.toString() }).toEqual({ id: "2", ip: "192.168.0.3" }); - expect({ id: node3.id, ip: node3.ip.toString() }).toEqual({ id: "3", ip: "192.168.0.2" }); - expect({ id: node4.id, ip: node4.ip.toString() }).toEqual({ id: "4", ip: "192.168.0.4" }); - }); - - it("should not add node with an existing ID", async () => { - const network = await Network.create(yagnaApi, { networkOwnerId: "1", networkIp: "192.168.0.0/24" }); - await expect(network.addNode("1")).rejects.toMatchError( - new GolemNetworkError( - "Network ID '1' has already been assigned in this network.", - NetworkErrorCode.AddressAlreadyAssigned, - network.getNetworkInfo(), - ), - ); - }); - - it("should not add node with an existing IP", async () => { - const network = await Network.create(yagnaApi, { networkOwnerId: "1", networkIp: "192.168.0.0/24" }); - await network.addNode("2", "192.168.0.3"); - await expect(network.addNode("3", "192.168.0.3")).rejects.toMatchError( - new GolemNetworkError( - "IP '192.168.0.3' has already been assigned in this network.", - NetworkErrorCode.AddressAlreadyAssigned, - network.getNetworkInfo(), - ), - ); - }); - - it("should not add node with address outside the network range", async () => { - const network = await Network.create(yagnaApi, { networkOwnerId: "1", networkIp: "192.168.0.0/24" }); - await expect(network.addNode("2", "192.168.2.2")).rejects.toMatchError( - new GolemNetworkError( - "The given IP ('192.168.2.2') address must belong to the network ('192.168.0.0/24').", - NetworkErrorCode.AddressOutOfRange, - network.getNetworkInfo(), - ), - ); - }); - - it("should not add too many nodes", async () => { - const network = await Network.create(yagnaApi, { networkOwnerId: "1", networkIp: "192.168.0.0/30" }); - await network.addNode("2"); - await network.addNode("3"); - await expect(network.addNode("4")).rejects.toMatchError( - new GolemNetworkError( - "No more addresses available in 192.168.0.0/30", - NetworkErrorCode.NoAddressesAvailable, - network.getNetworkInfo(), - ), - ); - }); - - it("should throw an error when there are no free IPs available", async () => { - const network = await Network.create(yagnaApi, { networkOwnerId: "1", networkIp: "192.168.0.0/30" }); - await network.addNode("2"); - await network.addNode("3"); - await network.removeNode("2"); - await expect(network.addNode("4")).rejects.toMatchError( - new GolemNetworkError( - "No more addresses available in 192.168.0.0/30", - NetworkErrorCode.NoAddressesAvailable, - network.getNetworkInfo(), - ), - ); - }); - - it("should return true if node belongs to the network", async () => { - const network = await Network.create(yagnaApi, { networkOwnerId: "1", networkIp: "192.168.0.0/30" }); - await network.addNode("2"); - expect(network.hasNode("2")).toEqual(true); - }); - - it("should return false if node does not belong to the network", async () => { - const network = await Network.create(yagnaApi, { networkOwnerId: "1", networkIp: "192.168.0.0/30" }); - await network.addNode("2"); - expect(network.hasNode("77")).toEqual(false); - }); - - it("should get node network config", async () => { - const network = await Network.create(yagnaApi, { networkOwnerId: "1", networkIp: "192.168.0.0/24" }); - const node = await network.addNode("2"); - expect(node.getNetworkConfig()).toEqual({ - net: [ - { - id: network.id, - ip: "192.168.0.0", - mask: "255.255.255.0", - nodeIp: "192.168.0.2", - nodes: { - "192.168.0.1": "1", - "192.168.0.2": "2", - }, - }, - ], - }); - }); - - it("should get node websocket uri", async () => { - const network = await Network.create(yagnaApi, { networkOwnerId: "1", networkIp: "192.168.0.0/24" }); - const node = await network.addNode("2"); - expect(node.getWebsocketUri(22)).toEqual( - `ws://${process.env?.YAGNA_API_URL?.substring(7) || "localhost"}/net-api/v1/net/${ - network.id - }/tcp/192.168.0.2/22`, - ); - }); - - it("should remove node from the network", async () => { - const network = await Network.create(yagnaApi, { networkOwnerId: "1", networkIp: "192.168.0.0/24" }); - const node = await network.addNode("7"); - await network.removeNode(node.id); - verify(mockNet.removeNode(network.id, node.id)).once(); - }); - - it("should not remove node from the network if it does not exist", async () => { - const network = await Network.create(yagnaApi, { networkOwnerId: "1", networkIp: "192.168.0.0/24" }); - await network.addNode("7"); - await expect(network.removeNode("88")).rejects.toMatchError( - new GolemNetworkError( - "Unable to remove node 88. There is no such node in the network", - NetworkErrorCode.NodeRemovalFailed, - network.getNetworkInfo(), - ), - ); - }); - }); - - describe("Removing", () => { - it("should remove network", async () => { - const network = await Network.create(yagnaApi, { networkOwnerId: "1", networkIp: "192.168.0.0/24" }); - await network.remove(); - verify(mockNet.removeNetwork(anything())).once(); - }); - - it("should not remove network that doesn't exist", async () => { - const network = await Network.create(yagnaApi, { networkOwnerId: "1", networkIp: "192.168.0.0/24" }); - - when(mockNet.removeNetwork(anything())).thenReject(new Error("404")); - - await expect(network.remove()).rejects.toMatchError( - new GolemNetworkError( - `Unable to remove network. Error: 404`, - NetworkErrorCode.NetworkRemovalFailed, - network.getNetworkInfo(), - new Error("404"), - ), - ); - }); - }); -}); diff --git a/tests/unit/network_service.test.ts b/tests/unit/network_service.test.ts deleted file mode 100644 index ac8efc70f..000000000 --- a/tests/unit/network_service.test.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { GolemNetworkError, NetworkErrorCode, NetworkService, YagnaApi } from "../../src"; -import { anything, instance, mock, reset, verify, when } from "@johanblumenberg/ts-mockito"; -import { LoggerMock } from "../mock/utils/logger"; -import { NetApi } from "ya-ts-client"; - -const logger = new LoggerMock(); - -const mockYagna = mock(YagnaApi); -const mockNet = mock(NetApi.RequestorService); -const mockHttpRequest = mock(NetApi.BaseHttpRequest); - -const yagnaApi = instance(mockYagna); - -describe("Network Service", () => { - beforeEach(() => { - logger.clear(); - - reset(mockYagna); - reset(mockNet); - - when(mockYagna.net).thenReturn(instance(mockNet)); - when(mockNet.httpRequest).thenReturn(instance(mockHttpRequest)); - when(mockHttpRequest.config).thenReturn({ - BASE: "http://localhost/net-api/v1", - CREDENTIALS: "same-origin", - WITH_CREDENTIALS: true, - VERSION: "v1", - }); - - when(mockNet.createNetwork(anything())).thenCall((body) => - Promise.resolve({ - id: "network-id", - ip: "192.168.0.0", - mask: "255.255.255.0", - gateway: body.gateway, - }), - ); - }); - - describe("Creating", () => { - it("should start service and create network", async () => { - const networkService = new NetworkService(yagnaApi, { logger }); - await networkService.run("test_owner_id"); - await logger.expectToInclude( - "Network created", - { - id: expect.anything(), - ip: "192.168.0.0", - mask: "255.255.255.0", - }, - 10, - ); - await logger.expectToInclude("Network Service has started"); - await networkService.end(); - }); - }); - - describe("Nodes", () => { - describe("adding", () => { - it("should add node to network", async () => { - const networkService = new NetworkService(yagnaApi, { logger }); - await networkService.run("test_owner_id"); - await networkService.addNode("provider_2"); - await logger.expectToInclude( - "Node has added to the network.", - { - id: "provider_2", - ip: "192.168.0.2", - }, - 10, - ); - await networkService.end(); - }); - - it("should not add node if the service is not started", async () => { - const networkService = new NetworkService(yagnaApi, { logger }); - const result = networkService.addNode("provider_2"); - await expect(result).rejects.toMatchError( - new GolemNetworkError( - "The service is not started and the network does not exist", - NetworkErrorCode.NetworkSetupMissing, - ), - ); - }); - describe("removing", () => { - it("should remove node from the network", async () => { - const networkService = new NetworkService(yagnaApi, { logger }); - await networkService.run("test_owner_id"); - await networkService.addNode("provider_2"); - await networkService.removeNode("provider_2"); - - verify(mockNet.removeNode(anything(), "provider_2")).once(); - - await networkService.end(); - }); - it("should not remove node from the network", async () => { - const networkService = new NetworkService(yagnaApi, { logger }); - await networkService.run("test_owner_id"); - await networkService.addNode("provider_2"); - await expect(networkService.removeNode("provider_777")).rejects.toMatchError( - new GolemNetworkError( - "Unable to remove node provider_777. There is no such node in the network", - NetworkErrorCode.NodeRemovalFailed, - networkService["network"]?.getNetworkInfo(), - ), - ); - await networkService.end(); - }); - }); - }); - - describe("Removing", () => { - it("should end service and remove network", async () => { - const networkService = new NetworkService(yagnaApi, { logger }); - await networkService.run("test_owner_id"); - await networkService.end(); - await logger.expectToInclude( - "Network has removed:", - { - id: expect.anything(), - ip: expect.anything(), - }, - 60, - ); - await logger.expectToInclude("Network Service has been stopped"); - await networkService.end(); - }); - }); - }); -}); diff --git a/tests/unit/work.test.ts b/tests/unit/work.test.ts index 7c0c79c99..ce8eb592a 100644 --- a/tests/unit/work.test.ts +++ b/tests/unit/work.test.ts @@ -17,8 +17,10 @@ import { IPv4 } from "ip-num"; import { StorageProviderDataCallback } from "../../src/shared/storage/provider"; import { ActivityApi } from "ya-ts-client"; import { ExeScriptExecutor } from "../../src/activity/exe-script-executor"; +import { INetworkApi } from "../../src/network/api"; const mockActivityApi = imock(); +const mockNetworkApi = imock(); const mockActivity = mock(Activity); const mockExecutor = mock(ExeScriptExecutor); const mockActivityControl = imock(); @@ -30,6 +32,7 @@ describe("Work Context", () => { beforeEach(() => { // Make mocks ready to re-use reset(mockActivityApi); + reset(mockNetworkApi); reset(mockActivity); reset(mockActivityControl); reset(mockExecObserver); @@ -66,6 +69,7 @@ describe("Work Context", () => { instance(mockActivityControl), instance(mockExecObserver), instance(mockActivity), + instance(mockNetworkApi), ); await ctx.before(); const results = await worker(ctx); @@ -91,6 +95,7 @@ describe("Work Context", () => { instance(mockActivityControl), instance(mockExecObserver), instance(mockActivity), + instance(mockNetworkApi), ); await ctx.before(); const results = await worker(ctx); @@ -119,6 +124,7 @@ describe("Work Context", () => { instance(mockActivityControl), instance(mockExecObserver), instance(mockActivity), + instance(mockNetworkApi), { storageProvider: instance(mockStorageProvider) }, ); await ctx.before(); @@ -147,6 +153,7 @@ describe("Work Context", () => { instance(mockActivityControl), instance(mockExecObserver), instance(mockActivity), + instance(mockNetworkApi), { storageProvider: instance(mockStorageProvider) }, ); await ctx.before(); @@ -178,6 +185,7 @@ describe("Work Context", () => { instance(mockActivityControl), instance(mockExecObserver), instance(mockActivity), + instance(mockNetworkApi), { storageProvider: instance(mockStorageProvider) }, ); @@ -215,6 +223,7 @@ describe("Work Context", () => { instance(mockActivityControl), instance(mockExecObserver), instance(mockActivity), + instance(mockNetworkApi), { storageProvider: instance(mockStorageProvider) }, ); const result = await ctx.downloadJson("/golem/file.txt"); @@ -252,6 +261,7 @@ describe("Work Context", () => { instance(mockActivityControl), instance(mockExecObserver), instance(mockActivity), + instance(mockNetworkApi), { storageProvider: instance(mockStorageProvider) }, ); const result = await ctx.downloadData("/golem/file.txt"); @@ -275,6 +285,7 @@ describe("Work Context", () => { instance(mockActivityControl), instance(mockExecObserver), instance(mockActivity), + instance(mockNetworkApi), ); when(mockExecutor.execute(_, _, _)).thenResolve( buildExecutorResults([ @@ -300,6 +311,7 @@ describe("Work Context", () => { instance(mockActivityControl), instance(mockExecObserver), instance(mockActivity), + instance(mockNetworkApi), ); when(mockExecutor.execute(_, _, _)).thenResolve( @@ -336,6 +348,7 @@ describe("Work Context", () => { instance(mockActivityControl), instance(mockExecObserver), instance(mockActivity), + instance(mockNetworkApi), { storageProvider: instance(mockStorageProvider) }, ); const expectedStdout = [ @@ -372,6 +385,7 @@ describe("Work Context", () => { instance(mockActivityControl), instance(mockExecObserver), instance(mockActivity), + instance(mockNetworkApi), { storageProvider: instance(mockStorageProvider) }, ); const expectedStdout = [ @@ -411,6 +425,7 @@ describe("Work Context", () => { instance(mockActivityControl), instance(mockExecObserver), instance(mockActivity), + instance(mockNetworkApi), ); await expect(ctx.getState()).resolves.toEqual(ActivityStateEnum.Deployed); }); @@ -423,30 +438,27 @@ describe("Work Context", () => { instance(mockActivityControl), instance(mockExecObserver), instance(mockActivity), + instance(mockNetworkApi), ); expect(() => ctx.getIp()).toThrow(new Error("There is no network in this work context")); }); it("should return ip address of provider vpn network node", async () => { - const networkNode = new NetworkNode( - "test-node", - IPv4.fromString("192.168.0.10"), - () => ({ - id: "test-network", - ip: "192.168.0.0/24", - nodes: { - "192.168.0.10": "example-provider-id", - }, - mask: "255.255.255.0", - }), - "http://localhost", - ); + const networkNode = new NetworkNode("test-node", "192.168.0.10", () => ({ + id: "test-network", + ip: "192.168.0.0/24", + nodes: { + "192.168.0.10": "example-provider-id", + }, + mask: "255.255.255.0", + })); const ctx = new WorkContext( instance(mockActivityApi), instance(mockActivityControl), instance(mockExecObserver), instance(mockActivity), + instance(mockNetworkApi), { networkNode }, ); @@ -461,6 +473,7 @@ describe("Work Context", () => { instance(mockActivityControl), instance(mockExecObserver), instance(mockActivity), + instance(mockNetworkApi), ); expect(() => ctx.getWebsocketUri(80)).toThrow(new Error("There is no network in this work context")); @@ -474,10 +487,11 @@ describe("Work Context", () => { instance(mockActivityControl), instance(mockExecObserver), instance(mockActivity), + instance(mockNetworkApi), { networkNode: instance(mockNode) }, ); - when(mockNode.getWebsocketUri(20)).thenReturn("ws://localhost:20"); + when(mockNetworkApi.getWebsocketUri(instance(mockNode), 20)).thenReturn("ws://localhost:20"); expect(ctx.getWebsocketUri(20)).toEqual("ws://localhost:20"); }); @@ -505,6 +519,7 @@ describe("Work Context", () => { instance(mockActivityControl), instance(mockExecObserver), instance(mockActivity), + instance(mockNetworkApi), { storageProvider: instance(mockStorageProvider) }, ); @@ -535,6 +550,7 @@ describe("Work Context", () => { instance(mockActivityControl), instance(mockExecObserver), instance(mockActivity), + instance(mockNetworkApi), { activityReadySetupFunctions }, ); @@ -552,6 +568,7 @@ describe("Work Context", () => { instance(mockActivityControl), instance(mockExecObserver), instance(mockActivity), + instance(mockNetworkApi), ); when(mockExecutor.execute(_)).thenResolve( @@ -570,6 +587,7 @@ describe("Work Context", () => { instance(mockActivityControl), instance(mockExecObserver), instance(mockActivity), + instance(mockNetworkApi), ); when(mockExecutor.execute(_)).thenResolve(