diff --git a/bun.lock b/bun.lock index a746737..229158f 100644 --- a/bun.lock +++ b/bun.lock @@ -6,7 +6,8 @@ "name": "tanstack-start-app", "dependencies": { "@clickhouse/click-ui": "0.2.0-rc.4", - "@librechat/data-schemas": "^0.0.53", + "@librechat/data-schemas": "^0.0.54", + "@radix-ui/react-dialog": "1.1.15", "@tailwindcss/vite": "^4.1.18", "@tanstack/react-devtools": "0.10.0", "@tanstack/react-query": "5.95.2", @@ -25,7 +26,7 @@ "i18next-browser-languagedetector": "^8.2.1", "input-otp": "^1.4.2", "js-yaml": "^4.1.1", - "librechat-data-provider": "^0.8.505", + "librechat-data-provider": "^0.8.507", "lucide-react": "^0.545.0", "prom-client": "^15.1.3", "react": "^19.2.0", @@ -293,7 +294,7 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "3.1.2", "@jridgewell/sourcemap-codec": "1.5.5" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - "@librechat/data-schemas": ["@librechat/data-schemas@0.0.53", "", { "peerDependencies": { "jsonwebtoken": "^9.0.2", "klona": "^2.0.6", "librechat-data-provider": "*", "lodash": "^4.17.23", "meilisearch": "^0.38.0", "mongoose": "^8.23.1", "nanoid": "^3.3.7", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" } }, "sha512-R3wcxa4wEd6xzlrh4WuJlMaN9G2hnrUTcRccHOnOnAJUjzhZwFS44+0GOT1/ReQD+SQVeRtQzU/IXuD5u8gRzg=="], + "@librechat/data-schemas": ["@librechat/data-schemas@0.0.54", "", { "peerDependencies": { "jsonwebtoken": "^9.0.2", "klona": "^2.0.6", "librechat-data-provider": "*", "lodash": "^4.17.23", "meilisearch": "^0.38.0", "mongoose": "^8.23.1", "nanoid": "^3.3.7", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" } }, "sha512-WOeDwKLKT+IopowF0GTw0djMMzWBsPjBPCDdCrrhJO+gzjqmRqWnWoPNDbkupMnzG5AijLzzit8PCYjnknCIRQ=="], "@mongodb-js/saslprep": ["@mongodb-js/saslprep@1.4.6", "", { "dependencies": { "sparse-bitfield": "^3.0.3" } }, "sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g=="], @@ -367,7 +368,7 @@ "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="], - "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A=="], + "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], @@ -1189,7 +1190,7 @@ "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], - "librechat-data-provider": ["librechat-data-provider@0.8.505", "", { "dependencies": { "axios": "^1.16.0", "dayjs": "^1.11.13", "js-yaml": "^4.1.1", "zod": "^3.22.4" }, "peerDependencies": { "@tanstack/react-query": "^4.28.0" } }, "sha512-Bcy7i7H1YR5lGNd44s6Rwf6+ceCUY0npJkDjwdOyUWW63DMRzSEHPWkNAofAyPns4TLXfrCTmSKFGddD7mDkMw=="], + "librechat-data-provider": ["librechat-data-provider@0.8.507", "", { "dependencies": { "axios": "^1.16.0", "dayjs": "^1.11.13", "js-yaml": "^4.2.0", "zod": "^3.22.4" }, "peerDependencies": { "@tanstack/react-query": "^4.28.0" } }, "sha512-1+IiAk9bkr6FioWG+EcbzYSUpsuYsen5MnPG919J67Gbhds4LjIku/0QjxcPubjqeyXJkVq0Mz7ZIGlF/5JgWA=="], "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], @@ -1689,12 +1690,12 @@ "@radix-ui/react-checkbox/@radix-ui/react-context": ["@radix-ui/react-context@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q=="], + "@radix-ui/react-checkbox/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A=="], + "@radix-ui/react-checkbox/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.0", "", { "dependencies": { "@radix-ui/react-slot": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw=="], "@radix-ui/react-checkbox/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.1.0", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw=="], - "@radix-ui/react-collapsible/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], - "@radix-ui/react-collapsible/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], "@radix-ui/react-context-menu/@radix-ui/primitive": ["@radix-ui/primitive@1.1.0", "", {}, "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA=="], @@ -1705,8 +1706,6 @@ "@radix-ui/react-context-menu/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.1.0", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw=="], - "@radix-ui/react-dialog/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], - "@radix-ui/react-dismissable-layer/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], "@radix-ui/react-dropdown-menu/@radix-ui/primitive": ["@radix-ui/primitive@1.1.0", "", {}, "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA=="], @@ -1733,6 +1732,8 @@ "@radix-ui/react-hover-card/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.2", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.0", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg=="], + "@radix-ui/react-hover-card/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A=="], + "@radix-ui/react-hover-card/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.0", "", { "dependencies": { "@radix-ui/react-slot": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw=="], "@radix-ui/react-hover-card/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.1.0", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw=="], @@ -1759,6 +1760,8 @@ "@radix-ui/react-menu/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.2", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.0", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg=="], + "@radix-ui/react-menu/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A=="], + "@radix-ui/react-menu/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.0", "", { "dependencies": { "@radix-ui/react-slot": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw=="], "@radix-ui/react-menu/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw=="], @@ -1781,6 +1784,8 @@ "@radix-ui/react-popover/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.2", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.0", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg=="], + "@radix-ui/react-popover/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A=="], + "@radix-ui/react-popover/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.0", "", { "dependencies": { "@radix-ui/react-slot": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw=="], "@radix-ui/react-popover/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw=="], @@ -1797,7 +1802,7 @@ "@radix-ui/react-portal/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], - "@radix-ui/react-presence/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw=="], + "@radix-ui/react-presence/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], "@radix-ui/react-radio-group/@radix-ui/primitive": ["@radix-ui/primitive@1.1.0", "", {}, "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA=="], @@ -1807,6 +1812,8 @@ "@radix-ui/react-radio-group/@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg=="], + "@radix-ui/react-radio-group/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A=="], + "@radix-ui/react-radio-group/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.0", "", { "dependencies": { "@radix-ui/react-slot": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw=="], "@radix-ui/react-radio-group/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.1.0", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw=="], @@ -1847,6 +1854,8 @@ "@radix-ui/react-tabs/@radix-ui/react-id": ["@radix-ui/react-id@1.1.0", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA=="], + "@radix-ui/react-tabs/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A=="], + "@radix-ui/react-tabs/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.0", "", { "dependencies": { "@radix-ui/react-slot": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw=="], "@radix-ui/react-tabs/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.1.0", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw=="], @@ -1861,6 +1870,8 @@ "@radix-ui/react-toast/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.2", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.0", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg=="], + "@radix-ui/react-toast/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A=="], + "@radix-ui/react-toast/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.0", "", { "dependencies": { "@radix-ui/react-slot": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw=="], "@radix-ui/react-toast/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.1.0", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw=="], @@ -1941,6 +1952,8 @@ "htmlparser2/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], + "librechat-data-provider/js-yaml": ["js-yaml@4.2.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw=="], + "librechat-data-provider/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "log-update/slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="], @@ -2009,8 +2022,6 @@ "@radix-ui/react-context-menu/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw=="], - "@radix-ui/react-dialog/@radix-ui/react-presence/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], - "@radix-ui/react-dropdown-menu/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw=="], "@radix-ui/react-hover-card/@radix-ui/react-popper/@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.0", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw=="], @@ -2041,6 +2052,8 @@ "@radix-ui/react-switch/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw=="], + "@radix-ui/react-tabs/@radix-ui/react-presence/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw=="], + "@radix-ui/react-tabs/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw=="], "@radix-ui/react-toast/@radix-ui/react-collection/@radix-ui/react-context": ["@radix-ui/react-context@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A=="], diff --git a/e2e/grants.spec.ts b/e2e/grants.spec.ts index 0c13bd0..3ed7cc4 100644 --- a/e2e/grants.spec.ts +++ b/e2e/grants.spec.ts @@ -148,7 +148,7 @@ test.describe('Grants page - Audit Log tab', () => { }); test('export button is present', async ({ page }) => { - const exportBtn = page.getByRole('button', { name: /export as csv/i }); + const exportBtn = page.getByRole('button', { name: /export all matching/i }); await expect(exportBtn).toBeVisible(); }); @@ -180,7 +180,7 @@ test.describe('Grants page - Tab switching', () => { await page.waitForTimeout(300); await expect(page).toHaveURL(/tab=audit-log/); - await expect(page.getByRole('button', { name: /export as csv/i })).toBeVisible(); + await expect(page.getByRole('button', { name: /export all matching/i })).toBeVisible(); const mgmtTab = page.getByRole('tab', { name: /management/i }); await mgmtTab.click(); diff --git a/package.json b/package.json index 7c4ca0b..d7e7e3f 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ }, "dependencies": { "@clickhouse/click-ui": "0.2.0-rc.4", - "@librechat/data-schemas": "^0.0.53", + "@librechat/data-schemas": "^0.0.54", + "@radix-ui/react-dialog": "1.1.15", "@tailwindcss/vite": "^4.1.18", "@tanstack/react-devtools": "0.10.0", "@tanstack/react-query": "5.95.2", @@ -41,7 +42,7 @@ "i18next-browser-languagedetector": "^8.2.1", "input-otp": "^1.4.2", "js-yaml": "^4.1.1", - "librechat-data-provider": "^0.8.505", + "librechat-data-provider": "^0.8.507", "lucide-react": "^0.545.0", "prom-client": "^15.1.3", "react": "^19.2.0", diff --git a/server.ts b/server.ts index d5457a5..40cd010 100644 --- a/server.ts +++ b/server.ts @@ -41,6 +41,43 @@ function getCacheHeaders(filePath: string): Record { return {}; } +// 'unsafe-inline' in style-src is required because Tailwind 4 + click-ui inject inline styles at runtime. +// TanStack Start's SSR injects an inline `` to +// boot the client. Without a nonce or 'unsafe-inline' for script-src, browsers will block hydration. +// Threading a per-request nonce through TanStack Start's manifest is non-trivial; until that wiring +// lands we ship the policy as report-only so it surfaces violations in dev tooling without breaking +// hydration in prod. Set ADMIN_PANEL_CSP_ENFORCE=true to switch back to enforcement (only safe once +// the nonce path is in place). +const CSP_VALUE = [ + "default-src 'self'", + "script-src 'self'", + "style-src 'self' 'unsafe-inline'", + "img-src 'self' data: blob:", + "font-src 'self' data:", + "connect-src 'self'", + "object-src 'none'", + "frame-ancestors 'none'", + "base-uri 'self'", + "form-action 'self'", +].join('; '); + +const CSP_ENFORCE = process.env.ADMIN_PANEL_CSP_ENFORCE === 'true'; +const CSP_HEADER_NAME = CSP_ENFORCE + ? 'Content-Security-Policy' + : 'Content-Security-Policy-Report-Only'; + +function applySecurityHeaders(headers: Headers): void { + const contentType = headers.get('Content-Type') ?? ''; + if (!contentType.toLowerCase().startsWith('text/html')) return; + headers.set(CSP_HEADER_NAME, CSP_VALUE); + headers.set('X-Content-Type-Options', 'nosniff'); + headers.set('Referrer-Policy', 'strict-origin-when-cross-origin'); + headers.set('X-Frame-Options', 'DENY'); + if (process.env.NODE_ENV === 'production') { + headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'); + } +} + type Handler = { default: { fetch: (req: Request) => Promise } }; const { default: handler } = (await import(SERVER_ENTRY.href)) as Handler; @@ -66,11 +103,11 @@ async function buildStaticRoutes(): Promise Pro const cache = getCacheHeaders(path); const routePath = `${BASE_PATH}/${path}`; routes[routePath] = (req) => - withHttpMetrics( - req, - routePath, - () => new Response(file, { headers: { 'Content-Type': file.type, ...cache } }), - ); + withHttpMetrics(req, routePath, () => { + const res = new Response(file, { headers: { 'Content-Type': file.type, ...cache } }); + applySecurityHeaders(res.headers); + return res; + }); } return routes; } @@ -92,6 +129,7 @@ const server = Bun.serve({ for (const [k, v] of Object.entries(NO_CACHE)) { patched.headers.set(k, v); } + applySecurityHeaders(patched.headers); return patched; }, }, diff --git a/src/components/grants/AuditLogDetailDrawer.tsx b/src/components/grants/AuditLogDetailDrawer.tsx new file mode 100644 index 0000000..9c42eea --- /dev/null +++ b/src/components/grants/AuditLogDetailDrawer.tsx @@ -0,0 +1,553 @@ +import * as Dialog from '@radix-ui/react-dialog'; +import { PrincipalType } from 'librechat-data-provider'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { Badge, Button, Icon, IconButton } from '@clickhouse/click-ui'; +import type { ReactElement } from 'react'; +import type * as t from '@/types'; +import { + ACTION_BADGE_STATE, + ACTION_LABEL_KEY, + auditCapability, + capabilityLabel, + formatTimestamp, +} from './auditLogUtils'; +import { LoadingState } from '@/components/shared'; +import { getScopeTypeConfig } from '@/constants'; +import { useLocalize } from '@/hooks'; +import { cn } from '@/utils'; + +interface AuditLogDetailDrawerProps { + entry: t.AuditLogEntryWithDiff | null; + open: boolean; + onClose: () => void; + /** Resolves to `true` when the clipboard write succeeded so the drawer only + * flips to its "Copied!" affordance after a real success. */ + onCopyPermalink: (entryId: string) => Promise; + /** Invoked when any inline `CopyableMono` (timestamp, actor/target/entry IDs, + * capability) fails to write to the clipboard. The caller is responsible for + * surfacing the failure — typically via the same screen-reader announcement + * used for the permalink-copy failure path so the drawer's copy controls are + * not silent on clipboard error. */ + onCopyFailed?: () => void; + /** Render a "no entry found" message instead of the detail body when the + * deep-linked id couldn't be located (e.g. the entry was purged). */ + notFound?: boolean; + /** Render a loading shell while the single-entry deep-link query is in flight + * for an entry that is not on the current page. Without this, the drawer + * would either flicker shut during the fetch or never appear on cold load. */ + loading?: boolean; + /** Render a load-error message + close affordance when the single-entry + * fetch failed for reasons other than not-found (network failure, 5xx). The + * caller is responsible for distinguishing this from `notFound` (which is + * the 404 case where the request itself succeeded but the entry is gone). */ + loadError?: boolean; +} + +function CopyableMono({ + value, + ariaLabel, + onCopyFailed, +}: { + value: string; + ariaLabel: string; + onCopyFailed?: () => void; +}): ReactElement { + const [copied, setCopied] = useState(false); + const timerRef = useRef | undefined>(undefined); + useEffect(() => { + return () => { + if (timerRef.current) clearTimeout(timerRef.current); + }; + }, []); + + const handleCopy = useCallback(async () => { + if (typeof navigator === 'undefined' || !navigator.clipboard) { + onCopyFailed?.(); + return; + } + try { + await navigator.clipboard.writeText(value); + } catch { + onCopyFailed?.(); + return; + } + setCopied(true); + if (timerRef.current) clearTimeout(timerRef.current); + timerRef.current = setTimeout(() => setCopied(false), 1500); + }, [value, onCopyFailed]); + + return ( + + ); +} + +function DiffList({ + items, + variant, + localize, +}: { + items: readonly string[]; + variant: 'added' | 'removed'; + localize: ReturnType; +}): ReactElement { + if (items.length === 0) { + return ( +

