Merge branch 'deep-ketch' into development
CI/CD Pipeline / Build and Push PocketBase Docker Image (push) Successful in 27s
CI/CD Pipeline / i18n Catalog Check (push) Successful in 41s
CI/CD Pipeline / Build and Push App Docker Image (push) Successful in 3m30s
CI/CD Pipeline / Deploy to Kubernetes (push) Failing after 22s
CI/CD Pipeline / Build and Push PocketBase Docker Image (push) Successful in 27s
CI/CD Pipeline / i18n Catalog Check (push) Successful in 41s
CI/CD Pipeline / Build and Push App Docker Image (push) Successful in 3m30s
CI/CD Pipeline / Deploy to Kubernetes (push) Failing after 22s
This commit is contained in:
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,13 @@ import { ColorSchemeScript, mantineHtmlProps } from "@mantine/core";
|
|||||||
import { HeaderConfig } from "@/features/core/types/header-config";
|
import { HeaderConfig } from "@/features/core/types/header-config";
|
||||||
import { playerQueries } from "@/features/players/queries";
|
import { playerQueries } from "@/features/players/queries";
|
||||||
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
|
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
|
||||||
import { DEFAULT_LOCALE, ensureMessages, resolveLocale } from "@/lib/i18n";
|
import {
|
||||||
|
DEFAULT_LOCALE,
|
||||||
|
ensureMessages,
|
||||||
|
getCachedMessages,
|
||||||
|
i18nMessagesQueryKey,
|
||||||
|
resolveLocale,
|
||||||
|
} from "@/lib/i18n";
|
||||||
import { getRootMeta } from "@/lib/i18n/meta";
|
import { getRootMeta } from "@/lib/i18n/meta";
|
||||||
import FullScreenLoader from "@/components/full-screen-loader";
|
import FullScreenLoader from "@/components/full-screen-loader";
|
||||||
import { CHROME_COLORS } from "@/lib/mantine/theme-colors";
|
import { CHROME_COLORS } from "@/lib/mantine/theme-colors";
|
||||||
@@ -132,7 +138,14 @@ export const Route = createRootRouteWithContext<{
|
|||||||
context.queryClient,
|
context.queryClient,
|
||||||
playerQueries.auth()
|
playerQueries.auth()
|
||||||
);
|
);
|
||||||
await ensureMessages(resolveLocale(auth?.metadata?.locale));
|
const locale = resolveLocale(auth?.metadata?.locale);
|
||||||
|
await ensureMessages(locale);
|
||||||
|
if (typeof window === "undefined" && locale !== DEFAULT_LOCALE) {
|
||||||
|
context.queryClient.setQueryData(
|
||||||
|
i18nMessagesQueryKey(locale),
|
||||||
|
getCachedMessages(locale)
|
||||||
|
);
|
||||||
|
}
|
||||||
return { auth };
|
return { auth };
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (isRedirect(error) || error instanceof Response) throw error;
|
if (isRedirect(error) || error instanceof Response) throw error;
|
||||||
|
|||||||
@@ -2,9 +2,15 @@ import { useEffect, useState } from 'react'
|
|||||||
import { Box, Paper, Group, Text, ActionIcon } from '@mantine/core'
|
import { Box, Paper, Group, Text, ActionIcon } from '@mantine/core'
|
||||||
import { DownloadIcon, XIcon } from '@phosphor-icons/react'
|
import { DownloadIcon, XIcon } from '@phosphor-icons/react'
|
||||||
import { Trans, useLingui } from '@lingui/react/macro'
|
import { Trans, useLingui } from '@lingui/react/macro'
|
||||||
|
import { useAuth } from '@/contexts/auth-context'
|
||||||
|
import { useIsMobile } from '@/hooks/use-is-mobile'
|
||||||
|
|
||||||
|
const ABOVE_MOBILE_NAV_OFFSET = 'calc(4.5rem + env(safe-area-inset-bottom, 0px))'
|
||||||
|
|
||||||
export function IOSInstallPrompt() {
|
export function IOSInstallPrompt() {
|
||||||
const { t } = useLingui()
|
const { t } = useLingui()
|
||||||
|
const { user } = useAuth()
|
||||||
|
const isMobile = useIsMobile()
|
||||||
const [show, setShow] = useState(false)
|
const [show, setShow] = useState(false)
|
||||||
const [platform, setPlatform] = useState<'ios' | 'android' | null>(null)
|
const [platform, setPlatform] = useState<'ios' | 'android' | null>(null)
|
||||||
|
|
||||||
@@ -36,22 +42,27 @@ export function IOSInstallPrompt() {
|
|||||||
? t`Tap Share → Add to Home Screen`
|
? t`Tap Share → Add to Home Screen`
|
||||||
: t`Tap Menu (⋮) → Add to Home screen`
|
: t`Tap Menu (⋮) → Add to Home screen`
|
||||||
|
|
||||||
|
const aboveBottomNav = Boolean(user) && isMobile
|
||||||
|
const bottomStyle = aboveBottomNav
|
||||||
|
? { bottom: ABOVE_MOBILE_NAV_OFFSET, paddingBottom: '8px' }
|
||||||
|
: { bottom: 0, paddingBottom: 'calc(8px + env(safe-area-inset-bottom, 0px))' }
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box style={{ position: 'fixed', bottom: 0, left: 0, right: 0, zIndex: 1000, padding: '8px' }}>
|
<Box style={{ position: 'fixed', left: 0, right: 0, zIndex: 1000, padding: '8px', ...bottomStyle }}>
|
||||||
<Paper shadow="lg" p="sm" style={{ background: 'var(--mantine-color-blue-9)', color: 'white' }}>
|
<Paper shadow="lg" p="sm" style={{ background: 'var(--mantine-color-blue-9)', color: 'white' }}>
|
||||||
<Group justify="space-between" wrap="nowrap">
|
<Group justify="space-between" wrap="nowrap">
|
||||||
<Group gap="xs" wrap="nowrap" style={{ flex: 1 }}>
|
<Group gap="xs" wrap="nowrap" style={{ flex: 1 }}>
|
||||||
<DownloadIcon size={20} style={{ flexShrink: 0 }} />
|
<DownloadIcon size={20} style={{ flexShrink: 0 }} />
|
||||||
<Box style={{ flex: 1, minWidth: 0 }}>
|
<Box style={{ flex: 1, minWidth: 0 }}>
|
||||||
<Text size="sm" fw={500} style={{ lineHeight: 1.3 }}>
|
<Text size="sm" fw={500} style={{ lineHeight: 1.3 }}>
|
||||||
<Trans>Please install FLXN • This will save me Twilio credits as you won't be signed out!</Trans>
|
<Trans>Add FLXN to your home screen for a smoother experience</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="xs" opacity={0.9} style={{ lineHeight: 1.2 }}>
|
<Text size="xs" opacity={0.9} style={{ lineHeight: 1.2 }}>
|
||||||
{instructions}
|
{instructions}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Group>
|
</Group>
|
||||||
<ActionIcon variant="subtle" color="white" onClick={handleDismiss}>
|
<ActionIcon variant="subtle" color="white" onClick={handleDismiss} aria-label={t`Dismiss`}>
|
||||||
<XIcon size={18} />
|
<XIcon size={18} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Group>
|
</Group>
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -14,7 +15,7 @@ export const getPlayerBadges = createServerFn()
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const migrateBadgeProgress = createServerFn()
|
export const migrateBadgeProgress = createServerFn()
|
||||||
.middleware([superTokensAdminFunctionMiddleware])
|
.middleware([superTokensAdminFunctionMiddleware, serverFnLoggingMiddleware])
|
||||||
.handler(async () =>
|
.handler(async () =>
|
||||||
toServerResult(async () => startBadgeMigration())
|
toServerResult(async () => startBadgeMigration())
|
||||||
);
|
);
|
||||||
@@ -40,7 +41,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);
|
||||||
|
|||||||
@@ -1,15 +1,57 @@
|
|||||||
|
import { useLayoutEffect, useRef } from "react";
|
||||||
import { Title, AppShell, Flex } from "@mantine/core";
|
import { Title, AppShell, Flex } from "@mantine/core";
|
||||||
import { useLingui } from "@lingui/react";
|
import { useLingui } from "@lingui/react";
|
||||||
import { HeaderConfig } from "../types/header-config";
|
import { HeaderConfig } from "../types/header-config";
|
||||||
import BackButton from "./back-button";
|
import BackButton from "./back-button";
|
||||||
|
|
||||||
|
const MIN_TITLE_SCALE = 0.55;
|
||||||
|
|
||||||
const Header = ({ collapsed, title, titleValues, withBackButton }: HeaderConfig) => {
|
const Header = ({ collapsed, title, titleValues, withBackButton }: HeaderConfig) => {
|
||||||
const { i18n } = useLingui();
|
const { i18n } = useLingui();
|
||||||
|
const titleRef = useRef<HTMLHeadingElement>(null);
|
||||||
const resolvedTitle =
|
const resolvedTitle =
|
||||||
typeof title === "string" || title === undefined
|
typeof title === "string" || title === undefined
|
||||||
? title
|
? title
|
||||||
: i18n._({ ...title, values: titleValues });
|
: i18n._({ ...title, values: titleValues });
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const el = titleRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
const textWidth = () => {
|
||||||
|
const range = document.createRange();
|
||||||
|
range.selectNodeContents(el);
|
||||||
|
return range.getBoundingClientRect().width;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fit = () => {
|
||||||
|
el.style.fontSize = "";
|
||||||
|
const available = el.clientWidth - 1;
|
||||||
|
if (available <= 0) return;
|
||||||
|
let needed = textWidth();
|
||||||
|
if (needed <= available) return;
|
||||||
|
const natural = parseFloat(getComputedStyle(el).fontSize);
|
||||||
|
let scale = Math.max(available / needed, MIN_TITLE_SCALE);
|
||||||
|
el.style.fontSize = `${natural * scale}px`;
|
||||||
|
for (let i = 0; i < 3 && scale > MIN_TITLE_SCALE && textWidth() > available; i++) {
|
||||||
|
scale = Math.max(scale * 0.98, MIN_TITLE_SCALE);
|
||||||
|
el.style.fontSize = `${natural * scale}px`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fit();
|
||||||
|
document.fonts?.ready.then(() => {
|
||||||
|
if (!cancelled) fit();
|
||||||
|
});
|
||||||
|
const observer = new ResizeObserver(fit);
|
||||||
|
observer.observe(el);
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
observer.disconnect();
|
||||||
|
};
|
||||||
|
}, [resolvedTitle]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppShell.Header
|
<AppShell.Header
|
||||||
id='app-header'
|
id='app-header'
|
||||||
@@ -20,8 +62,20 @@ const Header = ({ collapsed, title, titleValues, withBackButton }: HeaderConfig)
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{ withBackButton && <BackButton /> }
|
{ withBackButton && <BackButton /> }
|
||||||
<Flex justify='center' px='md' mt={8}>
|
<Flex justify='center' px={48} mt={8} w='100%'>
|
||||||
<Title order={1} lts='0.08em' style={{ userSelect: 'none' }}>
|
<Title
|
||||||
|
ref={titleRef}
|
||||||
|
order={1}
|
||||||
|
lts='0.08em'
|
||||||
|
style={{
|
||||||
|
userSelect: 'none',
|
||||||
|
width: '100%',
|
||||||
|
textAlign: 'center',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
}}
|
||||||
|
>
|
||||||
{resolvedTitle?.toLocaleUpperCase()}
|
{resolvedTitle?.toLocaleUpperCase()}
|
||||||
</Title>
|
</Title>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@@ -5,11 +5,10 @@ const eventListerOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const useVisualViewportSize = () => {
|
const useVisualViewportSize = () => {
|
||||||
const windowExists = typeof window !== 'undefined';
|
|
||||||
const [windowSize, setWindowSize] = useState({
|
const [windowSize, setWindowSize] = useState({
|
||||||
width: windowExists ? window.visualViewport?.width || 0 : 0,
|
width: 0,
|
||||||
height: windowExists ? window.visualViewport?.height || 0 : 0,
|
height: 0,
|
||||||
top: windowExists ? window.visualViewport?.offsetTop || 0 : 0,
|
top: 0,
|
||||||
});
|
});
|
||||||
const rafRef = useRef<number | null>(null);
|
const rafRef = useRef<number | null>(null);
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
+18
-3
@@ -32,6 +32,8 @@ export function resolveLocale(candidate: string | null | undefined): AppLocale {
|
|||||||
: DEFAULT_LOCALE;
|
: DEFAULT_LOCALE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const inflightLoads: Partial<Record<AppLocale, Promise<Messages>>> = {};
|
||||||
|
|
||||||
export async function ensureMessages(locale: AppLocale): Promise<Messages> {
|
export async function ensureMessages(locale: AppLocale): Promise<Messages> {
|
||||||
const cached = messageCache[locale];
|
const cached = messageCache[locale];
|
||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
@@ -39,11 +41,24 @@ export async function ensureMessages(locale: AppLocale): Promise<Messages> {
|
|||||||
const loader = catalogLoaders[`/src/locales/${locale}/messages.po`];
|
const loader = catalogLoaders[`/src/locales/${locale}/messages.po`];
|
||||||
if (!loader) return messageCache[DEFAULT_LOCALE]!;
|
if (!loader) return messageCache[DEFAULT_LOCALE]!;
|
||||||
|
|
||||||
const { messages } = await loader();
|
const pending = (inflightLoads[locale] ??= loader()
|
||||||
messageCache[locale] = messages;
|
.then(({ messages }) => {
|
||||||
return messages;
|
messageCache[locale] = messages;
|
||||||
|
return messages;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
delete inflightLoads[locale];
|
||||||
|
}));
|
||||||
|
return pending;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function primeMessages(locale: AppLocale, messages: Messages) {
|
||||||
|
messageCache[locale] ??= messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const i18nMessagesQueryKey = (locale: AppLocale) =>
|
||||||
|
["i18n-messages", locale] as const;
|
||||||
|
|
||||||
export function getCachedMessages(locale: AppLocale): Messages {
|
export function getCachedMessages(locale: AppLocale): Messages {
|
||||||
return messageCache[locale] ?? messageCache[DEFAULT_LOCALE]!;
|
return messageCache[locale] ?? messageCache[DEFAULT_LOCALE]!;
|
||||||
}
|
}
|
||||||
|
|||||||
+28
-10
@@ -1,11 +1,14 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { I18nProvider } from "@lingui/react";
|
import { I18nProvider } from "@lingui/react";
|
||||||
|
import type { Messages } from "@lingui/core";
|
||||||
import { playerQueries } from "@/features/players/queries";
|
import { playerQueries } from "@/features/players/queries";
|
||||||
import {
|
import {
|
||||||
createI18n,
|
createI18n,
|
||||||
ensureMessages,
|
ensureMessages,
|
||||||
hasCachedMessages,
|
hasCachedMessages,
|
||||||
|
i18nMessagesQueryKey,
|
||||||
|
primeMessages,
|
||||||
resolveLocale,
|
resolveLocale,
|
||||||
} from "@/lib/i18n";
|
} from "@/lib/i18n";
|
||||||
|
|
||||||
@@ -29,27 +32,42 @@ export const LinguiProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const locale = resolveLocale(data?.metadata?.locale);
|
const locale = resolveLocale(data?.metadata?.locale);
|
||||||
const [catalogVersion, setCatalogVersion] = useState(0);
|
const [catalogVersion, setCatalogVersion] = useState(0);
|
||||||
|
|
||||||
// Fresh instance per locale (and per SSR request tree). __root beforeLoad
|
|
||||||
// preloads the catalog on authed routes; on public routes (login) it may
|
|
||||||
// not be cached yet — createI18n falls back to en until the effect below
|
|
||||||
// loads it and bumps catalogVersion.
|
|
||||||
const i18n = useMemo(
|
const i18n = useMemo(
|
||||||
() => createI18n(locale),
|
() => {
|
||||||
|
if (!hasCachedMessages(locale)) {
|
||||||
|
const seeded = queryClient.getQueryData<Messages>(
|
||||||
|
i18nMessagesQueryKey(locale)
|
||||||
|
);
|
||||||
|
if (seeded) primeMessages(locale, seeded);
|
||||||
|
}
|
||||||
|
return createI18n(locale);
|
||||||
|
},
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
[locale, catalogVersion]
|
[locale, catalogVersion, queryClient]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasCachedMessages(locale)) return;
|
if (hasCachedMessages(locale)) return;
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
ensureMessages(locale).then(() => {
|
let retryTimer: ReturnType<typeof setTimeout> | undefined;
|
||||||
if (!cancelled) setCatalogVersion((v) => v + 1);
|
const load = () => {
|
||||||
});
|
ensureMessages(locale).then(
|
||||||
|
() => {
|
||||||
|
if (!cancelled) setCatalogVersion((v) => v + 1);
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
if (!cancelled) retryTimer = setTimeout(load, 3000);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
load();
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
|
if (retryTimer) clearTimeout(retryTimer);
|
||||||
};
|
};
|
||||||
}, [locale]);
|
}, [locale]);
|
||||||
|
|
||||||
|
|||||||
@@ -19,15 +19,30 @@ class PocketBaseAdminClient {
|
|||||||
this.pb = new PocketBase(process.env.POCKETBASE_URL);
|
this.pb = new PocketBase(process.env.POCKETBASE_URL);
|
||||||
|
|
||||||
this.pb.beforeSend = async (url, options) => {
|
this.pb.beforeSend = async (url, options) => {
|
||||||
await this.authPromise;
|
// Auth requests skip the gate: awaiting authPromise here would deadlock them.
|
||||||
|
if (!url.includes("/collections/_superusers/auth-")) {
|
||||||
if (this.pb.authStore.isValid && this.isTokenExpiringSoon()) {
|
|
||||||
try {
|
try {
|
||||||
await this.refreshAuth();
|
await this.authPromise;
|
||||||
} catch (error) {
|
} catch {}
|
||||||
console.error('Failed to refresh admin token, re-authenticating:', error);
|
|
||||||
await this.authenticate();
|
if (!this.pb.authStore.isValid) {
|
||||||
|
this.authPromise = this.authenticate();
|
||||||
|
await this.authPromise;
|
||||||
|
this.startTokenRefresh();
|
||||||
|
} else if (this.isTokenExpiringSoon()) {
|
||||||
|
try {
|
||||||
|
await this.refreshAuth();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to refresh admin token, re-authenticating:', error);
|
||||||
|
this.authPromise = this.authenticate();
|
||||||
|
await this.authPromise;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
options.headers = {
|
||||||
|
...options.headers,
|
||||||
|
Authorization: this.pb.authStore.token,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
options.cache = "no-store";
|
options.cache = "no-store";
|
||||||
@@ -54,9 +69,16 @@ class PocketBaseAdminClient {
|
|||||||
Object.assign(this, createPushService(this.pb));
|
Object.assign(this, createPushService(this.pb));
|
||||||
|
|
||||||
this.authPromise = this.authenticate();
|
this.authPromise = this.authenticate();
|
||||||
this.authPromise.then(() => {
|
this.authPromise
|
||||||
this.startTokenRefresh();
|
.then(() => {
|
||||||
});
|
this.startTokenRefresh();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
console.error(
|
||||||
|
"PocketBase admin authentication failed - is PocketBase running and are " +
|
||||||
|
"POCKETBASE_URL / POCKETBASE_ADMIN_EMAIL / POCKETBASE_ADMIN_PASSWORD set?"
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async authenticate() {
|
private async authenticate() {
|
||||||
@@ -98,6 +120,8 @@ class PocketBaseAdminClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private startTokenRefresh() {
|
private startTokenRefresh() {
|
||||||
|
if (this.refreshInterval) return;
|
||||||
|
|
||||||
this.refreshInterval = setInterval(async () => {
|
this.refreshInterval = setInterval(async () => {
|
||||||
try {
|
try {
|
||||||
await this.refreshAuth();
|
await this.refreshAuth();
|
||||||
|
|||||||
@@ -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,20 @@
|
|||||||
|
import type PocketBase from "pocketbase";
|
||||||
|
|
||||||
|
export type FilterParam = string | number | boolean | Date | null;
|
||||||
|
|
||||||
|
const PLACEHOLDER = /\{:([^}]+)\}/g;
|
||||||
|
|
||||||
|
// pb.filter $-expands string replacements; doubling `$` keeps the value literal.
|
||||||
|
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,3 @@
|
|||||||
|
// Escapes LIKE metacharacters and wraps in `%` for a literal substring match.
|
||||||
|
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,18 @@
|
|||||||
|
export const SENSITIVE_KEY = /token|secret|password|phone|otp|code|auth|key/i;
|
||||||
|
|
||||||
|
const REDACTED = "[redacted]";
|
||||||
|
|
||||||
|
export const redactValue = (value: unknown): unknown => {
|
||||||
|
if (Array.isArray(value)) return value.map(redactValue);
|
||||||
|
if (value && typeof value === "object") {
|
||||||
|
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 = (
|
||||||
@@ -20,54 +19,15 @@ export const createServerError = (
|
|||||||
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 };
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -392,6 +392,10 @@ msgstr "Aktivitäten"
|
|||||||
msgid "Activity Details"
|
msgid "Activity Details"
|
||||||
msgstr "Aktivitätsdetails"
|
msgstr "Aktivitätsdetails"
|
||||||
|
|
||||||
|
#: src/components/ios-install-prompt.tsx:58
|
||||||
|
msgid "Add FLXN to your home screen for a smoother experience"
|
||||||
|
msgstr "Leg FLXN auf deinen Homescreen für ein flüssigeres Erlebnis"
|
||||||
|
|
||||||
#: src/features/tournaments/components/edit-enrolled-players.tsx:117
|
#: src/features/tournaments/components/edit-enrolled-players.tsx:117
|
||||||
msgid "Add Player"
|
msgid "Add Player"
|
||||||
msgstr "Spieler hinzufügen"
|
msgstr "Spieler hinzufügen"
|
||||||
@@ -853,6 +857,10 @@ msgstr "deviceId ist für die Übertragungsaktion erforderlich"
|
|||||||
msgid "Disconnect Spotify"
|
msgid "Disconnect Spotify"
|
||||||
msgstr "Spotify trennen"
|
msgstr "Spotify trennen"
|
||||||
|
|
||||||
|
#: src/components/ios-install-prompt.tsx:65
|
||||||
|
msgid "Dismiss"
|
||||||
|
msgstr "Schließen"
|
||||||
|
|
||||||
#: src/features/bracket/components/match-report.tsx:159
|
#: src/features/bracket/components/match-report.tsx:159
|
||||||
msgid "Does this look right?"
|
msgid "Does this look right?"
|
||||||
msgstr "Sieht das richtig aus?"
|
msgstr "Sieht das richtig aus?"
|
||||||
@@ -1885,10 +1893,6 @@ msgstr "Spieler sind erforderlich"
|
|||||||
msgid "Playing on {0}"
|
msgid "Playing on {0}"
|
||||||
msgstr "Wird auf {0} abgespielt"
|
msgstr "Wird auf {0} abgespielt"
|
||||||
|
|
||||||
#: src/components/ios-install-prompt.tsx:47
|
|
||||||
msgid "Please install FLXN • This will save me Twilio credits as you won't be signed out!"
|
|
||||||
msgstr "Installier FLXN • Das spart mir Twilio-Credits, weil du nicht ausgeloggt wirst!"
|
|
||||||
|
|
||||||
#: src/features/tournaments/components/group-stage-view.tsx:314
|
#: src/features/tournaments/components/group-stage-view.tsx:314
|
||||||
msgid "Populate Knockout Bracket"
|
msgid "Populate Knockout Bracket"
|
||||||
msgstr "K.-o.-Runde befüllen"
|
msgstr "K.-o.-Runde befüllen"
|
||||||
@@ -2347,11 +2351,11 @@ msgstr "T"
|
|||||||
msgid "Tap an opponent below to compare"
|
msgid "Tap an opponent below to compare"
|
||||||
msgstr "Tippe unten auf einen Gegner zum Vergleichen"
|
msgstr "Tippe unten auf einen Gegner zum Vergleichen"
|
||||||
|
|
||||||
#: src/components/ios-install-prompt.tsx:37
|
#: src/components/ios-install-prompt.tsx:43
|
||||||
msgid "Tap Menu (⋮) → Add to Home screen"
|
msgid "Tap Menu (⋮) → Add to Home screen"
|
||||||
msgstr "Tippe auf Menü (⋮) → Zum Home-Bildschirm hinzufügen"
|
msgstr "Tippe auf Menü (⋮) → Zum Home-Bildschirm hinzufügen"
|
||||||
|
|
||||||
#: src/components/ios-install-prompt.tsx:36
|
#: src/components/ios-install-prompt.tsx:42
|
||||||
msgid "Tap Share → Add to Home Screen"
|
msgid "Tap Share → Add to Home Screen"
|
||||||
msgstr "Tippe auf Teilen → Zum Home-Bildschirm hinzufügen"
|
msgstr "Tippe auf Teilen → Zum Home-Bildschirm hinzufügen"
|
||||||
|
|
||||||
|
|||||||
@@ -392,6 +392,10 @@ msgstr "Activities"
|
|||||||
msgid "Activity Details"
|
msgid "Activity Details"
|
||||||
msgstr "Activity Details"
|
msgstr "Activity Details"
|
||||||
|
|
||||||
|
#: src/components/ios-install-prompt.tsx:58
|
||||||
|
msgid "Add FLXN to your home screen for a smoother experience"
|
||||||
|
msgstr "Add FLXN to your home screen for a smoother experience"
|
||||||
|
|
||||||
#: src/features/tournaments/components/edit-enrolled-players.tsx:117
|
#: src/features/tournaments/components/edit-enrolled-players.tsx:117
|
||||||
msgid "Add Player"
|
msgid "Add Player"
|
||||||
msgstr "Add Player"
|
msgstr "Add Player"
|
||||||
@@ -853,6 +857,10 @@ msgstr "deviceId is required for transfer action"
|
|||||||
msgid "Disconnect Spotify"
|
msgid "Disconnect Spotify"
|
||||||
msgstr "Disconnect Spotify"
|
msgstr "Disconnect Spotify"
|
||||||
|
|
||||||
|
#: src/components/ios-install-prompt.tsx:65
|
||||||
|
msgid "Dismiss"
|
||||||
|
msgstr "Dismiss"
|
||||||
|
|
||||||
#: src/features/bracket/components/match-report.tsx:159
|
#: src/features/bracket/components/match-report.tsx:159
|
||||||
msgid "Does this look right?"
|
msgid "Does this look right?"
|
||||||
msgstr "Does this look right?"
|
msgstr "Does this look right?"
|
||||||
@@ -1885,10 +1893,6 @@ msgstr "Players are required"
|
|||||||
msgid "Playing on {0}"
|
msgid "Playing on {0}"
|
||||||
msgstr "Playing on {0}"
|
msgstr "Playing on {0}"
|
||||||
|
|
||||||
#: src/components/ios-install-prompt.tsx:47
|
|
||||||
msgid "Please install FLXN • This will save me Twilio credits as you won't be signed out!"
|
|
||||||
msgstr "Please install FLXN • This will save me Twilio credits as you won't be signed out!"
|
|
||||||
|
|
||||||
#: src/features/tournaments/components/group-stage-view.tsx:314
|
#: src/features/tournaments/components/group-stage-view.tsx:314
|
||||||
msgid "Populate Knockout Bracket"
|
msgid "Populate Knockout Bracket"
|
||||||
msgstr "Populate Knockout Bracket"
|
msgstr "Populate Knockout Bracket"
|
||||||
@@ -2347,11 +2351,11 @@ msgstr "T"
|
|||||||
msgid "Tap an opponent below to compare"
|
msgid "Tap an opponent below to compare"
|
||||||
msgstr "Tap an opponent below to compare"
|
msgstr "Tap an opponent below to compare"
|
||||||
|
|
||||||
#: src/components/ios-install-prompt.tsx:37
|
#: src/components/ios-install-prompt.tsx:43
|
||||||
msgid "Tap Menu (⋮) → Add to Home screen"
|
msgid "Tap Menu (⋮) → Add to Home screen"
|
||||||
msgstr "Tap Menu (⋮) → Add to Home screen"
|
msgstr "Tap Menu (⋮) → Add to Home screen"
|
||||||
|
|
||||||
#: src/components/ios-install-prompt.tsx:36
|
#: src/components/ios-install-prompt.tsx:42
|
||||||
msgid "Tap Share → Add to Home Screen"
|
msgid "Tap Share → Add to Home Screen"
|
||||||
msgstr "Tap Share → Add to Home Screen"
|
msgstr "Tap Share → Add to Home Screen"
|
||||||
|
|
||||||
|
|||||||
@@ -392,6 +392,10 @@ msgstr "Actividades"
|
|||||||
msgid "Activity Details"
|
msgid "Activity Details"
|
||||||
msgstr "Detalles de la actividad"
|
msgstr "Detalles de la actividad"
|
||||||
|
|
||||||
|
#: src/components/ios-install-prompt.tsx:58
|
||||||
|
msgid "Add FLXN to your home screen for a smoother experience"
|
||||||
|
msgstr "Agrega FLXN a tu pantalla de inicio para una experiencia más fluida"
|
||||||
|
|
||||||
#: src/features/tournaments/components/edit-enrolled-players.tsx:117
|
#: src/features/tournaments/components/edit-enrolled-players.tsx:117
|
||||||
msgid "Add Player"
|
msgid "Add Player"
|
||||||
msgstr "Agregar Jugador"
|
msgstr "Agregar Jugador"
|
||||||
@@ -853,6 +857,10 @@ msgstr "Se requiere deviceId para la acción de transferencia"
|
|||||||
msgid "Disconnect Spotify"
|
msgid "Disconnect Spotify"
|
||||||
msgstr "Desconectar Spotify"
|
msgstr "Desconectar Spotify"
|
||||||
|
|
||||||
|
#: src/components/ios-install-prompt.tsx:65
|
||||||
|
msgid "Dismiss"
|
||||||
|
msgstr "Cerrar"
|
||||||
|
|
||||||
#: src/features/bracket/components/match-report.tsx:159
|
#: src/features/bracket/components/match-report.tsx:159
|
||||||
msgid "Does this look right?"
|
msgid "Does this look right?"
|
||||||
msgstr "¿Se ve bien así?"
|
msgstr "¿Se ve bien así?"
|
||||||
@@ -1885,10 +1893,6 @@ msgstr "Los jugadores son obligatorios"
|
|||||||
msgid "Playing on {0}"
|
msgid "Playing on {0}"
|
||||||
msgstr "Reproduciendo en {0}"
|
msgstr "Reproduciendo en {0}"
|
||||||
|
|
||||||
#: src/components/ios-install-prompt.tsx:47
|
|
||||||
msgid "Please install FLXN • This will save me Twilio credits as you won't be signed out!"
|
|
||||||
msgstr "Por favor instala FLXN • Así me ahorras créditos de Twilio porque no se cerrará tu sesión"
|
|
||||||
|
|
||||||
#: src/features/tournaments/components/group-stage-view.tsx:314
|
#: src/features/tournaments/components/group-stage-view.tsx:314
|
||||||
msgid "Populate Knockout Bracket"
|
msgid "Populate Knockout Bracket"
|
||||||
msgstr "Llenar el bracket eliminatorio"
|
msgstr "Llenar el bracket eliminatorio"
|
||||||
@@ -2347,11 +2351,11 @@ msgstr "T"
|
|||||||
msgid "Tap an opponent below to compare"
|
msgid "Tap an opponent below to compare"
|
||||||
msgstr "Toca un oponente abajo para comparar"
|
msgstr "Toca un oponente abajo para comparar"
|
||||||
|
|
||||||
#: src/components/ios-install-prompt.tsx:37
|
#: src/components/ios-install-prompt.tsx:43
|
||||||
msgid "Tap Menu (⋮) → Add to Home screen"
|
msgid "Tap Menu (⋮) → Add to Home screen"
|
||||||
msgstr "Toca Menú (⋮) → Agregar a pantalla de inicio"
|
msgstr "Toca Menú (⋮) → Agregar a pantalla de inicio"
|
||||||
|
|
||||||
#: src/components/ios-install-prompt.tsx:36
|
#: src/components/ios-install-prompt.tsx:42
|
||||||
msgid "Tap Share → Add to Home Screen"
|
msgid "Tap Share → Add to Home Screen"
|
||||||
msgstr "Toca Compartir → Agregar a pantalla de inicio"
|
msgstr "Toca Compartir → Agregar a pantalla de inicio"
|
||||||
|
|
||||||
|
|||||||
@@ -392,6 +392,10 @@ msgstr "アクティビティ"
|
|||||||
msgid "Activity Details"
|
msgid "Activity Details"
|
||||||
msgstr "アクティビティの詳細"
|
msgstr "アクティビティの詳細"
|
||||||
|
|
||||||
|
#: src/components/ios-install-prompt.tsx:58
|
||||||
|
msgid "Add FLXN to your home screen for a smoother experience"
|
||||||
|
msgstr "FLXNをホーム画面に追加すると、もっと快適に使えます"
|
||||||
|
|
||||||
#: src/features/tournaments/components/edit-enrolled-players.tsx:117
|
#: src/features/tournaments/components/edit-enrolled-players.tsx:117
|
||||||
msgid "Add Player"
|
msgid "Add Player"
|
||||||
msgstr "プレイヤーを追加"
|
msgstr "プレイヤーを追加"
|
||||||
@@ -853,6 +857,10 @@ msgstr "転送操作にはdeviceIdが必要です"
|
|||||||
msgid "Disconnect Spotify"
|
msgid "Disconnect Spotify"
|
||||||
msgstr "Spotifyの連携を解除"
|
msgstr "Spotifyの連携を解除"
|
||||||
|
|
||||||
|
#: src/components/ios-install-prompt.tsx:65
|
||||||
|
msgid "Dismiss"
|
||||||
|
msgstr "閉じる"
|
||||||
|
|
||||||
#: src/features/bracket/components/match-report.tsx:159
|
#: src/features/bracket/components/match-report.tsx:159
|
||||||
msgid "Does this look right?"
|
msgid "Does this look right?"
|
||||||
msgstr "これで合っていますか?"
|
msgstr "これで合っていますか?"
|
||||||
@@ -1885,10 +1893,6 @@ msgstr "プレイヤーの選択が必要です"
|
|||||||
msgid "Playing on {0}"
|
msgid "Playing on {0}"
|
||||||
msgstr "{0}で再生中"
|
msgstr "{0}で再生中"
|
||||||
|
|
||||||
#: src/components/ios-install-prompt.tsx:47
|
|
||||||
msgid "Please install FLXN • This will save me Twilio credits as you won't be signed out!"
|
|
||||||
msgstr "FLXNをインストールしてください • ログアウトされなくなるので、Twilioのクレジット節約になります!"
|
|
||||||
|
|
||||||
#: src/features/tournaments/components/group-stage-view.tsx:314
|
#: src/features/tournaments/components/group-stage-view.tsx:314
|
||||||
msgid "Populate Knockout Bracket"
|
msgid "Populate Knockout Bracket"
|
||||||
msgstr "ノックアウトブラケットを作成"
|
msgstr "ノックアウトブラケットを作成"
|
||||||
@@ -2347,11 +2351,11 @@ msgstr "T"
|
|||||||
msgid "Tap an opponent below to compare"
|
msgid "Tap an opponent below to compare"
|
||||||
msgstr "下の対戦相手をタップして比較"
|
msgstr "下の対戦相手をタップして比較"
|
||||||
|
|
||||||
#: src/components/ios-install-prompt.tsx:37
|
#: src/components/ios-install-prompt.tsx:43
|
||||||
msgid "Tap Menu (⋮) → Add to Home screen"
|
msgid "Tap Menu (⋮) → Add to Home screen"
|
||||||
msgstr "メニュー (⋮) をタップ →「ホーム画面に追加」"
|
msgstr "メニュー (⋮) をタップ →「ホーム画面に追加」"
|
||||||
|
|
||||||
#: src/components/ios-install-prompt.tsx:36
|
#: src/components/ios-install-prompt.tsx:42
|
||||||
msgid "Tap Share → Add to Home Screen"
|
msgid "Tap Share → Add to Home Screen"
|
||||||
msgstr "共有をタップ →「ホーム画面に追加」"
|
msgstr "共有をタップ →「ホーム画面に追加」"
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
+72
-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,61 @@ 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 };
|
||||||
|
|
||||||
|
// The URL segment is a hash of the fn id; serverFnMeta carries the readable name.
|
||||||
|
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";
|
||||||
|
};
|
||||||
|
|
||||||
|
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 +79,25 @@ export const serverFnLoggingMiddleware = createMiddleware({
|
|||||||
const result = await next();
|
const result = await next();
|
||||||
const duration = Date.now() - startTime;
|
const duration = Date.now() - startTime;
|
||||||
|
|
||||||
|
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 +107,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,12 @@ export const superTokensAdminFunctionMiddleware = createMiddleware({
|
|||||||
return next({ context });
|
return next({ context });
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.error("Unauthorized user in admin function.", context);
|
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