Adding: themes, predictions, match reporting, ui fixes, sse fixes, badge fixes, match flow, i18n, match edits, perf, auth fix #22
@@ -62,6 +62,7 @@
|
|||||||
"typescript": "^5.7.2",
|
"typescript": "^5.7.2",
|
||||||
"vite": "^7.1.7",
|
"vite": "^7.1.7",
|
||||||
"vite-tsconfig-paths": "^5.1.4",
|
"vite-tsconfig-paths": "^5.1.4",
|
||||||
|
"vitest": "^4.1.11",
|
||||||
"workbox-build": "^7.4.1",
|
"workbox-build": "^7.4.1",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -499,6 +500,8 @@
|
|||||||
|
|
||||||
"@solid-primitives/utils": ["@solid-primitives/utils@6.3.2", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-hZ/M/qr25QOCcwDPOHtGjxTD8w2mNyVAYvcfgwzBHq2RwNqHNdDNsMZYap20+ruRwW4A3Cdkczyoz0TSxLCAPQ=="],
|
"@solid-primitives/utils": ["@solid-primitives/utils@6.3.2", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-hZ/M/qr25QOCcwDPOHtGjxTD8w2mNyVAYvcfgwzBHq2RwNqHNdDNsMZYap20+ruRwW4A3Cdkczyoz0TSxLCAPQ=="],
|
||||||
|
|
||||||
|
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||||
|
|
||||||
"@tanstack/devtools": ["@tanstack/devtools@0.7.0", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/keyboard": "^1.3.3", "@solid-primitives/resize-observer": "^2.1.3", "@tanstack/devtools-client": "0.0.3", "@tanstack/devtools-event-bus": "0.3.3", "@tanstack/devtools-ui": "0.4.4", "clsx": "^2.1.1", "goober": "^2.1.16", "solid-js": "^1.9.9" } }, "sha512-AlAoCqJhWLg9GBEaoV1g/j+X/WA1aJSWOsekxeuZpYeS2hdVuKAjj04KQLUMJhtLfNl2s2E+TCj7ZRtWyY3U4w=="],
|
"@tanstack/devtools": ["@tanstack/devtools@0.7.0", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/keyboard": "^1.3.3", "@solid-primitives/resize-observer": "^2.1.3", "@tanstack/devtools-client": "0.0.3", "@tanstack/devtools-event-bus": "0.3.3", "@tanstack/devtools-ui": "0.4.4", "clsx": "^2.1.1", "goober": "^2.1.16", "solid-js": "^1.9.9" } }, "sha512-AlAoCqJhWLg9GBEaoV1g/j+X/WA1aJSWOsekxeuZpYeS2hdVuKAjj04KQLUMJhtLfNl2s2E+TCj7ZRtWyY3U4w=="],
|
||||||
|
|
||||||
"@tanstack/devtools-client": ["@tanstack/devtools-client@0.0.3", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.3.3" } }, "sha512-kl0r6N5iIL3t9gGDRAv55VRM3UIyMKVH83esRGq7xBjYsRLe/BeCIN2HqrlJkObUXQMKhy7i8ejuGOn+bDqDBw=="],
|
"@tanstack/devtools-client": ["@tanstack/devtools-client@0.0.3", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.3.3" } }, "sha512-kl0r6N5iIL3t9gGDRAv55VRM3UIyMKVH83esRGq7xBjYsRLe/BeCIN2HqrlJkObUXQMKhy7i8ejuGOn+bDqDBw=="],
|
||||||
@@ -631,6 +634,10 @@
|
|||||||
|
|
||||||
"@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="],
|
"@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="],
|
||||||
|
|
||||||
|
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
|
||||||
|
|
||||||
|
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
|
||||||
|
|
||||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||||
|
|
||||||
"@types/istanbul-lib-coverage": ["@types/istanbul-lib-coverage@2.0.6", "", {}, "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w=="],
|
"@types/istanbul-lib-coverage": ["@types/istanbul-lib-coverage@2.0.6", "", {}, "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w=="],
|
||||||
@@ -665,6 +672,20 @@
|
|||||||
|
|
||||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.3", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.2", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-NVUnA6gQCl8jfoYqKqQU5Clv0aPw14KkZYCsX6T9Lfu9slI0LOU10OTwFHS/WmptsMMpshNd/1tuWsHQ2Uk+cg=="],
|
"@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.3", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.2", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-NVUnA6gQCl8jfoYqKqQU5Clv0aPw14KkZYCsX6T9Lfu9slI0LOU10OTwFHS/WmptsMMpshNd/1tuWsHQ2Uk+cg=="],
|
||||||
|
|
||||||
|
"@vitest/expect": ["@vitest/expect@4.1.11", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.11", "@vitest/utils": "4.1.11", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw=="],
|
||||||
|
|
||||||
|
"@vitest/mocker": ["@vitest/mocker@4.1.11", "", { "dependencies": { "@vitest/spy": "4.1.11", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ=="],
|
||||||
|
|
||||||
|
"@vitest/pretty-format": ["@vitest/pretty-format@4.1.11", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw=="],
|
||||||
|
|
||||||
|
"@vitest/runner": ["@vitest/runner@4.1.11", "", { "dependencies": { "@vitest/utils": "4.1.11", "pathe": "^2.0.3" } }, "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw=="],
|
||||||
|
|
||||||
|
"@vitest/snapshot": ["@vitest/snapshot@4.1.11", "", { "dependencies": { "@vitest/pretty-format": "4.1.11", "@vitest/utils": "4.1.11", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog=="],
|
||||||
|
|
||||||
|
"@vitest/spy": ["@vitest/spy@4.1.11", "", {}, "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA=="],
|
||||||
|
|
||||||
|
"@vitest/utils": ["@vitest/utils@4.1.11", "", { "dependencies": { "@vitest/pretty-format": "4.1.11", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ=="],
|
||||||
|
|
||||||
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||||
|
|
||||||
"agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
|
"agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
|
||||||
@@ -687,6 +708,8 @@
|
|||||||
|
|
||||||
"asn1.js": ["asn1.js@5.4.1", "", { "dependencies": { "bn.js": "^4.0.0", "inherits": "^2.0.1", "minimalistic-assert": "^1.0.0", "safer-buffer": "^2.1.0" } }, "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA=="],
|
"asn1.js": ["asn1.js@5.4.1", "", { "dependencies": { "bn.js": "^4.0.0", "inherits": "^2.0.1", "minimalistic-assert": "^1.0.0", "safer-buffer": "^2.1.0" } }, "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA=="],
|
||||||
|
|
||||||
|
"assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
|
||||||
|
|
||||||
"async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
|
"async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
|
||||||
|
|
||||||
"async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="],
|
"async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="],
|
||||||
@@ -749,6 +772,8 @@
|
|||||||
|
|
||||||
"caniuse-lite": ["caniuse-lite@1.0.30001769", "", {}, "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg=="],
|
"caniuse-lite": ["caniuse-lite@1.0.30001769", "", {}, "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg=="],
|
||||||
|
|
||||||
|
"chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
|
||||||
|
|
||||||
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
||||||
|
|
||||||
"chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
|
"chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
|
||||||
@@ -855,6 +880,8 @@
|
|||||||
|
|
||||||
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||||
|
|
||||||
|
"es-module-lexer": ["es-module-lexer@2.3.2", "", {}, "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw=="],
|
||||||
|
|
||||||
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||||
|
|
||||||
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
|
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
|
||||||
@@ -867,12 +894,14 @@
|
|||||||
|
|
||||||
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||||
|
|
||||||
"estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
|
||||||
|
|
||||||
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
|
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
|
||||||
|
|
||||||
"eta": ["eta@4.6.0", "", {}, "sha512-lW6is4T1NFOYnmqGZIfvixqj7A7sSvScF+DN8EK6K58xI5MZ5UvYe0GjopxOXQtZvUn4eDdVuZ8XSoYWTMEKwA=="],
|
"eta": ["eta@4.6.0", "", {}, "sha512-lW6is4T1NFOYnmqGZIfvixqj7A7sSvScF+DN8EK6K58xI5MZ5UvYe0GjopxOXQtZvUn4eDdVuZ8XSoYWTMEKwA=="],
|
||||||
|
|
||||||
|
"expect-type": ["expect-type@1.4.0", "", {}, "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA=="],
|
||||||
|
|
||||||
"exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="],
|
"exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="],
|
||||||
|
|
||||||
"facehash": ["facehash@0.0.7", "", { "peerDependencies": { "@types/react": "", "next": ">=15", "react": ">=18 <20", "react-dom": ">=18 <20" }, "optionalPeers": ["@types/react", "next"] }, "sha512-P4fw6z5DIGMbjtqEaOw7fYvYpQetSOSJOfqy3xuET7cDUI6f9CKlSX0UZIYNrtsPpCoz3LoPP5E8bNbpZBP30A=="],
|
"facehash": ["facehash@0.0.7", "", { "peerDependencies": { "@types/react": "", "next": ">=15", "react": ">=18 <20", "react-dom": ">=18 <20" }, "optionalPeers": ["@types/react", "next"] }, "sha512-P4fw6z5DIGMbjtqEaOw7fYvYpQetSOSJOfqy3xuET7cDUI6f9CKlSX0UZIYNrtsPpCoz3LoPP5E8bNbpZBP30A=="],
|
||||||
@@ -1189,6 +1218,8 @@
|
|||||||
|
|
||||||
"object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="],
|
"object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="],
|
||||||
|
|
||||||
|
"obug": ["obug@2.1.4", "", {}, "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA=="],
|
||||||
|
|
||||||
"onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
|
"onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
|
||||||
|
|
||||||
"ora": ["ora@9.4.1", "", { "dependencies": { "chalk": "^5.6.2", "cli-cursor": "^5.0.0", "cli-spinners": "^3.2.0", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.1.0", "log-symbols": "^7.0.1", "stdin-discarder": "^0.3.2", "string-width": "^8.1.0" } }, "sha512-6VlU9MLXbjVQD04AZCMX28hVtA5bUoadvUqO76MUCVA0ilwJbMiHsITRPfyVm6p/BC0Av/BXMujx39WCe1LEqw=="],
|
"ora": ["ora@9.4.1", "", { "dependencies": { "chalk": "^5.6.2", "cli-cursor": "^5.0.0", "cli-spinners": "^3.2.0", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.1.0", "log-symbols": "^7.0.1", "stdin-discarder": "^0.3.2", "string-width": "^8.1.0" } }, "sha512-6VlU9MLXbjVQD04AZCMX28hVtA5bUoadvUqO76MUCVA0ilwJbMiHsITRPfyVm6p/BC0Av/BXMujx39WCe1LEqw=="],
|
||||||
@@ -1397,6 +1428,8 @@
|
|||||||
|
|
||||||
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
|
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
|
||||||
|
|
||||||
|
"siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
|
||||||
|
|
||||||
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||||
|
|
||||||
"smob": ["smob@1.6.2", "", {}, "sha512-RQsvleCbF8cVHEv+xuDGaA4pOizFqJ0GgjtMSRo6oP8pnN7WsigHgVGey6aILRBKv4W2YOMHLqbKdnB6hpB9fw=="],
|
"smob": ["smob@1.6.2", "", {}, "sha512-RQsvleCbF8cVHEv+xuDGaA4pOizFqJ0GgjtMSRo6oP8pnN7WsigHgVGey6aILRBKv4W2YOMHLqbKdnB6hpB9fw=="],
|
||||||
@@ -1413,6 +1446,10 @@
|
|||||||
|
|
||||||
"srvx": ["srvx@0.11.22", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-LqZxxBDMKuMAZzFzJnDCkFOrs9MZQZr0LvHiO/SuSZVdQaXD7xQ5UWTUxheJrQPve1qk9MG2B/yttUvJxw8egQ=="],
|
"srvx": ["srvx@0.11.22", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-LqZxxBDMKuMAZzFzJnDCkFOrs9MZQZr0LvHiO/SuSZVdQaXD7xQ5UWTUxheJrQPve1qk9MG2B/yttUvJxw8egQ=="],
|
||||||
|
|
||||||
|
"stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
|
||||||
|
|
||||||
|
"std-env": ["std-env@4.2.0", "", {}, "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw=="],
|
||||||
|
|
||||||
"stdin-discarder": ["stdin-discarder@0.3.2", "", {}, "sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A=="],
|
"stdin-discarder": ["stdin-discarder@0.3.2", "", {}, "sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A=="],
|
||||||
|
|
||||||
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
|
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
|
||||||
@@ -1457,10 +1494,16 @@
|
|||||||
|
|
||||||
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
|
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
|
||||||
|
|
||||||
|
"tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
|
||||||
|
|
||||||
|
"tinyexec": ["tinyexec@1.3.0", "", {}, "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ=="],
|
||||||
|
|
||||||
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||||
|
|
||||||
"tinypool": ["tinypool@2.1.0", "", {}, "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw=="],
|
"tinypool": ["tinypool@2.1.0", "", {}, "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw=="],
|
||||||
|
|
||||||
|
"tinyrainbow": ["tinyrainbow@3.1.1", "", {}, "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw=="],
|
||||||
|
|
||||||
"tldts": ["tldts@6.1.86", "", { "dependencies": { "tldts-core": "^6.1.86" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ=="],
|
"tldts": ["tldts@6.1.86", "", { "dependencies": { "tldts-core": "^6.1.86" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ=="],
|
||||||
|
|
||||||
"tldts-core": ["tldts-core@6.1.86", "", {}, "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA=="],
|
"tldts-core": ["tldts-core@6.1.86", "", {}, "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA=="],
|
||||||
@@ -1541,6 +1584,8 @@
|
|||||||
|
|
||||||
"vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="],
|
"vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="],
|
||||||
|
|
||||||
|
"vitest": ["vitest@4.1.11", "", { "dependencies": { "@vitest/expect": "4.1.11", "@vitest/mocker": "4.1.11", "@vitest/pretty-format": "4.1.11", "@vitest/runner": "4.1.11", "@vitest/snapshot": "4.1.11", "@vitest/spy": "4.1.11", "@vitest/utils": "4.1.11", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.11", "@vitest/browser-preview": "4.1.11", "@vitest/browser-webdriverio": "4.1.11", "@vitest/coverage-istanbul": "4.1.11", "@vitest/coverage-v8": "4.1.11", "@vitest/ui": "4.1.11", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw=="],
|
||||||
|
|
||||||
"w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="],
|
"w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="],
|
||||||
|
|
||||||
"web-push": ["web-push@3.6.7", "", { "dependencies": { "asn1.js": "^5.3.0", "http_ece": "1.2.0", "https-proxy-agent": "^7.0.0", "jws": "^4.0.0", "minimist": "^1.2.5" }, "bin": { "web-push": "src/cli.js" } }, "sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A=="],
|
"web-push": ["web-push@3.6.7", "", { "dependencies": { "asn1.js": "^5.3.0", "http_ece": "1.2.0", "https-proxy-agent": "^7.0.0", "jws": "^4.0.0", "minimist": "^1.2.5" }, "bin": { "web-push": "src/cli.js" } }, "sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A=="],
|
||||||
@@ -1561,6 +1606,8 @@
|
|||||||
|
|
||||||
"which-typed-array": ["which-typed-array@1.1.22", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.9", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw=="],
|
"which-typed-array": ["which-typed-array@1.1.22", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.9", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw=="],
|
||||||
|
|
||||||
|
"why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
|
||||||
|
|
||||||
"workbox-background-sync": ["workbox-background-sync@7.4.1", "", { "dependencies": { "idb": "^7.0.1", "workbox-core": "7.4.1" } }, "sha512-HhT7KE8tOWDm02wRNshXUnUPofMlhenF2DBdUnDPOubhizzPeItkYTmAB6td1Z2cjYPa98vzEiPLEuzn5hN66g=="],
|
"workbox-background-sync": ["workbox-background-sync@7.4.1", "", { "dependencies": { "idb": "^7.0.1", "workbox-core": "7.4.1" } }, "sha512-HhT7KE8tOWDm02wRNshXUnUPofMlhenF2DBdUnDPOubhizzPeItkYTmAB6td1Z2cjYPa98vzEiPLEuzn5hN66g=="],
|
||||||
|
|
||||||
"workbox-broadcast-update": ["workbox-broadcast-update@7.4.1", "", { "dependencies": { "workbox-core": "7.4.1" } }, "sha512-uAlgslKLvbQY+suirIdnBCSYrcgBhjp81Nj4l1lj/Jmj0MJO2CJERnCJjT0GFVwmReV0N+zs78K6gqd5gr9/+A=="],
|
"workbox-broadcast-update": ["workbox-broadcast-update@7.4.1", "", { "dependencies": { "workbox-core": "7.4.1" } }, "sha512-uAlgslKLvbQY+suirIdnBCSYrcgBhjp81Nj4l1lj/Jmj0MJO2CJERnCJjT0GFVwmReV0N+zs78K6gqd5gr9/+A=="],
|
||||||
@@ -1817,6 +1864,8 @@
|
|||||||
|
|
||||||
"@jest/types/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
"@jest/types/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||||
|
|
||||||
|
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||||
|
|
||||||
"@rollup/pluginutils/picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="],
|
"@rollup/pluginutils/picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="],
|
||||||
|
|
||||||
"@tanstack/router-generator/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
|
"@tanstack/router-generator/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
|
||||||
@@ -1907,6 +1956,8 @@
|
|||||||
|
|
||||||
"unplugin/picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="],
|
"unplugin/picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="],
|
||||||
|
|
||||||
|
"vitest/picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="],
|
||||||
|
|
||||||
"web-push/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
"web-push/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
||||||
|
|
||||||
"workbox-build/source-map": ["source-map@0.8.0-beta.0", "", { "dependencies": { "whatwg-url": "^7.0.0" } }, "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA=="],
|
"workbox-build/source-map": ["source-map@0.8.0-beta.0", "", { "dependencies": { "whatwg-url": "^7.0.0" } }, "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA=="],
|
||||||
|
|||||||
+3
-1
@@ -12,7 +12,8 @@
|
|||||||
"extract": "lingui extract --clean",
|
"extract": "lingui extract --clean",
|
||||||
"i18n:check": "lingui extract --clean && lingui compile --strict",
|
"i18n:check": "lingui extract --clean && lingui compile --strict",
|
||||||
"build": "lingui extract --clean && vite build && tsc --noEmit && bun scripts/generate-sw.mjs",
|
"build": "lingui extract --clean && vite build && tsc --noEmit && bun scripts/generate-sw.mjs",
|
||||||
"start": "bun run server.ts"
|
"start": "bun run server.ts",
|
||||||
|
"test": "vitest run"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hello-pangea/dnd": "^18.0.1",
|
"@hello-pangea/dnd": "^18.0.1",
|
||||||
@@ -73,6 +74,7 @@
|
|||||||
"typescript": "^5.7.2",
|
"typescript": "^5.7.2",
|
||||||
"vite": "^7.1.7",
|
"vite": "^7.1.7",
|
||||||
"vite-tsconfig-paths": "^5.1.4",
|
"vite-tsconfig-paths": "^5.1.4",
|
||||||
|
"vitest": "^4.1.11",
|
||||||
"workbox-build": "^7.4.1"
|
"workbox-build": "^7.4.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { toServerResult } from "@/lib/tanstack-query/utils/to-server-result";
|
import { toServerResult } from "@/lib/tanstack-query/utils/to-server-result";
|
||||||
import { superTokensAdminFunctionMiddleware, superTokensFunctionMiddleware } from "@/utils/supertokens";
|
import { superTokensAdminFunctionMiddleware, superTokensFunctionMiddleware } from "@/utils/supertokens";
|
||||||
|
import { serverFnLoggingMiddleware } from "@/utils/activities";
|
||||||
import { createServerFn } from "@tanstack/react-start";
|
import { createServerFn } from "@tanstack/react-start";
|
||||||
import { pbAdmin } from "@/lib/pocketbase/client";
|
import { pbAdmin } from "@/lib/pocketbase/client";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -13,7 +14,7 @@ export const getPlayerBadges = createServerFn()
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const migrateBadgeProgress = createServerFn()
|
export const migrateBadgeProgress = createServerFn()
|
||||||
.middleware([superTokensAdminFunctionMiddleware])
|
.middleware([superTokensAdminFunctionMiddleware, serverFnLoggingMiddleware])
|
||||||
.handler(async () =>
|
.handler(async () =>
|
||||||
toServerResult(async () => {
|
toServerResult(async () => {
|
||||||
const result = await pbAdmin.migrateBadgeProgress();
|
const result = await pbAdmin.migrateBadgeProgress();
|
||||||
@@ -37,7 +38,7 @@ export const awardManualBadge = createServerFn()
|
|||||||
playerId: z.string(),
|
playerId: z.string(),
|
||||||
badgeId: z.string(),
|
badgeId: z.string(),
|
||||||
}))
|
}))
|
||||||
.middleware([superTokensAdminFunctionMiddleware])
|
.middleware([superTokensAdminFunctionMiddleware, serverFnLoggingMiddleware])
|
||||||
.handler(async ({ data }) =>
|
.handler(async ({ data }) =>
|
||||||
toServerResult(async () => {
|
toServerResult(async () => {
|
||||||
const result = await pbAdmin.awardManualBadge(data.playerId, data.badgeId);
|
const result = await pbAdmin.awardManualBadge(data.playerId, data.badgeId);
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export const updatePlayer = createServerFn()
|
|||||||
|
|
||||||
export const createPlayer = createServerFn()
|
export const createPlayer = createServerFn()
|
||||||
.validator(playerInputSchema)
|
.validator(playerInputSchema)
|
||||||
.middleware([superTokensFunctionMiddleware])
|
.middleware([superTokensFunctionMiddleware, serverFnLoggingMiddleware])
|
||||||
.handler(async ({ context, data }) =>
|
.handler(async ({ context, data }) =>
|
||||||
toServerResult(async () => {
|
toServerResult(async () => {
|
||||||
const userAuthId = context.userAuthId;
|
const userAuthId = context.userAuthId;
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ export const generateRandomTeams = createServerFn()
|
|||||||
tournamentId: z.string(),
|
tournamentId: z.string(),
|
||||||
seed: z.number().optional()
|
seed: z.number().optional()
|
||||||
}))
|
}))
|
||||||
.middleware([superTokensAdminFunctionMiddleware])
|
.middleware([superTokensAdminFunctionMiddleware, serverFnLoggingMiddleware])
|
||||||
.handler(async ({ data }) =>
|
.handler(async ({ data }) =>
|
||||||
toServerResult(async () => {
|
toServerResult(async () => {
|
||||||
const freeAgents = await pbAdmin.getFreeAgents(data.tournamentId);
|
const freeAgents = await pbAdmin.getFreeAgents(data.tournamentId);
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import PocketBase from "pocketbase";
|
import PocketBase from "pocketbase";
|
||||||
import { PlayerInfo } from "@/features/players/types";
|
import { PlayerInfo } from "@/features/players/types";
|
||||||
|
import { pbFilter } from "../util/filter";
|
||||||
|
import { likePattern } from "../util/like-pattern";
|
||||||
|
|
||||||
export interface Activity {
|
export interface Activity {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -61,15 +63,15 @@ export function createActivitiesService(pb: PocketBase) {
|
|||||||
const filters: string[] = [];
|
const filters: string[] = [];
|
||||||
|
|
||||||
if (name) {
|
if (name) {
|
||||||
filters.push(`name ~ "${name}"`);
|
filters.push(pbFilter(pb, "name ~ {:name}", { name: likePattern(name) }));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (player) {
|
if (player) {
|
||||||
filters.push(`player = "${player}"`);
|
filters.push(pbFilter(pb, "player = {:player}", { player }));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (success !== undefined) {
|
if (success !== undefined) {
|
||||||
filters.push(`success = ${success}`);
|
filters.push(pbFilter(pb, "success = {:success}", { success }));
|
||||||
}
|
}
|
||||||
|
|
||||||
const filterString = filters.length > 0 ? filters.join(" && ") : "";
|
const filterString = filters.length > 0 ? filters.join(" && ") : "";
|
||||||
@@ -98,7 +100,7 @@ export function createActivitiesService(pb: PocketBase) {
|
|||||||
|
|
||||||
async getActivitiesByUser(userId: string, limit: number = 50): Promise<Activity[]> {
|
async getActivitiesByUser(userId: string, limit: number = 50): Promise<Activity[]> {
|
||||||
const result = await pb.collection("activities").getList<Activity>(1, limit, {
|
const result = await pb.collection("activities").getList<Activity>(1, limit, {
|
||||||
filter: `player = "${userId}"`,
|
filter: pbFilter(pb, "player = {:userId}", { userId }),
|
||||||
sort: "-created",
|
sort: "-created",
|
||||||
});
|
});
|
||||||
return result.items;
|
return result.items;
|
||||||
@@ -106,7 +108,7 @@ export function createActivitiesService(pb: PocketBase) {
|
|||||||
|
|
||||||
async getActivitiesByFunction(functionName: string, limit: number = 50): Promise<Activity[]> {
|
async getActivitiesByFunction(functionName: string, limit: number = 50): Promise<Activity[]> {
|
||||||
const result = await pb.collection("activities").getList<Activity>(1, limit, {
|
const result = await pb.collection("activities").getList<Activity>(1, limit, {
|
||||||
filter: `name = "${functionName}"`,
|
filter: pbFilter(pb, "name = {:functionName}", { functionName }),
|
||||||
sort: "-created",
|
sort: "-created",
|
||||||
});
|
});
|
||||||
return result.items;
|
return result.items;
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import PocketBase from "pocketbase";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { pbFilter } from "./filter";
|
||||||
|
|
||||||
|
// Real SDK, not a stub: the escaping under test lives in PocketBase's own
|
||||||
|
// replaceAll.
|
||||||
|
const pb = new PocketBase("http://pocketbase.test");
|
||||||
|
|
||||||
|
const quotedLiteral = (expression: string) => {
|
||||||
|
const open = expression.indexOf("'");
|
||||||
|
let literal = "";
|
||||||
|
for (let i = open + 1; i < expression.length; i++) {
|
||||||
|
if (expression[i] === "\\") {
|
||||||
|
literal += expression[i + 1];
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (expression[i] === "'") return literal;
|
||||||
|
literal += expression[i];
|
||||||
|
}
|
||||||
|
throw new Error(`unterminated literal in ${expression}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("pbFilter", () => {
|
||||||
|
it.each([
|
||||||
|
["a match-substitution pattern", "a$&b"],
|
||||||
|
["a preceding-input pattern", "a$`b"],
|
||||||
|
["a following-input pattern", "a$'b"],
|
||||||
|
["an escaped-dollar pattern", "a$$b"],
|
||||||
|
["a bare dollar", "a$b"],
|
||||||
|
["nothing but a dollar", "$"],
|
||||||
|
["a dollar beside a metacharacter", "50$%"],
|
||||||
|
])("substitutes %s as a literal", (_label, term) => {
|
||||||
|
const expression = pbFilter(pb, "first_name ~ {:query}", { query: term });
|
||||||
|
|
||||||
|
expect(quotedLiteral(expression)).toBe(term);
|
||||||
|
expect(expression).not.toContain("{:query}");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps both dollars of an escaped-dollar pattern", () => {
|
||||||
|
expect(pbFilter(pb, "f ~ {:q}", { q: "a$$b" })).toBe("f ~ 'a$$b'");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("splices no part of the surrounding expression into the literal", () => {
|
||||||
|
const expression = pbFilter(pb, "first_name ~ {:query}", {
|
||||||
|
query: "a$`b",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(expression).toBe("first_name ~ 'a$`b'");
|
||||||
|
expect(quotedLiteral(expression)).not.toContain("first_name");
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["a single quote", "o'brien"],
|
||||||
|
["a trailing backslash", "ada\\"],
|
||||||
|
["a percent sign", "50%"],
|
||||||
|
])("escapes %s exactly as the SDK does", (_label, term) => {
|
||||||
|
expect(pbFilter(pb, "f ~ {:q}", { q: term })).toBe(
|
||||||
|
pb.filter("f ~ {:q}", { q: term })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["a number", 42],
|
||||||
|
["a boolean", true],
|
||||||
|
["null", null],
|
||||||
|
["a date", new Date(Date.UTC(2024, 0, 2, 3, 4, 5, 678))],
|
||||||
|
])("renders %s identically to pb.filter", (_label, value) => {
|
||||||
|
expect(pbFilter(pb, "f = {:v}", { v: value })).toBe(
|
||||||
|
pb.filter("f = {:v}", { v: value })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("leaves a placeholder with no matching parameter verbatim", () => {
|
||||||
|
expect(pbFilter(pb, "a = {:missing}", {})).toBe("a = {:missing}");
|
||||||
|
expect(pbFilter(pb, "a = {:missing}", {})).toBe(
|
||||||
|
pb.filter("a = {:missing}", {})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["a hyphen", "user-id"],
|
||||||
|
["a dot", "meta.user"],
|
||||||
|
["a digit and underscore", "auth_id2"],
|
||||||
|
])("resolves a key containing %s the way the SDK does", (_label, key) => {
|
||||||
|
const raw = `a = {:${key}}`;
|
||||||
|
|
||||||
|
expect(pbFilter(pb, raw, { [key]: "v" })).toBe("a = 'v'");
|
||||||
|
expect(pbFilter(pb, raw, { [key]: "v" })).toBe(pb.filter(raw, { [key]: "v" }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not absorb another parameter's value into a literal", () => {
|
||||||
|
expect(
|
||||||
|
pbFilter(pb, "a = {:x} && b = {:y}", { x: "%{:y}%", y: "SECRET" })
|
||||||
|
).toBe("a = '%{:y}%' && b = 'SECRET'");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("substitutes every occurrence of a repeated placeholder", () => {
|
||||||
|
expect(
|
||||||
|
pbFilter(pb, "(first_name ~ {:q} || last_name ~ {:q})", { q: "%ada%" })
|
||||||
|
).toBe("(first_name ~ '%ada%' || last_name ~ '%ada%')");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pins the SDK behaviour the doubling in `quote` compensates for. If an upgrade
|
||||||
|
// fixes the expansion upstream, these fail rather than the doubling silently
|
||||||
|
// becoming a double-escape.
|
||||||
|
describe("the pocketbase SDK substitution this works around", () => {
|
||||||
|
it("still mis-expands a dollar pattern in a parameter value", () => {
|
||||||
|
expect(pb.filter("a ~ {:q}", { q: "x$&y" })).toContain("{:q}");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("still splices a later parameter into an earlier literal", () => {
|
||||||
|
expect(pb.filter("a = {:x} && b = {:y}", { x: "%{:y}%", y: "SECRET" })).toBe(
|
||||||
|
"a = '%'SECRET'%' && b = 'SECRET'"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import type PocketBase from "pocketbase";
|
||||||
|
|
||||||
|
export type FilterParam = string | number | boolean | Date | null;
|
||||||
|
|
||||||
|
// Any key the SDK's own `replaceAll("{:" + key + "}", …)` loop would substitute,
|
||||||
|
// so which keys resolve does not depend on the characters they are spelled with.
|
||||||
|
const PLACEHOLDER = /\{:([^}]+)\}/g;
|
||||||
|
|
||||||
|
// `pb.filter` substitutes with String.replaceAll and a *string* replacement, so
|
||||||
|
// `$&`, `` $` ``, `$'` and `$$` inside a value are expanded as replacement
|
||||||
|
// patterns: the value's own text, or a slice of the surrounding expression,
|
||||||
|
// gets spliced into the quoted literal. Doubling every `$` first collapses back
|
||||||
|
// to the exact literal inside that same replaceAll. The FilterParam union is
|
||||||
|
// load-bearing — it keeps objects and arrays out of the SDK's JSON.stringify
|
||||||
|
// branch, which would reintroduce an undoubled `$`.
|
||||||
|
const quote = (pb: PocketBase, value: FilterParam) =>
|
||||||
|
pb.filter("{:v}", {
|
||||||
|
v: typeof value === "string" ? value.replaceAll("$", () => "$$") : value,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const pbFilter = (
|
||||||
|
pb: PocketBase,
|
||||||
|
raw: string,
|
||||||
|
params: Record<string, FilterParam>
|
||||||
|
) =>
|
||||||
|
raw.replace(PLACEHOLDER, (token, key: string) =>
|
||||||
|
Object.hasOwn(params, key) ? quote(pb, params[key]) : token
|
||||||
|
);
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import PocketBase from "pocketbase";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { literalTerminates } from "@/test/pb-filter";
|
||||||
|
import { pbFilter } from "./filter";
|
||||||
|
import { likePattern } from "./like-pattern";
|
||||||
|
|
||||||
|
const compose = (term: string) =>
|
||||||
|
pbFilter(new PocketBase("http://pocketbase.test"), "first_name ~ {:query}", {
|
||||||
|
query: likePattern(term),
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("likePattern", () => {
|
||||||
|
it("wraps a plain term so ~ matches a substring", () => {
|
||||||
|
expect(likePattern("ada")).toBe("%ada%");
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["a percent sign", "50%", "%50\\%%"],
|
||||||
|
["an underscore", "_", "%\\_%"],
|
||||||
|
["a backslash", "ada\\", "%ada\\\\%"],
|
||||||
|
])("escapes %s so it matches literally", (_label, term, expected) => {
|
||||||
|
expect(likePattern(term)).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("always ends the operand with an unescaped wildcard", () => {
|
||||||
|
// An odd run of backslashes before the final % would mean the % is itself
|
||||||
|
// escaped, which is the shape that swallows the closing quote.
|
||||||
|
for (const term of ["ada", "ada\\", "\\", "50%", "_", "o'brien\\"]) {
|
||||||
|
const pattern = likePattern(term);
|
||||||
|
const trailingSlashes = /(\\*)%$/.exec(pattern)?.[1] ?? "";
|
||||||
|
|
||||||
|
expect(pattern.endsWith("%")).toBe(true);
|
||||||
|
expect(trailingSlashes.length % 2).toBe(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("likePattern composed through the real SDK", () => {
|
||||||
|
it.each(["ada\\", "\\", "a_b\\", "o'brien\\", "ada\\\\"])(
|
||||||
|
"keeps the filter expression parseable for %j",
|
||||||
|
(term) => {
|
||||||
|
expect(literalTerminates(compose(term))).toBe(true);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
it("detects the unterminated literal a raw term produces", () => {
|
||||||
|
const raw = new PocketBase("http://pocketbase.test").filter(
|
||||||
|
"first_name ~ {:query}",
|
||||||
|
{ query: "ada\\" }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(raw).toBe("first_name ~ 'ada\\'");
|
||||||
|
expect(literalTerminates(raw)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("leaves a term without metacharacters exactly as before", () => {
|
||||||
|
expect(compose("ada")).toBe("first_name ~ '%ada%'");
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(["a$&b", "a$`b", "50$%", "a$&_b", "$"])(
|
||||||
|
"carries %j through both escaping layers intact",
|
||||||
|
(term) => {
|
||||||
|
const composed = compose(term);
|
||||||
|
|
||||||
|
expect(composed).toContain(likePattern(term));
|
||||||
|
expect(literalTerminates(composed)).toBe(true);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
// `pb.filter()` escapes single quotes and nothing else, so a term ending in a
|
||||||
|
// backslash escapes the closing quote of the literal it is substituted into and
|
||||||
|
// PocketBase rejects the whole expression with 400 validation_invalid_filter.
|
||||||
|
// Escaping `\ % _` and appending the wildcards here keeps the operand's last
|
||||||
|
// character a literal `%`, and makes `~` an unconditional substring match:
|
||||||
|
// PocketBase only auto-wraps (and only auto-escapes) operands that contain no
|
||||||
|
// `%` of their own, so a term carrying one would otherwise silently become a
|
||||||
|
// prefix match, and a bare `_` would match every row.
|
||||||
|
export const likePattern = (term: string) =>
|
||||||
|
`%${term.replace(/[\\%_]/g, (char) => `\\${char}`)}%`;
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { redactValue, SENSITIVE_KEY } from "./redact";
|
||||||
|
|
||||||
|
// Shared scrubber for logs + audit rows - a regression leaks PII from both.
|
||||||
|
describe("redactValue", () => {
|
||||||
|
it("redacts sensitive keys and preserves benign ones", () => {
|
||||||
|
expect(
|
||||||
|
redactValue({ phone: "+17135550142", first_name: "Ada", token: "abc" })
|
||||||
|
).toEqual({ phone: "[redacted]", first_name: "Ada", token: "[redacted]" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("redacts recursively through nested objects and arrays", () => {
|
||||||
|
expect(
|
||||||
|
redactValue({
|
||||||
|
user: { first_name: "Ada", password: "hunter2" },
|
||||||
|
items: [{ authToken: "x" }, { label: "ok" }],
|
||||||
|
})
|
||||||
|
).toEqual({
|
||||||
|
user: { first_name: "Ada", password: "[redacted]" },
|
||||||
|
items: [{ authToken: "[redacted]" }, { label: "ok" }],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes primitives through untouched (a bare string is not assumed secret)", () => {
|
||||||
|
expect(redactValue("hello")).toBe("hello");
|
||||||
|
expect(redactValue(42)).toBe(42);
|
||||||
|
expect(redactValue(null)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("covers the documented sensitive keys", () => {
|
||||||
|
for (const key of ["token", "secret", "password", "phone", "otp", "code", "auth", "key"]) {
|
||||||
|
expect(SENSITIVE_KEY.test(key)).toBe(true);
|
||||||
|
}
|
||||||
|
expect(SENSITIVE_KEY.test("first_name")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// Shared redaction for logs + audit rows so the two never drift.
|
||||||
|
export const SENSITIVE_KEY = /token|secret|password|phone|otp|code|auth|key/i;
|
||||||
|
|
||||||
|
const REDACTED = "[redacted]";
|
||||||
|
|
||||||
|
// Deep-redact: keys matching SENSITIVE_KEY become "[redacted]"; primitives pass through.
|
||||||
|
export const redactValue = (value: unknown): unknown => {
|
||||||
|
if (Array.isArray(value)) return value.map(redactValue);
|
||||||
|
if (value && typeof value === "object") {
|
||||||
|
// Keep Error serializable.
|
||||||
|
if (value instanceof Error) {
|
||||||
|
return { name: value.name, message: value.message, stack: value.stack };
|
||||||
|
}
|
||||||
|
const out: Record<string, unknown> = {};
|
||||||
|
for (const [key, v] of Object.entries(value as Record<string, unknown>)) {
|
||||||
|
out[key] = SENSITIVE_KEY.test(key) ? REDACTED : redactValue(v);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { logger } from "../../logger";
|
import { logger } from "../../logger";
|
||||||
import { ErrorType, ServerError, ServerResult } from "../types";
|
import { ErrorType, ServerError, ServerResult } from "../types";
|
||||||
import { getRequest } from "@tanstack/react-start/server";
|
|
||||||
import { isRedirect } from "@tanstack/react-router";
|
import { isRedirect } from "@tanstack/react-router";
|
||||||
|
|
||||||
export const createServerError = (
|
export const createServerError = (
|
||||||
@@ -17,57 +16,20 @@ export const createServerError = (
|
|||||||
context,
|
context,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Audit rows are written by serverFnLoggingMiddleware, which reads the
|
||||||
|
// returned envelope's success flag — never write them here.
|
||||||
export const toServerResult = async <T>(
|
export const toServerResult = async <T>(
|
||||||
serverFn: () => Promise<T>
|
serverFn: () => Promise<T>
|
||||||
): Promise<ServerResult<T>> => {
|
): Promise<ServerResult<T>> => {
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await serverFn();
|
const data = await serverFn();
|
||||||
return { success: true, data };
|
return { success: true, data };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isRedirect(error) || error instanceof Response) throw error;
|
if (isRedirect(error) || error instanceof Response) throw error;
|
||||||
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
logger.error('Server Fn Error', error);
|
logger.error('Server Fn Error', error);
|
||||||
|
|
||||||
const mappedError = mapKnownError(error);
|
return { success: false, error: mapKnownError(error) };
|
||||||
|
|
||||||
let fnName = 'unknown';
|
|
||||||
try {
|
|
||||||
const request = getRequest();
|
|
||||||
const url = new URL(request.url);
|
|
||||||
|
|
||||||
const functionId = url.searchParams.get('_serverFnId') || url.pathname;
|
|
||||||
|
|
||||||
if (functionId.includes('--')) {
|
|
||||||
const match = functionId.match(/--([^_]+)_/);
|
|
||||||
fnName = match?.[1] || functionId.split('--')[1]?.split('_')[0] || 'unknown';
|
|
||||||
} else {
|
|
||||||
fnName = serverFn.name || 'unknown';
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
fnName = serverFn.name || 'unknown';
|
|
||||||
}
|
|
||||||
|
|
||||||
import("../../pocketbase/client")
|
|
||||||
.then(async ({ pbAdmin }) => {
|
|
||||||
await pbAdmin.authPromise;
|
|
||||||
await pbAdmin.createActivity({
|
|
||||||
name: fnName,
|
|
||||||
duration,
|
|
||||||
success: false,
|
|
||||||
error: mappedError.message,
|
|
||||||
arguments: {
|
|
||||||
errorType: mappedError.code,
|
|
||||||
statusCode: mappedError.statusCode,
|
|
||||||
userMessage: mappedError.userMessage,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
|
|
||||||
return { success: false, error: mappedError };
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// Mirrors PocketBase's own scan of a quoted literal: a backslash consumes the
|
||||||
|
// character after it, so an unterminated literal is exactly what a trailing
|
||||||
|
// backslash produces.
|
||||||
|
export const literalTerminates = (expression: string) => {
|
||||||
|
const open = expression.indexOf("'");
|
||||||
|
for (let i = open + 1; i < expression.length; i++) {
|
||||||
|
if (expression[i] === "\\") {
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (expression[i] === "'") return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
const h = vi.hoisted(() => ({
|
||||||
|
request: undefined as { url: string; headers: Headers } | undefined,
|
||||||
|
createActivity: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@tanstack/react-start/server", () => ({
|
||||||
|
getRequest: () => h.request,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/lib/pocketbase/client", () => ({
|
||||||
|
pbAdmin: { authPromise: Promise.resolve(), createActivity: h.createActivity },
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/lib/logger", () => ({
|
||||||
|
Logger: class {
|
||||||
|
error() {}
|
||||||
|
info() {}
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { recordDeniedServerFn, serverFnLoggingMiddleware } from "./activities";
|
||||||
|
|
||||||
|
type ServerHandler = (opts: {
|
||||||
|
next: () => Promise<unknown>;
|
||||||
|
data: unknown;
|
||||||
|
context: unknown;
|
||||||
|
serverFnMeta?: { id: string; name?: string; filename?: string };
|
||||||
|
}) => Promise<unknown>;
|
||||||
|
|
||||||
|
const runMiddleware = (opts: Parameters<ServerHandler>[0]) =>
|
||||||
|
(serverFnLoggingMiddleware as any).options.server(opts) as ReturnType<ServerHandler>;
|
||||||
|
|
||||||
|
const setRequest = (url: string, userAgent = "vitest") => {
|
||||||
|
h.request = { url, headers: new Headers({ "user-agent": userAgent }) };
|
||||||
|
};
|
||||||
|
|
||||||
|
const successEnvelope = { result: { success: true, data: {} } };
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
h.createActivity.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("serverFnLoggingMiddleware", () => {
|
||||||
|
it("records the source name from the compile-time meta, not the hashed url segment", async () => {
|
||||||
|
const hashedId = "a".repeat(64);
|
||||||
|
setRequest(`http://localhost:3000/_serverFn/${hashedId}`);
|
||||||
|
|
||||||
|
await runMiddleware({
|
||||||
|
next: async () => successEnvelope,
|
||||||
|
data: undefined,
|
||||||
|
context: { metadata: { player_id: "p1" } },
|
||||||
|
serverFnMeta: {
|
||||||
|
id: hashedId,
|
||||||
|
name: "updatePlayer",
|
||||||
|
filename: "src/features/players/server.ts",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await vi.waitFor(() => expect(h.createActivity).toHaveBeenCalledTimes(1));
|
||||||
|
expect(h.createActivity.mock.calls[0][0]).toMatchObject({
|
||||||
|
name: "updatePlayer",
|
||||||
|
player: "p1",
|
||||||
|
success: true,
|
||||||
|
user_agent: "vitest",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to the last path segment when the meta carries no name", async () => {
|
||||||
|
setRequest("http://localhost:3000/_serverFn/deadbeef");
|
||||||
|
|
||||||
|
await runMiddleware({
|
||||||
|
next: async () => successEnvelope,
|
||||||
|
data: undefined,
|
||||||
|
context: {},
|
||||||
|
serverFnMeta: { id: "deadbeef" },
|
||||||
|
});
|
||||||
|
|
||||||
|
await vi.waitFor(() => expect(h.createActivity).toHaveBeenCalledTimes(1));
|
||||||
|
expect(h.createActivity.mock.calls[0][0].name).toBe("deadbeef");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to the last path segment when there is no meta at all", async () => {
|
||||||
|
setRequest("http://localhost:3000/_serverFn/legacySegment");
|
||||||
|
|
||||||
|
await runMiddleware({
|
||||||
|
next: async () => successEnvelope,
|
||||||
|
data: undefined,
|
||||||
|
context: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
await vi.waitFor(() => expect(h.createActivity).toHaveBeenCalledTimes(1));
|
||||||
|
expect(h.createActivity.mock.calls[0][0].name).toBe("legacySegment");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("records no player when the session has no player_id", async () => {
|
||||||
|
setRequest("http://localhost:3000/_serverFn/doThing");
|
||||||
|
|
||||||
|
await runMiddleware({
|
||||||
|
next: async () => successEnvelope,
|
||||||
|
data: undefined,
|
||||||
|
context: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
await vi.waitFor(() => expect(h.createActivity).toHaveBeenCalledTimes(1));
|
||||||
|
expect(h.createActivity.mock.calls[0][0].player).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to 'unknown' when the path has no trailing segment", async () => {
|
||||||
|
setRequest("http://localhost:3000/");
|
||||||
|
|
||||||
|
await runMiddleware({
|
||||||
|
next: async () => successEnvelope,
|
||||||
|
data: undefined,
|
||||||
|
context: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
await vi.waitFor(() => expect(h.createActivity).toHaveBeenCalledTimes(1));
|
||||||
|
expect(h.createActivity.mock.calls[0][0].name).toBe("unknown");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("records success:false by reading the returned envelope, not by throwing", async () => {
|
||||||
|
setRequest("http://localhost:3000/_serverFn/doThing");
|
||||||
|
|
||||||
|
await runMiddleware({
|
||||||
|
next: async () => ({
|
||||||
|
result: {
|
||||||
|
success: false,
|
||||||
|
error: { code: "NOT_FOUND", userMessage: "nope" },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
data: undefined,
|
||||||
|
context: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
await vi.waitFor(() => expect(h.createActivity).toHaveBeenCalledTimes(1));
|
||||||
|
const row = h.createActivity.mock.calls[0][0];
|
||||||
|
expect(row.success).toBe(false);
|
||||||
|
expect(row.error).toContain("NOT_FOUND");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("records success:false and rethrows when the handler throws", async () => {
|
||||||
|
setRequest("http://localhost:3000/_serverFn/doThing");
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
runMiddleware({
|
||||||
|
next: async () => {
|
||||||
|
throw new Error("boom");
|
||||||
|
},
|
||||||
|
data: undefined,
|
||||||
|
context: {},
|
||||||
|
})
|
||||||
|
).rejects.toThrow("boom");
|
||||||
|
|
||||||
|
await vi.waitFor(() => expect(h.createActivity).toHaveBeenCalledTimes(1));
|
||||||
|
expect(h.createActivity.mock.calls[0][0]).toMatchObject({
|
||||||
|
success: false,
|
||||||
|
error: "boom",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("redacts sensitive argument keys before they reach the audit row", async () => {
|
||||||
|
setRequest("http://localhost:3000/_serverFn/login");
|
||||||
|
|
||||||
|
await runMiddleware({
|
||||||
|
next: async () => successEnvelope,
|
||||||
|
data: { phone: "+17135550142", first_name: "Ada" },
|
||||||
|
context: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
await vi.waitFor(() => expect(h.createActivity).toHaveBeenCalledTimes(1));
|
||||||
|
expect(h.createActivity.mock.calls[0][0].arguments).toEqual({
|
||||||
|
phone: "[redacted]",
|
||||||
|
first_name: "Ada",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("truncates oversized arguments", async () => {
|
||||||
|
setRequest("http://localhost:3000/_serverFn/doThing");
|
||||||
|
|
||||||
|
await runMiddleware({
|
||||||
|
next: async () => successEnvelope,
|
||||||
|
data: { blob: "x".repeat(5000) },
|
||||||
|
context: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
await vi.waitFor(() => expect(h.createActivity).toHaveBeenCalledTimes(1));
|
||||||
|
const args = h.createActivity.mock.calls[0][0].arguments;
|
||||||
|
expect(args.truncated).toBeDefined();
|
||||||
|
expect(args.truncated.length).toBeLessThanOrEqual(2048);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("recordDeniedServerFn", () => {
|
||||||
|
const deniedRequest = (url: string) =>
|
||||||
|
({ url, headers: new Headers({ "user-agent": "vitest" }) }) as Request;
|
||||||
|
|
||||||
|
it("names the denial row from the meta so it matches the success rows", async () => {
|
||||||
|
recordDeniedServerFn(
|
||||||
|
deniedRequest(`http://localhost:3000/_serverFn/${"b".repeat(64)}`),
|
||||||
|
{ player_id: "p1" },
|
||||||
|
{ name: "createTournament" }
|
||||||
|
);
|
||||||
|
|
||||||
|
await vi.waitFor(() => expect(h.createActivity).toHaveBeenCalledTimes(1));
|
||||||
|
expect(h.createActivity.mock.calls[0][0]).toMatchObject({
|
||||||
|
name: "createTournament",
|
||||||
|
player: "p1",
|
||||||
|
success: false,
|
||||||
|
error: "FORBIDDEN: Access denied",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("still records a name when the meta is absent", async () => {
|
||||||
|
recordDeniedServerFn(
|
||||||
|
deniedRequest("http://localhost:3000/_serverFn/rawSegment")
|
||||||
|
);
|
||||||
|
|
||||||
|
await vi.waitFor(() => expect(h.createActivity).toHaveBeenCalledTimes(1));
|
||||||
|
expect(h.createActivity.mock.calls[0][0]).toMatchObject({
|
||||||
|
name: "rawSegment",
|
||||||
|
success: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
+79
-13
@@ -1,6 +1,7 @@
|
|||||||
import { createMiddleware } from "@tanstack/react-start";
|
import { createMiddleware } from "@tanstack/react-start";
|
||||||
import { getRequest } from "@tanstack/react-start/server";
|
import { getRequest } from "@tanstack/react-start/server";
|
||||||
import { Logger } from "@/lib/logger";
|
import { Logger } from "@/lib/logger";
|
||||||
|
import { redactValue } from "@/lib/redact";
|
||||||
import type { ActivityInput } from "@/lib/pocketbase/services/activities";
|
import type { ActivityInput } from "@/lib/pocketbase/services/activities";
|
||||||
|
|
||||||
const logger = new Logger("Activities");
|
const logger = new Logger("Activities");
|
||||||
@@ -16,16 +17,67 @@ const recordActivity = (activity: ActivityInput) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MAX_ARGUMENTS_CHARS = 2048;
|
||||||
|
|
||||||
|
const redactArguments = (data: unknown): unknown => {
|
||||||
|
if (data === undefined) return undefined;
|
||||||
|
|
||||||
|
const redacted = redactValue(data);
|
||||||
|
const serialized = JSON.stringify(redacted) ?? "";
|
||||||
|
if (serialized.length > MAX_ARGUMENTS_CHARS) {
|
||||||
|
return { truncated: serialized.slice(0, MAX_ARGUMENTS_CHARS) };
|
||||||
|
}
|
||||||
|
return redacted;
|
||||||
|
};
|
||||||
|
|
||||||
|
const auditErrorMessage = (
|
||||||
|
error?: { code?: string; userMessage?: string }
|
||||||
|
): string => {
|
||||||
|
if (!error) return "error";
|
||||||
|
return [error.code, error.userMessage].filter(Boolean).join(": ") || "error";
|
||||||
|
};
|
||||||
|
|
||||||
|
type Actor = { player_id?: string };
|
||||||
|
|
||||||
|
type ServerFnMeta = { name?: string };
|
||||||
|
|
||||||
|
// Start's default generateFunctionId hashes the entry id, so the URL segment is
|
||||||
|
// a sha256 and the compile-time meta is the only readable name. The path stays
|
||||||
|
// as the fallback for requests that carry no meta.
|
||||||
|
const serverFnName = (request: Request, meta?: ServerFnMeta): string => {
|
||||||
|
if (meta?.name) return meta.name;
|
||||||
|
const pathParts = new URL(request.url).pathname.split("/").filter(Boolean);
|
||||||
|
return pathParts[pathParts.length - 1] || "unknown";
|
||||||
|
};
|
||||||
|
|
||||||
|
// The admin middleware refuses before serverFnLoggingMiddleware ever runs, so
|
||||||
|
// a denial has to write its own row — otherwise an operator reviewing the log
|
||||||
|
// during a probing attempt sees a clean history. The rejected payload is left
|
||||||
|
// out: it never reached a validator, so it is unbounded attacker input.
|
||||||
|
export const recordDeniedServerFn = (
|
||||||
|
request: Request,
|
||||||
|
actor?: Actor,
|
||||||
|
meta?: ServerFnMeta
|
||||||
|
) => {
|
||||||
|
recordActivity({
|
||||||
|
name: serverFnName(request, meta),
|
||||||
|
player: actor?.player_id,
|
||||||
|
duration: 0,
|
||||||
|
success: false,
|
||||||
|
error: "FORBIDDEN: Access denied",
|
||||||
|
user_agent: request.headers.get("user-agent") || undefined,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const serverFnLoggingMiddleware = createMiddleware({
|
export const serverFnLoggingMiddleware = createMiddleware({
|
||||||
type: "function",
|
type: "function",
|
||||||
}).server(async ({ next, data, context }) => {
|
}).server(async ({ next, data, context, serverFnMeta }) => {
|
||||||
const request = getRequest();
|
const request = getRequest();
|
||||||
|
|
||||||
const url = new URL(request.url);
|
const name = serverFnName(request, serverFnMeta);
|
||||||
const pathParts = url.pathname.split('/').filter(Boolean);
|
const playerId = (context as any)?.metadata?.player_id as string | undefined;
|
||||||
const serverFnName = pathParts[pathParts.length - 1] || 'unknown';
|
const userAgent = request.headers.get("user-agent") || undefined;
|
||||||
const userId = (context as any)?.metadata?.player_id || 'unknown';
|
const safeArgs = redactArguments(data);
|
||||||
const userAgent = request.headers.get('user-agent') || undefined;
|
|
||||||
|
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
|
||||||
@@ -33,12 +85,26 @@ export const serverFnLoggingMiddleware = createMiddleware({
|
|||||||
const result = await next();
|
const result = await next();
|
||||||
const duration = Date.now() - startTime;
|
const duration = Date.now() - startTime;
|
||||||
|
|
||||||
|
// Single audit writer; toServerResult resolves { success:false } instead of throwing, so read the flag.
|
||||||
|
const envelope = (result as { result?: unknown })?.result;
|
||||||
|
const failed =
|
||||||
|
!!envelope &&
|
||||||
|
typeof envelope === "object" &&
|
||||||
|
"success" in envelope &&
|
||||||
|
(envelope as { success: boolean }).success === false;
|
||||||
|
|
||||||
recordActivity({
|
recordActivity({
|
||||||
name: serverFnName,
|
name,
|
||||||
player: userId !== 'unknown' ? userId : undefined,
|
player: playerId,
|
||||||
duration,
|
duration,
|
||||||
success: true,
|
success: !failed,
|
||||||
arguments: data,
|
error: failed
|
||||||
|
? auditErrorMessage(
|
||||||
|
(envelope as { error?: { code?: string; userMessage?: string } })
|
||||||
|
.error
|
||||||
|
)
|
||||||
|
: undefined,
|
||||||
|
arguments: safeArgs,
|
||||||
user_agent: userAgent,
|
user_agent: userAgent,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -48,12 +114,12 @@ export const serverFnLoggingMiddleware = createMiddleware({
|
|||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
|
||||||
recordActivity({
|
recordActivity({
|
||||||
name: serverFnName,
|
name,
|
||||||
player: userId !== 'unknown' ? userId : undefined,
|
player: playerId,
|
||||||
duration,
|
duration,
|
||||||
success: false,
|
success: false,
|
||||||
error: errorMessage,
|
error: errorMessage,
|
||||||
arguments: data,
|
arguments: safeArgs,
|
||||||
user_agent: userAgent,
|
user_agent: userAgent,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export const superTokensFunctionMiddleware = createMiddleware({
|
|||||||
|
|
||||||
export const superTokensAdminFunctionMiddleware = createMiddleware({
|
export const superTokensAdminFunctionMiddleware = createMiddleware({
|
||||||
type: "function",
|
type: "function",
|
||||||
}).server(async ({ next }) => {
|
}).server(async ({ next, serverFnMeta }) => {
|
||||||
const request = getRequest();
|
const request = getRequest();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -62,7 +62,13 @@ export const superTokensAdminFunctionMiddleware = createMiddleware({
|
|||||||
return next({ context });
|
return next({ context });
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.error("Unauthorized user in admin function.", context);
|
// Identifiers only — the full context carries phone + metadata.
|
||||||
|
logger.error("Unauthorized user in admin function.", {
|
||||||
|
userAuthId: context.userAuthId,
|
||||||
|
roles: context.roles,
|
||||||
|
});
|
||||||
|
const { recordDeniedServerFn } = await import("./activities");
|
||||||
|
recordDeniedServerFn(request, context.metadata, serverFnMeta);
|
||||||
throw new Error("Unauthorized");
|
throw new Error("Unauthorized");
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (error.message === "SESSION_REFRESH_REQUIRED") {
|
if (error.message === "SESSION_REFRESH_REQUIRED") {
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { defineConfig } from 'vitest/config'
|
||||||
|
import tsConfigPaths from 'vite-tsconfig-paths'
|
||||||
|
|
||||||
|
// Excludes the TanStack Start plugin: its SSR/router codegen isn't needed for unit tests.
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [tsConfigPaths({ projects: ['./tsconfig.json'] })],
|
||||||
|
test: {
|
||||||
|
environment: 'node',
|
||||||
|
include: ['src/**/*.test.{ts,tsx}'],
|
||||||
|
restoreMocks: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user