+ {localize('com_audit_detail_no_changes')} +

+ ); + } + const state = variant === 'added' ? 'success' : 'danger'; + return ( +
    + {items.map((cap) => ( +
  • + + {cap} +
  • + ))} +
+ ); +} + +export function AuditLogDetailDrawer({ + entry, + open, + onClose, + onCopyPermalink, + onCopyFailed, + notFound = false, + loading = false, + loadError = false, +}: AuditLogDetailDrawerProps): ReactElement | null { + const localize = useLocalize(); + + // Keep the last non-null entry so the close animation has content to render + // while Radix Dialog slides the panel out. Without this, unmounting on + // `entry === null` would cut off the data-state="closed" exit animation. + const [latestEntry, setLatestEntry] = useState(entry); + // Mirror the `latestEntry` pattern for the not-found path so the drawer can + // animate out: when the user closes the not-found drawer, `notFound` flips + // false in the same tick that `open` flips false. Without this latch the + // component would short-circuit to `return null` and skip the exit animation. + const [latestNotFound, setLatestNotFound] = useState(notFound); + // Same latch idea for the load-error branch — close should slide the error + // shell out rather than yank it. + const [latestLoadError, setLatestLoadError] = useState(loadError); + useEffect(() => { + if (entry) { + setLatestEntry(entry); + setLatestNotFound(false); + setLatestLoadError(false); + } else if (notFound) { + /** + * A permalink to a missing entry can arrive after the user has opened a + * real entry earlier in the session, leaving `latestEntry` stale. Clear + * it so the not-found branch — which is gated on `!latestEntry` — fires + * instead of falling through and showing the previous entry's content + * under a not-found URL. + */ + setLatestEntry(null); + setLatestNotFound(true); + setLatestLoadError(false); + } else if (loadError) { + /** Fetch failed non-404 — surface the error shell + drop any stale latches. */ + setLatestEntry(null); + setLatestNotFound(false); + setLatestLoadError(true); + } else if (loading) { + /** + * `entryId` switched to a new row while its fetch is in flight (e.g. + * the user clicks a different audit row before the previous one + * loaded). Without clearing the latches, the loading branch — gated on + * empty latches — would skip and the drawer would keep rendering the + * previous entry's content (or a stale not-found state) under the new + * `entryId`. + */ + setLatestEntry(null); + setLatestNotFound(false); + setLatestLoadError(false); + } + }, [entry, notFound, loading, loadError]); + + // Copied-feedback state for the permalink button. + const [copied, setCopied] = useState(false); + const copiedTimerRef = useRef | undefined>(undefined); + useEffect(() => { + return () => { + if (copiedTimerRef.current) clearTimeout(copiedTimerRef.current); + }; + }, []); + const handleCopyPermalinkClick = useCallback(async () => { + if (!entry) return; + const ok = await onCopyPermalink(entry.id); + if (!ok) return; + setCopied(true); + if (copiedTimerRef.current) clearTimeout(copiedTimerRef.current); + copiedTimerRef.current = setTimeout(() => setCopied(false), 1500); + }, [entry, onCopyPermalink]); + + /** + * Loading shell while the single-entry deep-link fetch is in flight for an + * entry that is not on the current page. Shows the same panel chrome (header + * + close button) as the not-found / regular branches so the drawer never + * appears to flicker shut during the fetch. + */ + if (loading && !latestEntry && !latestNotFound) { + return ( + { + if (!isOpen) onClose(); + }} + > + + + onClose()} + className={cn( + 'fixed top-0 right-0 z-(--z-overlay) flex h-full w-full flex-col bg-(--cui-color-background-panel) shadow-xl sm:w-120', + 'border-l border-(--cui-color-stroke-default)', + 'will-change-transform', + 'data-[state=closed]:animate-drawer-out data-[state=open]:animate-drawer-in', + )} + > + {localize('com_audit_detail_title')} +
+ + {localize('com_audit_detail_title')} + + +
+
+ +
+
+
+
+ ); + } + + if (latestLoadError && !latestEntry && !latestNotFound) { + return ( + { + if (!isOpen) onClose(); + }} + > + + + onClose()} + className={cn( + 'fixed top-0 right-0 z-(--z-overlay) flex h-full w-full flex-col bg-(--cui-color-background-panel) shadow-xl sm:w-120', + 'border-l border-(--cui-color-stroke-default)', + 'will-change-transform', + 'data-[state=closed]:animate-drawer-out data-[state=open]:animate-drawer-in', + )} + > + {localize('com_audit_detail_title')} +
+ + {localize('com_audit_detail_title')} + + +
+
+

