diff --git a/leaf/LICENSE.md b/leaf/LICENSE.md index a6582f19..e17fa342 100644 --- a/leaf/LICENSE.md +++ b/leaf/LICENSE.md @@ -1,8 +1,8 @@ Derived from `*/LICENSE.md`: > Weird source code is Copyright (c) 2024 Weird team ([Erlend Sogge Heggen](https://github.com/erlend-sh/) & [Kapono Haws](https://github.com/zicklag/)) -and licensed as [PolyForm NonCommercial v1.0](https://polyformproject.org/licenses/noncommercial/1.0.0/). -> +> and licensed as [PolyForm NonCommercial v1.0](https://polyformproject.org/licenses/noncommercial/1.0.0/). +> > An exception is made for the contents of `/leaf` (...) All contents of `/leaf` are licensed under [Blue Oak Model License v1.0](https://blueoakcouncil.org/license/1.0.0). diff --git a/package.json b/package.json index 31433135..dfe7b0df 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "lint": "prettier --check . --log-level warn", - "format": "prettier --write . --log-level warn" + "format": "prettier --write . --log-level warn", + "test": "vitest" }, "devDependencies": { "@codemirror/lang-markdown": "^6.3.0", @@ -31,6 +32,7 @@ "@types/cookie": "^0.6.0", "@types/dns-packet": "^5.6.5", "@types/eslint": "^8.56.12", + "@types/jsdom": "^21.1.7", "@types/node": "^20.16.11", "@types/nodemailer": "^6.4.16", "@types/sanitize-html": "^2.13.0", @@ -48,6 +50,7 @@ "eslint": "^8.57.1", "eslint-config-prettier": "^9.1.0", "eslint-plugin-svelte": "^2.44.1", + "jsdom": "^25.0.1", "keyv": "^4.5.4", "leaf-proto": "workspace:*", "linktree-parser": "^1.5.0", @@ -81,6 +84,7 @@ "vite": "^5.4.9", "vite-plugin-top-level-await": "^1.4.4", "vite-plugin-wasm": "^3.3.0", + "vitest": "^2.1.5", "ws": "^8.18.0", "zod": "^3.23.8" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 71d927f4..eaf13de6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -80,6 +80,9 @@ importers: '@types/eslint': specifier: ^8.56.12 version: 8.56.12 + '@types/jsdom': + specifier: ^21.1.7 + version: 21.1.7 '@types/node': specifier: ^20.16.11 version: 20.16.11 @@ -131,6 +134,9 @@ importers: eslint-plugin-svelte: specifier: ^2.44.1 version: 2.44.1(eslint@8.57.1)(svelte@5.0.0-next.239) + jsdom: + specifier: ^25.0.1 + version: 25.0.1 keyv: specifier: ^4.5.4 version: 4.5.4 @@ -230,6 +236,9 @@ importers: vite-plugin-wasm: specifier: ^3.3.0 version: 3.3.0(vite@5.4.9(@types/node@20.16.11)) + vitest: + specifier: ^2.1.5 + version: 2.1.5(@types/node@20.16.11)(jsdom@25.0.1) ws: specifier: ^8.18.0 version: 8.18.0 @@ -1238,6 +1247,9 @@ packages: '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/jsdom@21.1.7': + resolution: {integrity: sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -1268,6 +1280,9 @@ packages: '@types/sanitize-html@2.13.0': resolution: {integrity: sha512-X31WxbvW9TjIhZZNyNBZ/p5ax4ti7qsNDBDEnH4zAgmEh35YnFD1UiS6z9Cd34kKm0LslFW0KPmTQzu/oGtsqQ==} + '@types/tough-cookie@4.0.5': + resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + '@types/underscore@1.11.15': resolution: {integrity: sha512-HP38xE+GuWGlbSRq9WrZkousaQ7dragtZCruBVMi0oX1migFZavZ3OROKHSkNp/9ouq82zrWtZpg18jFnVN96g==} @@ -1338,6 +1353,35 @@ packages: '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + '@vitest/expect@2.1.5': + resolution: {integrity: sha512-nZSBTW1XIdpZvEJyoP/Sy8fUg0b8od7ZpGDkTUcfJ7wz/VoZAFzFfLyxVxGFhUjJzhYqSbIpfMtl/+k/dpWa3Q==} + + '@vitest/mocker@2.1.5': + resolution: {integrity: sha512-XYW6l3UuBmitWqSUXTNXcVBUCRytDogBsWuNXQijc00dtnU/9OqpXWp4OJroVrad/gLIomAq9aW8yWDBtMthhQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@2.1.5': + resolution: {integrity: sha512-4ZOwtk2bqG5Y6xRGHcveZVr+6txkH7M2e+nPFd6guSoN638v/1XQ0K06eOpi0ptVU/2tW/pIU4IoPotY/GZ9fw==} + + '@vitest/runner@2.1.5': + resolution: {integrity: sha512-pKHKy3uaUdh7X6p1pxOkgkVAFW7r2I818vHDthYLvUyjRfkKOU6P45PztOch4DZarWQne+VOaIMwA/erSSpB9g==} + + '@vitest/snapshot@2.1.5': + resolution: {integrity: sha512-zmYw47mhfdfnYbuhkQvkkzYroXUumrwWDGlMjpdUr4jBd3HZiV2w7CQHj+z7AAS4VOtWxI4Zt4bWt4/sKcoIjg==} + + '@vitest/spy@2.1.5': + resolution: {integrity: sha512-aWZF3P0r3w6DiYTVskOYuhBc7EMc3jvn1TkBg8ttylFFRqNN2XGD7V5a4aQdk6QiUzZQ4klNBSpCLJgWNdIiNw==} + + '@vitest/utils@2.1.5': + resolution: {integrity: sha512-yfj6Yrp0Vesw2cwJbP+cl04OC+IHFsuQsrsJBL9pyGeQXE56v1UAOQco+SR55Vf1nQzfV0QJg1Qum7AaWUwwYg==} + '@vladfrangu/async_event_emitter@2.4.6': resolution: {integrity: sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA==} engines: {node: '>=v14.0.0', npm: '>=7.0.0'} @@ -1401,10 +1445,17 @@ packages: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + ast-types@0.13.4: resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} engines: {node: '>=4'} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + autoprefixer@10.4.20: resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==} engines: {node: ^10 || ^12 || >=14} @@ -1470,6 +1521,10 @@ packages: buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -1484,6 +1539,10 @@ packages: ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chai@5.1.2: + resolution: {integrity: sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==} + engines: {node: '>=12'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -1494,6 +1553,10 @@ packages: character-entities-legacy@3.0.0: resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + check-error@2.1.1: + resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} + engines: {node: '>= 16'} + cheerio-select@2.1.0: resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} @@ -1526,6 +1589,10 @@ packages: resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} engines: {node: '>=12.5.0'} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} @@ -1562,10 +1629,18 @@ packages: engines: {node: '>=4'} hasBin: true + cssstyle@4.1.0: + resolution: {integrity: sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA==} + engines: {node: '>=18'} + data-uri-to-buffer@6.0.2: resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} engines: {node: '>= 14'} + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + date-fns@3.6.0: resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} @@ -1578,6 +1653,13 @@ packages: supports-color: optional: true + decimal.js@10.4.3: + resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -1589,6 +1671,10 @@ packages: resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} engines: {node: '>= 14'} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + denque@2.1.0: resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} engines: {node: '>=0.10'} @@ -1677,6 +1763,9 @@ packages: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} + es-module-lexer@1.5.4: + resolution: {integrity: sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==} + es6-promise@3.3.1: resolution: {integrity: sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==} @@ -1764,10 +1853,17 @@ packages: estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + expect-type@1.1.0: + resolution: {integrity: sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==} + engines: {node: '>=12.0.0'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1815,6 +1911,10 @@ packages: resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} engines: {node: '>=14'} + form-data@4.0.1: + resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} + engines: {node: '>= 6'} + fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} @@ -1891,6 +1991,10 @@ packages: hast-util-whitespace@3.0.0: resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} @@ -1983,6 +2087,9 @@ packages: resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} engines: {node: '>=0.10.0'} + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-reference@1.2.1: resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} @@ -2009,6 +2116,15 @@ packages: jsbn@1.1.0: resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==} + jsdom@25.0.1: + resolution: {integrity: sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^2.11.2 + peerDependenciesMeta: + canvas: + optional: true + json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -2082,6 +2198,9 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + loupe@3.1.2: + resolution: {integrity: sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -2136,6 +2255,14 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + min-indent@1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} @@ -2211,6 +2338,9 @@ packages: nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + nwsapi@2.2.13: + resolution: {integrity: sha512-cTGB9ptp9dY9A5VbMSe7fQBcl/tt22Vcqdq8+eN93rblOuE0aCFu4aZ2vMwct/2t+lFnosm8RkQW1I0Omb1UtQ==} + oauth-pkce@0.0.7: resolution: {integrity: sha512-HMMakoec7G3Fyhq/VfYfQXmsJhZGQLncPCj/ir0xQQqX/kRDE9w1WFOaMJXlxfNlDDq3XDSy4mXGFPqU6oMAGg==} @@ -2293,6 +2423,13 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + pathval@2.0.0: + resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} + engines: {node: '>= 14.16'} + picocolors@1.1.0: resolution: {integrity: sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==} @@ -2561,6 +2698,9 @@ packages: rope-sequence@1.3.4: resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} + rrweb-cssom@0.7.1: + resolution: {integrity: sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -2577,6 +2717,10 @@ packages: sanitize-html@2.13.1: resolution: {integrity: sha512-ZXtKq89oue4RP7abL9wp/9URJcqQNABB5GGJ2acW1sdO8JTVl92f4ygD7Yc9Ze09VAZhnt2zegeU0tbNsdcLYg==} + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + semver@7.6.3: resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} engines: {node: '>=10'} @@ -2600,6 +2744,9 @@ packages: shiki@1.18.0: resolution: {integrity: sha512-8jo7tOXr96h9PBQmOHVrltnETn1honZZY76YA79MHheGQg55jBvbm9dtU+MI5pjC5NJCFuA6rvVTLVeSW5cE4A==} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} @@ -2649,9 +2796,15 @@ packages: sprintf-js@1.1.3: resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + standard-as-callback@2.1.0: resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + std-env@3.8.0: + resolution: {integrity: sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -2751,6 +2904,9 @@ packages: resolution: {integrity: sha512-zJJlysnVl4zShMwrs6iIeQPm0bHi7eT0JR0W0U/u2naDb00azue1K5d14cbCoIh+/HA3F+tuawwSFISZsnadww==} engines: {node: '>=18'} + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + tailwindcss@3.4.13: resolution: {integrity: sha512-KqjHOJKogOUt5Bs752ykCeiwvi0fKVkr5oqsFNt/8px/tA8scFPIlkygsf6jXrfCqGHz7VflA6+yytWuM+XhFw==} engines: {node: '>=14.0.0'} @@ -2769,10 +2925,35 @@ packages: tiny-glob@0.2.9: resolution: {integrity: sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.1: + resolution: {integrity: sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==} + + tinypool@1.0.2: + resolution: {integrity: sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + tlds@1.255.0: resolution: {integrity: sha512-tcwMRIioTcF/FcxLev8MJWxCp+GUALRhFEqbDoZrnowmKSGqPrl5pqS+Sut2m8BgJ6S4FExCSSpGffZ0Tks6Aw==} hasBin: true + tldts-core@6.1.64: + resolution: {integrity: sha512-uqnl8vGV16KsyflHOzqrYjjArjfXaU6rMPXYy2/ZWoRKCkXtghgB4VwTDXUG+t0OTGeSewNAG31/x1gCTfLt+Q==} + + tldts@6.1.64: + resolution: {integrity: sha512-ph4AE5BXWIOsSy9stpoeo7bYe/Cy7VfpciIH4RhVZUPItCJmhqWCN0EVzxd8BOHiyNb42vuJc6NWTjJkg91Tuw==} + hasBin: true + to-data-view@1.1.0: resolution: {integrity: sha512-1eAdufMg6mwgmlojAx3QeMnzB/BTVp7Tbndi3U7ftcT2zCZadjxkkmLmd97zmaxWi+sgGcgWrokmpEoy0Dn0vQ==} @@ -2784,6 +2965,14 @@ packages: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} + tough-cookie@5.0.0: + resolution: {integrity: sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==} + engines: {node: '>=16'} + + tr46@5.0.0: + resolution: {integrity: sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==} + engines: {node: '>=18'} + trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -2883,6 +3072,11 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + vite-node@2.1.5: + resolution: {integrity: sha512-rd0QIgx74q4S1Rd56XIiL2cYEdyWn13cunYBIuqh9mpmQr7gGS0IxXoP8R6OaZtNQQLyXSWbd4rXKYUbhFpK5w==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + vite-plugin-top-level-await@1.4.4: resolution: {integrity: sha512-QyxQbvcMkgt+kDb12m2P8Ed35Sp6nXP+l8ptGrnHV9zgYDUpraO0CPdlqLSeBqvY2DToR52nutDG7mIHuysdiw==} peerDependencies: @@ -2932,9 +3126,42 @@ packages: vite: optional: true + vitest@2.1.5: + resolution: {integrity: sha512-P4ljsdpuzRTPI/kbND2sDZ4VmieerR2c9szEZpjc+98Z9ebvnXmM5+0tHEKqYZumXqlvnmfWsjeFOjXVriDG7A==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 2.1.5 + '@vitest/ui': 2.1.5 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + w3c-keyname@2.2.8: resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + whatwg-encoding@3.1.1: resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} engines: {node: '>=18'} @@ -2943,11 +3170,20 @@ packages: resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} engines: {node: '>=18'} + whatwg-url@14.0.0: + resolution: {integrity: sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==} + engines: {node: '>=18'} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -2975,6 +3211,13 @@ packages: utf-8-validate: optional: true + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} @@ -3925,6 +4168,12 @@ snapshots: dependencies: '@types/unist': 3.0.3 + '@types/jsdom@21.1.7': + dependencies: + '@types/node': 20.16.11 + '@types/tough-cookie': 4.0.5 + parse5: 7.2.0 + '@types/json-schema@7.0.15': {} '@types/linkify-it@5.0.0': {} @@ -3956,6 +4205,8 @@ snapshots: dependencies: htmlparser2: 8.0.2 + '@types/tough-cookie@4.0.5': {} + '@types/underscore@1.11.15': {} '@types/unist@3.0.3': {} @@ -4047,6 +4298,46 @@ snapshots: '@ungap/structured-clone@1.2.0': {} + '@vitest/expect@2.1.5': + dependencies: + '@vitest/spy': 2.1.5 + '@vitest/utils': 2.1.5 + chai: 5.1.2 + tinyrainbow: 1.2.0 + + '@vitest/mocker@2.1.5(vite@5.4.9(@types/node@20.16.11))': + dependencies: + '@vitest/spy': 2.1.5 + estree-walker: 3.0.3 + magic-string: 0.30.12 + optionalDependencies: + vite: 5.4.9(@types/node@20.16.11) + + '@vitest/pretty-format@2.1.5': + dependencies: + tinyrainbow: 1.2.0 + + '@vitest/runner@2.1.5': + dependencies: + '@vitest/utils': 2.1.5 + pathe: 1.1.2 + + '@vitest/snapshot@2.1.5': + dependencies: + '@vitest/pretty-format': 2.1.5 + magic-string: 0.30.12 + pathe: 1.1.2 + + '@vitest/spy@2.1.5': + dependencies: + tinyspy: 3.0.2 + + '@vitest/utils@2.1.5': + dependencies: + '@vitest/pretty-format': 2.1.5 + loupe: 3.1.2 + tinyrainbow: 1.2.0 + '@vladfrangu/async_event_emitter@2.4.6': {} acorn-jsx@5.3.2(acorn@8.12.1): @@ -4097,10 +4388,14 @@ snapshots: array-union@2.1.0: {} + assertion-error@2.0.1: {} + ast-types@0.13.4: dependencies: tslib: 2.7.0 + asynckit@0.4.0: {} + autoprefixer@10.4.20(postcss@8.4.47): dependencies: browserslist: 4.24.0 @@ -4165,6 +4460,8 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + cac@6.7.14: {} + callsites@3.1.0: {} camelcase-css@2.0.1: {} @@ -4173,6 +4470,14 @@ snapshots: ccount@2.0.1: {} + chai@5.1.2: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.1.2 + pathval: 2.0.0 + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -4182,6 +4487,8 @@ snapshots: character-entities-legacy@3.0.0: {} + check-error@2.1.1: {} + cheerio-select@2.1.0: dependencies: boolbase: 1.0.0 @@ -4247,6 +4554,10 @@ snapshots: color-convert: 2.0.1 color-string: 1.9.1 + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + comma-separated-tokens@2.0.3: {} commander@4.1.1: {} @@ -4277,14 +4588,27 @@ snapshots: cssesc@3.0.0: {} + cssstyle@4.1.0: + dependencies: + rrweb-cssom: 0.7.1 + data-uri-to-buffer@6.0.2: {} + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.0.0 + date-fns@3.6.0: {} debug@4.3.7: dependencies: ms: 2.1.3 + decimal.js@10.4.3: {} + + deep-eql@5.0.2: {} + deep-is@0.1.4: {} deepmerge@4.3.1: {} @@ -4295,6 +4619,8 @@ snapshots: escodegen: 2.1.0 esprima: 4.0.1 + delayed-stream@1.0.0: {} + denque@2.1.0: {} dequal@2.0.3: {} @@ -4387,6 +4713,8 @@ snapshots: entities@4.5.0: {} + es-module-lexer@1.5.4: {} + es6-promise@3.3.1: {} esbuild@0.21.5: @@ -4532,8 +4860,14 @@ snapshots: estree-walker@2.0.2: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.6 + esutils@2.0.3: {} + expect-type@1.1.0: {} + fast-deep-equal@3.1.3: {} fast-glob@3.3.2: @@ -4582,6 +4916,12 @@ snapshots: cross-spawn: 7.0.3 signal-exit: 4.1.0 + form-data@4.0.1: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + fraction.js@4.3.7: {} fs-extra@11.2.0: @@ -4679,6 +5019,10 @@ snapshots: dependencies: '@types/hast': 3.0.4 + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + html-void-elements@3.0.0: {} htmlparser2@8.0.2: @@ -4778,6 +5122,8 @@ snapshots: is-plain-object@5.0.0: {} + is-potential-custom-element-name@1.0.1: {} + is-reference@1.2.1: dependencies: '@types/estree': 1.0.6 @@ -4804,6 +5150,34 @@ snapshots: jsbn@1.1.0: {} + jsdom@25.0.1: + dependencies: + cssstyle: 4.1.0 + data-urls: 5.0.0 + decimal.js: 10.4.3 + form-data: 4.0.1 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.5 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.13 + parse5: 7.2.0 + rrweb-cssom: 0.7.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 5.0.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.0.0 + ws: 8.18.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + json-buffer@3.0.1: {} json-schema-traverse@0.4.1: {} @@ -4864,6 +5238,8 @@ snapshots: lodash@4.17.21: {} + loupe@3.1.2: {} + lru-cache@10.4.3: {} lru-cache@7.18.3: {} @@ -4925,6 +5301,12 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + min-indent@1.0.1: {} mini-svg-data-uri@1.4.4: {} @@ -4979,6 +5361,8 @@ snapshots: dependencies: boolbase: 1.0.0 + nwsapi@2.2.13: {} + oauth-pkce@0.0.7: {} object-assign@4.1.1: {} @@ -5066,6 +5450,10 @@ snapshots: path-type@4.0.0: {} + pathe@1.1.2: {} + + pathval@2.0.0: {} + picocolors@1.1.0: {} picomatch@2.3.1: {} @@ -5325,6 +5713,8 @@ snapshots: rope-sequence@1.3.4: {} + rrweb-cssom@0.7.1: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -5351,6 +5741,10 @@ snapshots: parse-srcset: 1.0.2 postcss: 8.4.47 + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + semver@7.6.3: {} set-cookie-parser@2.7.0: {} @@ -5396,6 +5790,8 @@ snapshots: '@shikijs/vscode-textmate': 9.2.2 '@types/hast': 3.0.4 + siginfo@2.0.0: {} + signal-exit@4.1.0: {} simple-swizzle@0.2.2: @@ -5443,8 +5839,12 @@ snapshots: sprintf-js@1.1.3: {} + stackback@0.0.2: {} + standard-as-callback@2.1.0: {} + std-env@3.8.0: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -5553,6 +5953,8 @@ snapshots: magic-string: 0.30.12 zimmerframe: 1.1.2 + symbol-tree@3.2.4: {} + tailwindcss@3.4.13: dependencies: '@alloc/quick-lru': 5.2.0 @@ -5595,8 +5997,24 @@ snapshots: globalyzer: 0.1.0 globrex: 0.1.2 + tinybench@2.9.0: {} + + tinyexec@0.3.1: {} + + tinypool@1.0.2: {} + + tinyrainbow@1.2.0: {} + + tinyspy@3.0.2: {} + tlds@1.255.0: {} + tldts-core@6.1.64: {} + + tldts@6.1.64: + dependencies: + tldts-core: 6.1.64 + to-data-view@1.1.0: {} to-regex-range@5.0.1: @@ -5605,6 +6023,14 @@ snapshots: totalist@3.0.1: {} + tough-cookie@5.0.0: + dependencies: + tldts: 6.1.64 + + tr46@5.0.0: + dependencies: + punycode: 2.3.1 + trim-lines@3.0.1: {} ts-api-utils@1.3.0(typescript@5.6.3): @@ -5697,6 +6123,24 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 + vite-node@2.1.5(@types/node@20.16.11): + dependencies: + cac: 6.7.14 + debug: 4.3.7 + es-module-lexer: 1.5.4 + pathe: 1.1.2 + vite: 5.4.9(@types/node@20.16.11) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + vite-plugin-top-level-await@1.4.4(rollup@4.24.0)(vite@5.4.9(@types/node@20.16.11)): dependencies: '@rollup/plugin-virtual': 3.0.2(rollup@4.24.0) @@ -5724,18 +6168,70 @@ snapshots: optionalDependencies: vite: 5.4.9(@types/node@20.16.11) + vitest@2.1.5(@types/node@20.16.11)(jsdom@25.0.1): + dependencies: + '@vitest/expect': 2.1.5 + '@vitest/mocker': 2.1.5(vite@5.4.9(@types/node@20.16.11)) + '@vitest/pretty-format': 2.1.5 + '@vitest/runner': 2.1.5 + '@vitest/snapshot': 2.1.5 + '@vitest/spy': 2.1.5 + '@vitest/utils': 2.1.5 + chai: 5.1.2 + debug: 4.3.7 + expect-type: 1.1.0 + magic-string: 0.30.12 + pathe: 1.1.2 + std-env: 3.8.0 + tinybench: 2.9.0 + tinyexec: 0.3.1 + tinypool: 1.0.2 + tinyrainbow: 1.2.0 + vite: 5.4.9(@types/node@20.16.11) + vite-node: 2.1.5(@types/node@20.16.11) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 20.16.11 + jsdom: 25.0.1 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + w3c-keyname@2.2.8: {} + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + webidl-conversions@7.0.0: {} + whatwg-encoding@3.1.1: dependencies: iconv-lite: 0.6.3 whatwg-mimetype@4.0.0: {} + whatwg-url@14.0.0: + dependencies: + tr46: 5.0.0 + webidl-conversions: 7.0.0 + which@2.0.2: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + word-wrap@1.2.5: {} wrap-ansi@7.0.0: @@ -5754,6 +6250,10 @@ snapshots: ws@8.18.0: {} + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + yallist@4.0.0: {} yaml@1.10.2: {} diff --git a/src/lib/leaf/profile.ts b/src/lib/leaf/profile.ts index 4f77f36c..ba8bb1c5 100644 --- a/src/lib/leaf/profile.ts +++ b/src/lib/leaf/profile.ts @@ -2,7 +2,13 @@ import { BorshSchema, Component, intoPathSegment } from 'leaf-proto'; import { CommonMark, Description, RawImage, Name } from 'leaf-proto/components'; import _ from 'underscore'; -import { listUsers, userSubspaceByRauthyId, userSubspaceByUsername } from '$lib/usernames/index'; +import { + listUsers, + userNameByRauthyId, + userSubspaceByRauthyId, + userSubspaceByUsername +} from '$lib/usernames/index'; +import { LinkVerifier } from '$lib/link_verifier/LinkVerifier'; import { resolveUserSubspaceFromDNS } from '$lib/dns/resolve'; import { leafClient, subspace_link } from '.'; @@ -302,6 +308,10 @@ export async function getProfileByDomain(domain: string): Promise { let link = await profileLinkById(rauthyId); if (!link) throw `user has not yet claimed a username.`; + const userName = await userNameByRauthyId(rauthyId); + if (!userName) throw `user has no username`; + const linkVerifier = new LinkVerifier(profile.links, userName); + await linkVerifier.verify(); await setProfile(link, profile); } diff --git a/src/lib/link_verifier/LinkVerifier.test.ts b/src/lib/link_verifier/LinkVerifier.test.ts new file mode 100644 index 00000000..e9597443 --- /dev/null +++ b/src/lib/link_verifier/LinkVerifier.test.ts @@ -0,0 +1,26 @@ +import { expect, test } from 'vitest'; + +import { WebLink, WebLinks } from '$lib/leaf/profile'; + +import { LinkVerifier } from './LinkVerifier'; + +test('filters verifiable origins', () => { + const webLinks = [ + { + url: 'https://github.com/EstebanBorai', + label: 'GitHub' + }, + { + url: 'https://www.nytimes.com/', + label: 'The New York Times' + } + ]; + const linkVerifier = new LinkVerifier(webLinks); + const filteredLinks = linkVerifier.links.map((webLink) => ({ + url: webLink.url, + label: webLink.label + })); + + expect(filteredLinks.find(({ url }) => webLinks[0].url === url)).toBeTruthy(); + expect(filteredLinks.find(({ url }) => webLinks[1].url === url)).toBeFalsy(); +}); diff --git a/src/lib/link_verifier/LinkVerifier.ts b/src/lib/link_verifier/LinkVerifier.ts new file mode 100644 index 00000000..ee049f36 --- /dev/null +++ b/src/lib/link_verifier/LinkVerifier.ts @@ -0,0 +1,82 @@ +import { JSDOM } from 'jsdom'; + +import { GitHubLinkVerificationStrategy } from './strategy/GitHubLinkVerificationStrategy'; + +import type { WebLink } from '$lib/leaf/profile'; +import type { LinkVerificationStrategyFactory } from './strategy/LinkVerificationStrategy'; +import type { ExactLink } from 'leaf-proto'; + +export const VERIFIABLE_ORIGIN_STRATEGY: Record = { + 'https://github.com': (dom) => new GitHubLinkVerificationStrategy(dom) +}; + +export const VERIFIABLE_ORIGINS: string[] = Object.keys(VERIFIABLE_ORIGIN_STRATEGY); + +export function verifiableOriginFilter(webLink: WebLink): boolean { + return ( + VERIFIABLE_ORIGINS.findIndex((url) => new URL(webLink.url).origin === new URL(url).origin) !== + -1 + ); +} + +export type LinkArray = { label?: string; url: string }[]; + +export class LinkVerifier { + private webLinks: LinkArray; + private userName: string; + + constructor(links: LinkArray, userName: string) { + this.webLinks = links.filter(verifiableOriginFilter); + this.userName = userName; + } + + private static async fetchHtml(webLink: WebLink): Promise { + const res = await fetch(webLink.url); + + if (res.status === 200) { + const resText = await res.text(); + return new JSDOM(resText); + } + + throw new Error( + `Failed to fetch "${webLink.url}", expected a 200 HTTP Response, got "${res.status}" instead.` + ); + } + + get links(): WebLink[] { + return [...this.webLinks]; + } + + async verify(): Promise { + const verifiedLinks: WebLink[] = []; + + for (const webLink of this.webLinks) { + const origin = new URL(webLink.url).origin; + const linkVerificationStrategyFactory = VERIFIABLE_ORIGIN_STRATEGY[ + origin + ] as LinkVerificationStrategyFactory | null; + + if (typeof linkVerificationStrategyFactory === 'function') { + const dom = await LinkVerifier.fetchHtml(webLink); + const strategy = linkVerificationStrategyFactory(dom); + const isVerified = await strategy.verify(this.userProfileLink()); + + if (isVerified) { + verifiedLinks.push(webLink); + } + + continue; + } + + // This should not happen, but if we got here somehow its likely we got a false positive + // in the origins map. + throw new Error(`The WebLink with URL "${webLink.url}" is not supported by any strategy.`); + } + + return verifiedLinks; + } + + private userProfileLink(): string { + return `https://a.weird.one/${this.userName}`; + } +} diff --git a/src/lib/link_verifier/strategy/GitHubLinkVerificationStrategy.test.ts b/src/lib/link_verifier/strategy/GitHubLinkVerificationStrategy.test.ts new file mode 100644 index 00000000..7d68ac5f --- /dev/null +++ b/src/lib/link_verifier/strategy/GitHubLinkVerificationStrategy.test.ts @@ -0,0 +1,82 @@ +import { JSDOM } from 'jsdom'; +import { expect, test } from 'vitest'; + +import { GitHubLinkVerificationStrategy } from './GitHubLinkVerificationStrategy'; + +const GITHUB_PROFILE_SNIPPET = ` + `; + +test('verifies an owned githubs link', async () => { + const dom = new JSDOM(GITHUB_PROFILE_SNIPPET); + const gitHubLinkVerificationStrategy = new GitHubLinkVerificationStrategy(dom); + const isOwner = await gitHubLinkVerificationStrategy.verify('https://a.weird.one/estebanborai'); + + expect(isOwner).toStrictEqual(true); +}); + +test('invalidates a non owners github link', async () => { + const dom = new JSDOM(GITHUB_PROFILE_SNIPPET); + const gitHubLinkVerificationStrategy = new GitHubLinkVerificationStrategy(dom); + const isOwner = await gitHubLinkVerificationStrategy.verify('https://a.weird.one/zicklag'); + + expect(isOwner).toStrictEqual(false); +}); diff --git a/src/lib/link_verifier/strategy/GitHubLinkVerificationStrategy.ts b/src/lib/link_verifier/strategy/GitHubLinkVerificationStrategy.ts new file mode 100644 index 00000000..c9a99d0c --- /dev/null +++ b/src/lib/link_verifier/strategy/GitHubLinkVerificationStrategy.ts @@ -0,0 +1,27 @@ +import { JSDOM } from 'jsdom'; + +import { LinkVerificationStrategy } from './LinkVerificationStrategy'; + +export class GitHubLinkVerificationStrategy extends LinkVerificationStrategy { + constructor(dom: JSDOM) { + super('GitHubLinkVerificationStrategy', dom); + } + + async verify(userProfileLink: string): Promise { + const document = this.dom.window.document; + const nodes = Array.from(document.querySelectorAll('a[rel="nofollow me"]')); + const element = nodes.find((node) => node.getAttribute('href')?.startsWith(userProfileLink)); + + if (!element) { + return false; + } + + const href = element.getAttribute('href'); + + if (!href) { + return false; + } + + return href.startsWith(userProfileLink); + } +} diff --git a/src/lib/link_verifier/strategy/LinkVerificationStrategy.ts b/src/lib/link_verifier/strategy/LinkVerificationStrategy.ts new file mode 100644 index 00000000..f07730d1 --- /dev/null +++ b/src/lib/link_verifier/strategy/LinkVerificationStrategy.ts @@ -0,0 +1,26 @@ +import { JSDOM } from 'jsdom'; + +export interface ILinkVerificationStrategy { + name: string; + verify(userProfileLink: string): Promise; +} + +export type LinkVerificationStrategyFactory = (dom: JSDOM) => ILinkVerificationStrategy; + +export abstract class LinkVerificationStrategy implements ILinkVerificationStrategy { + protected dom: JSDOM; + protected strategyName: string; + + constructor(name: string, dom: JSDOM) { + this.dom = dom; + this.strategyName = name; + } + + get name(): string { + return this.strategyName; + } + + public async verify(userProfileLink: string): Promise { + throw new Error('Not Implemented'); + } +} diff --git a/vite.config.ts b/vite.config.ts index 25c7f965..4018b104 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,3 +1,5 @@ +/// + import { sveltekit } from '@sveltejs/kit/vite'; import wasm from 'vite-plugin-wasm'; import topLevelAwait from 'vite-plugin-top-level-await'; @@ -18,5 +20,9 @@ export default defineConfig({ rollupOptions: { external: 'sharp' } + }, + test: { + include: ['./src/**/*.{test,spec}.?(c|m)[jt]s?(x)'], + watch: false } });