+ {localize('com_audit_detail_load_error')} +

+
+
+
+
+
+
+ ); + } + + if (latestNotFound && !latestEntry) { + return ( + { + if (!isOpen) onClose(); + }} + > + + + onClose()} + className={cn( + 'fixed top-0 right-0 z-(--z-overlay) flex h-full w-full flex-col bg-(--cui-color-background-panel) shadow-xl sm:w-120', + 'border-l border-(--cui-color-stroke-default)', + 'will-change-transform', + 'data-[state=closed]:animate-drawer-out data-[state=open]:animate-drawer-in', + )} + > + {localize('com_audit_detail_title')} +
+ + {localize('com_audit_detail_title')} + + +
+
+

+ {localize('com_audit_detail_not_found')} +

+
+
+
+
+
+
+ ); + } + + if (!latestEntry) return null; + + const targetConfig = getScopeTypeConfig(latestEntry.target.type as PrincipalType); + const capability = auditCapability(latestEntry); + const targetLabel = latestEntry.target.name ?? latestEntry.target.id ?? ''; + const summaryKey = + latestEntry.action === 'grant.assigned' + ? 'com_audit_detail_summary_assigned' + : 'com_audit_detail_summary_removed'; + + const before = latestEntry.before ?? []; + const after = latestEntry.after ?? []; + const hasDiff = before.length > 0 || after.length > 0; + + return ( + { + if (!isOpen) onClose(); + }} + > + + + onClose()} + className={cn( + 'fixed top-0 right-0 z-(--z-overlay) flex h-full w-full flex-col bg-(--cui-color-background-panel) shadow-xl sm:w-120', + 'border-l border-(--cui-color-stroke-default)', + 'will-change-transform', + 'data-[state=closed]:animate-drawer-out data-[state=open]:animate-drawer-in', + )} + > + {localize('com_audit_detail_title')} +
+
+ + + {localize('com_audit_detail_title')} + +
+ +
+ +
+
+

+ {localize(summaryKey, { + actor: latestEntry.actor.name, + capability: capabilityLabel(capability, localize), + target: targetLabel, + })} +

+ +
+ +
+ + {formatTimestamp(latestEntry.timestamp)} + + +
+
+ + +
+ + {latestEntry.actor.name} + + +
+
+ + +
+ + + + {localize(targetConfig.labelKey)} + + } + /> + + {targetLabel} + + + +
+
+ + +
+ + {capabilityLabel(capability, localize)} + + +
+
+ + + + +
+ + {hasDiff && ( +
+
+
+

+ {localize('com_audit_detail_before')} +

+ +
+
+

+ {localize('com_audit_detail_after')} +

+ +
+
+
+ )} +
+
+ +
+
+
+
+
+ ); +} + +function DetailRow({ label, children }: { label: string; children: ReactElement }): ReactElement { + return ( +
+
+ {label} +
+
{children}
+
+ ); +} diff --git a/src/components/grants/AuditLogRow.tsx b/src/components/grants/AuditLogRow.tsx deleted file mode 100644 index 3b1b2e0..0000000 --- a/src/components/grants/AuditLogRow.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { Icon } from '@clickhouse/click-ui'; -import type * as t from '@/types'; -import { capabilityLabel, formatTimestamp } from './auditLogUtils'; -import { getScopeTypeConfig } from '@/constants'; -import { useLocalize } from '@/hooks'; -import { cn } from '@/utils'; - -export function AuditLogRow({ entry, isLast }: t.AuditLogRowProps) { - const localize = useLocalize(); - const targetConfig = getScopeTypeConfig(entry.targetPrincipalType); - - return ( - - - - {entry.action === 'grant_assigned' - ? localize('com_audit_action_assigned') - : localize('com_audit_action_removed')} - - - - - - - {localize(targetConfig.labelKey)} - - {entry.targetName} - - - -
- - {capabilityLabel(entry.capability, localize)} - - {entry.capability} -
- - {entry.actorName} - - {formatTimestamp(entry.timestamp)} - - - ); -} diff --git a/src/components/grants/AuditLogTab.tsx b/src/components/grants/AuditLogTab.tsx index b1884eb..ce2021e 100644 --- a/src/components/grants/AuditLogTab.tsx +++ b/src/components/grants/AuditLogTab.tsx @@ -1,63 +1,391 @@ -import { Icon } from '@clickhouse/click-ui'; -import { useQuery } from '@tanstack/react-query'; -import { useState, useMemo, useCallback } from 'react'; +import { PrincipalType } from 'librechat-data-provider'; +import { useNavigate, useSearch } from '@tanstack/react-router'; +import { keepPreviousData, useQuery } from '@tanstack/react-query'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { Badge, Button, DatePicker, Icon, Select, TextField } from '@clickhouse/click-ui'; +import type { AuditAction } from '@librechat/data-schemas'; +import type { AuditFilters } from '@/server'; import type * as t from '@/types'; -import { EmptyState, LoadingState, SearchInput } from '@/components/shared'; -import { auditLogQueryOptions, exportAuditLogCsvFn } from '@/server'; -import { ACTION_FILTER_LABELS } from './auditLogUtils'; -import { AuditLogRow } from './AuditLogRow'; -import { useLocalize } from '@/hooks'; +import { + ACTION_BADGE_STATE, + ACTION_LABEL_KEY, + auditCapability, + buildEntryPermalink, + capabilityLabel, + dateToIsoDate, + formatTimestamp, + isoDateToDate, + localDayBoundaryIso, +} from './auditLogUtils'; +import { + AUDIT_LOG_PAGE_SIZE, + auditLogEntryQueryOptions, + auditLogQueryOptions, + exportAuditLogServerFn, +} from '@/server'; +import { + EmptyState, + LoadingState, + Pagination, + ScreenReaderAnnouncer, + SearchInput, +} from '@/components/shared'; +import { useAnnouncement, useDebouncedFilter, useLocalize } from '@/hooks'; +import { getScopeTypeConfig, isAuditEntryId } from '@/constants'; +import { AuditLogDetailDrawer } from './AuditLogDetailDrawer'; import { cn } from '@/utils'; +const AUDIT_ACTIONS: readonly AuditAction[] = ['grant.assigned', 'grant.removed'] as const; +const TARGET_TYPE_OPTIONS: readonly PrincipalType[] = [ + PrincipalType.USER, + PrincipalType.GROUP, + PrincipalType.ROLE, +] as const; +/** Radix `Select.Item` cannot use `value=""` (Radix reserves empty string for + * "no selection"). Use a non-empty sentinel and translate to `''` in state. */ +const TARGET_TYPE_ALL = '__all__'; + +/** + * Wraps a click-ui DatePicker so only the trigger button is tab-focusable. + * click-ui renders both a PopoverTrigger button AND an inner readonly input, + * which produces two stops in the tab order. The input has no exposed `tabIndex` + * prop, so we reach for the DOM node and set it to -1. The class hooks the + * CSS rule that rounds the trigger's focus outline to match the wrapper border. + * + * `resetKey` is the caller's signal that the inner DatePicker has been keyed + * to remount (e.g. the Clear button bumping a nonce): the inner `` is + * replaced and the previous patch is lost, so the effect must re-run and + * re-apply `tabIndex = -1` against the fresh DOM node. + */ +function DatePickerCell({ + children, + resetKey, + inputId, +}: { + children: React.ReactNode; + resetKey?: unknown; + /** + * Stamps the click-ui-rendered `` with an `id` so an external + * `