diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 02a14f18..a3ef5498 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -123,6 +123,10 @@ jobs: working-directory: frontend run: pnpm check + - name: Run tests + working-directory: frontend + run: pnpm test + - name: Build working-directory: frontend run: pnpm build diff --git a/frontend/package.json b/frontend/package.json index 05e303ac..2d87c3d4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,15 +11,17 @@ "check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch", "format": "prettier --write .", - "lint": "prettier --check . && eslint ." + "lint": "prettier --check . && eslint .", + "test": "vitest run", + "test:watch": "vitest" }, "devDependencies": { "@eslint/compat": "^2.1.0", "@eslint/js": "^10.0.1", - "@internationalized/date": "^3.12.2", - "@lucide/svelte": "^1.27.0", + "@internationalized/date": "^3.12.3", + "@lucide/svelte": "^1.28.0", "@sveltejs/adapter-node": "^5.5.7", - "@sveltejs/kit": "^2.70.1", + "@sveltejs/kit": "^2.70.2", "@sveltejs/vite-plugin-svelte": "^7.2.0", "@tailwindcss/typography": "^0.5.20", "@tailwindcss/vite": "^4.3.3", @@ -28,26 +30,27 @@ "eslint": "^10.8.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-svelte": "^3.22.0", - "globals": "^17.7.0", + "globals": "^17.9.0", "mode-watcher": "^1.1.0", "prettier": "^3.9.6", "prettier-plugin-svelte": "^4.1.1", "prettier-plugin-tailwindcss": "^0.8.1", "svelte": "^5.56.8", - "svelte-check": "^4.7.3", + "svelte-check": "^4.7.4", "svelte-sonner": "^1.1.1", "tailwindcss": "^4.3.3", "typescript": "^6.0.3", - "vite": "^8.1.5" + "vite": "^8.2.0", + "vitest": "^4.1.10" }, "dependencies": { - "@sentry/sveltekit": "^10.68.0", - "axios": "^1.18.1", + "@sentry/sveltekit": "^10.69.0", + "axios": "^1.19.0", "clsx": "^2.1.1", - "libphonenumber-js": "^1.13.9", - "svelte-dnd-action": "^0.9.74", + "libphonenumber-js": "^1.13.10", + "svelte-dnd-action": "^0.9.77", "tailwind-merge": "^3.6.0", - "tailwind-variants": "^3.2.2", + "tailwind-variants": "^3.3.0", "tw-animate-css": "^1.4.0" } } diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 600f65ac..4eac1802 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -4,31 +4,34 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + brace-expansion@>=4.0.0 <5.0.9: ^5.0.9 + importers: .: dependencies: '@sentry/sveltekit': - specifier: ^10.68.0 - version: 10.68.0(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@sveltejs/kit@2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8)(vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0)))(svelte@5.56.8)(typescript@6.0.3)(vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0)))(rollup@4.62.2)(svelte@5.56.8)(vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0)) + specifier: ^10.69.0 + version: 10.69.0(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@sveltejs/kit@2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8)(vite@8.2.0(@types/node@25.9.5)(jiti@2.7.0)))(svelte@5.56.8)(typescript@6.0.3)(vite@8.2.0(@types/node@25.9.5)(jiti@2.7.0)))(rollup@4.62.2)(svelte@5.56.8)(vite@8.2.0(@types/node@25.9.5)(jiti@2.7.0)) axios: - specifier: ^1.18.1 - version: 1.18.1(debug@4.4.3) + specifier: ^1.19.0 + version: 1.19.0(debug@4.4.3) clsx: specifier: ^2.1.1 version: 2.1.1 libphonenumber-js: - specifier: ^1.13.9 - version: 1.13.9 + specifier: ^1.13.10 + version: 1.13.10 svelte-dnd-action: - specifier: ^0.9.74 - version: 0.9.74(svelte@5.56.8) + specifier: ^0.9.77 + version: 0.9.77(svelte@5.56.8) tailwind-merge: specifier: ^3.6.0 version: 3.6.0 tailwind-variants: - specifier: ^3.2.2 - version: 3.2.2(tailwind-merge@3.6.0)(tailwindcss@4.3.3) + specifier: ^3.3.0 + version: 3.3.0(tailwind-merge@3.6.0)(tailwindcss@4.3.3) tw-animate-css: specifier: ^1.4.0 version: 1.4.0 @@ -40,32 +43,32 @@ importers: specifier: ^10.0.1 version: 10.0.1(eslint@10.8.0(jiti@2.7.0)) '@internationalized/date': - specifier: ^3.12.2 - version: 3.12.2 + specifier: ^3.12.3 + version: 3.12.3 '@lucide/svelte': - specifier: ^1.27.0 - version: 1.27.0(svelte@5.56.8) + specifier: ^1.28.0 + version: 1.28.0(svelte@5.56.8) '@sveltejs/adapter-node': specifier: ^5.5.7 - version: 5.5.7(@sveltejs/kit@2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8)(vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0)))(svelte@5.56.8)(typescript@6.0.3)(vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0))) + version: 5.5.7(@sveltejs/kit@2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8)(vite@8.2.0(@types/node@25.9.5)(jiti@2.7.0)))(svelte@5.56.8)(typescript@6.0.3)(vite@8.2.0(@types/node@25.9.5)(jiti@2.7.0))) '@sveltejs/kit': - specifier: ^2.70.1 - version: 2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8)(vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0)))(svelte@5.56.8)(typescript@6.0.3)(vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0)) + specifier: ^2.70.2 + version: 2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8)(vite@8.2.0(@types/node@25.9.5)(jiti@2.7.0)))(svelte@5.56.8)(typescript@6.0.3)(vite@8.2.0(@types/node@25.9.5)(jiti@2.7.0)) '@sveltejs/vite-plugin-svelte': specifier: ^7.2.0 - version: 7.2.0(svelte@5.56.8)(vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0)) + version: 7.2.0(svelte@5.56.8)(vite@8.2.0(@types/node@25.9.5)(jiti@2.7.0)) '@tailwindcss/typography': specifier: ^0.5.20 version: 0.5.20(tailwindcss@4.3.3) '@tailwindcss/vite': specifier: ^4.3.3 - version: 4.3.3(vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0)) + version: 4.3.3(vite@8.2.0(@types/node@25.9.5)(jiti@2.7.0)) '@types/node': specifier: ^25.9.5 version: 25.9.5 bits-ui: specifier: ^2.18.1 - version: 2.18.1(@internationalized/date@3.12.2)(@sveltejs/kit@2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8)(vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0)))(svelte@5.56.8)(typescript@6.0.3)(vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0)))(svelte@5.56.8) + version: 2.18.1(@internationalized/date@3.12.3)(@sveltejs/kit@2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8)(vite@8.2.0(@types/node@25.9.5)(jiti@2.7.0)))(svelte@5.56.8)(typescript@6.0.3)(vite@8.2.0(@types/node@25.9.5)(jiti@2.7.0)))(svelte@5.56.8) eslint: specifier: ^10.8.0 version: 10.8.0(jiti@2.7.0) @@ -76,8 +79,8 @@ importers: specifier: ^3.22.0 version: 3.22.0(eslint@10.8.0(jiti@2.7.0))(svelte@5.56.8) globals: - specifier: ^17.7.0 - version: 17.7.0 + specifier: ^17.9.0 + version: 17.9.0 mode-watcher: specifier: ^1.1.0 version: 1.1.0(svelte@5.56.8) @@ -94,8 +97,8 @@ importers: specifier: ^5.56.8 version: 5.56.8 svelte-check: - specifier: ^4.7.3 - version: 4.7.3(picomatch@4.0.5)(svelte@5.56.8)(typescript@6.0.3) + specifier: ^4.7.4 + version: 4.7.4(picomatch@4.0.5)(svelte@5.56.8)(typescript@6.0.3) svelte-sonner: specifier: ^1.1.1 version: 1.1.1(svelte@5.56.8) @@ -106,13 +109,16 @@ importers: specifier: ^6.0.3 version: 6.0.3 vite: - specifier: ^8.1.5 - version: 8.1.5(@types/node@25.9.5)(jiti@2.7.0) + specifier: ^8.2.0 + version: 8.2.0(@types/node@25.9.5)(jiti@2.7.0) + vitest: + specifier: ^4.1.10 + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@25.9.5)(vite@8.2.0(@types/node@25.9.5)(jiti@2.7.0)) packages: - '@apm-js-collab/code-transformer-bundler-plugins@0.7.1': - resolution: {integrity: sha512-Yidf5GOl60db80UxUtNdKK3pnY7obU/gs0xOfA0SCdnvVLMCvfYIer/egC3TqpPiT0Jg22eg3RlzcO+zKfPMcA==} + '@apm-js-collab/code-transformer-bundler-plugins@0.7.3': + resolution: {integrity: sha512-qNbPwuMZ8f5ZuGj/ttPeB7a6C/S1bB6tNYaEL5vNiRKydSAxa4AU0gxCWgaP4fVju+AuwhcumSFjrEcGF9Dv7Q==} engines: {node: '>=18.0.0'} '@apm-js-collab/code-transformer@0.18.1': @@ -134,8 +140,8 @@ packages: resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} engines: {node: '>=6.9.0'} - '@babel/generator@7.29.7': - resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} + '@babel/generator@7.29.8': + resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==} engines: {node: '>=6.9.0'} '@babel/helper-compilation-targets@7.29.7': @@ -172,8 +178,8 @@ packages: resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} engines: {node: '>=6.9.0'} - '@babel/parser@7.29.7': - resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + '@babel/parser@7.29.8': + resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==} engines: {node: '>=6.0.0'} hasBin: true @@ -181,22 +187,22 @@ packages: resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.29.7': - resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} + '@babel/traverse@7.29.8': + resolution: {integrity: sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==} engines: {node: '>=6.9.0'} - '@babel/types@7.29.7': - resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + '@babel/types@7.29.8': + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} engines: {node: '>=6.9.0'} - '@emnapi/core@1.11.1': - resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} + '@emnapi/core@2.0.0-alpha.3': + resolution: {integrity: sha512-AZypUeJ/yByuxyS7BlSNRDOMLMlROYtjYdIAuBmJssVz1UJDSeYxLrdizhXCFYhedC5bqd/ASy8EuNXbVVXp9g==} - '@emnapi/runtime@1.11.1': - resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + '@emnapi/runtime@2.0.0-alpha.3': + resolution: {integrity: sha512-hFPAhMUjJD9BSyCANEISPOogeXC9Zo9ZQl7L6vKnaVsMkCtzznaW/naYypeyl0Gv5rYfWYsZbpixTMpjDJzQeA==} - '@emnapi/wasi-threads@1.2.2': - resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + '@emnapi/wasi-threads@2.0.1': + resolution: {integrity: sha512-9DsSk+o5NBX0CCJT8s0EROGSGxjR/tKu6aBTaVyq+SjAEQH4XcdcRxPBRzsBLizTTJ49MJjF+jgu3qnO9GLQcQ==} '@eslint-community/eslint-utils@4.10.1': resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==} @@ -275,8 +281,8 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} - '@internationalized/date@3.12.2': - resolution: {integrity: sha512-FY1Y+H64NDs+HAF6omlnWxm3mEpfgaCSWtL5l551ZZfImA+kGjPFgrnJrGjH6lfmLL0g8Z/mBu1R3kufeCp6Jw==} + '@internationalized/date@3.12.3': + resolution: {integrity: sha512-fuLX+3ZKLsxI73y8b01EG/WjHb6gE6weCqlfawPO27kBWGMh9G1yH6Csv1uU7/cac9H2GHmOMt6CjmuQ1aia4Q==} '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -294,16 +300,17 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@lucide/svelte@1.27.0': - resolution: {integrity: sha512-qzNBHdqy/MKp2qxuy/DcYELuep6h19fjo+4ech8fZsS4GtZeBtg5jgCTRQvLvwhppt0mhRN6qt8Nt8rPb/J4Ag==} + '@lucide/svelte@1.28.0': + resolution: {integrity: sha512-C1Ge84KW2z4q44/WDIEo/2KC2jby5u6mSlta7q+p6g7u0ld8sC/w1fMBiSswF+4bxAl9jlzzaVTpp79qqkiy5g==} peerDependencies: svelte: ^5 - '@napi-rs/wasm-runtime@1.1.6': - resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} + '@napi-rs/wasm-runtime@1.2.2': + resolution: {integrity: sha512-JfB4kuJQjaoHuCTseIINHtHWeJnvgEcxjwA5t/Y00ZgaOO1Crz3fjT/p8kT28zA/Caz7oiUMn3d6H2yOVCVwuw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} peerDependencies: - '@emnapi/core': ^1.7.1 - '@emnapi/runtime': ^1.7.1 + '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.3 + '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.3 '@opentelemetry/api-logs@0.220.0': resolution: {integrity: sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w==} @@ -347,103 +354,102 @@ packages: resolution: {integrity: sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==} engines: {node: '>=14'} - '@oxc-project/types@0.139.0': - resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==} + '@oxc-project/types@0.142.0': + resolution: {integrity: sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ==} '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} - '@rolldown/binding-android-arm64@1.1.5': - resolution: {integrity: sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==} + '@rolldown/binding-android-arm64@1.2.1': + resolution: {integrity: sha512-02hOeOSryYxVrOIphmLAsqnCJWxwlzFk+pEt/N/i6OgT3lShHO7xGCU5cpgchRDHboAEbSjzgGh+O/u1GswQmA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.1.5': - resolution: {integrity: sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==} + '@rolldown/binding-darwin-arm64@1.2.1': + resolution: {integrity: sha512-fMsTOnN0OjFm3CyppWPitKnc8UlliVARUULW6cfU6AIqjdtgmSFWSk9vecHzZduv/yMWIHDlRhM1e8Iff9uAfA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.1.5': - resolution: {integrity: sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==} + '@rolldown/binding-darwin-x64@1.2.1': + resolution: {integrity: sha512-1wjKdz/XLGKHaTNHjQveQ/B23TKx4ItAqm1JbyVuvNPc4Ze0Fb48s49TAd/2zcplPl8okE/UbTgmlVfwT7eFeQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.1.5': - resolution: {integrity: sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==} + '@rolldown/binding-freebsd-x64@1.2.1': + resolution: {integrity: sha512-Fa0jHR07E7YBN4vOEsbVf2briYNsuOowfLJaXULZM0ldMlaCaj2LJgLMbMe4iacRyZmvR8efFhgR9wKuGclQUg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.1.5': - resolution: {integrity: sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==} + '@rolldown/binding-linux-arm-gnueabihf@1.2.1': + resolution: {integrity: sha512-pzkgu1SSHGgRRyRZ4fbmSgmajbVt+epaLP99NDjFft69v/ypfTi6swBMiVdh2EkQ0OSnHE1lZDM7DRGkyAzUpA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.1.5': - resolution: {integrity: sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==} + '@rolldown/binding-linux-arm64-gnu@1.2.1': + resolution: {integrity: sha512-QI5SEDY8cbiYWHx0VO4vIc3UlS6a32vXHjU8Qy/17adEmZIPuByJg13UEvo9c/UCiUkdcVWY83C+b+JrwnNyUg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.1.5': - resolution: {integrity: sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==} + '@rolldown/binding-linux-arm64-musl@1.2.1': + resolution: {integrity: sha512-Sm41FyCeXqmYcERoYOCbGIL5hNfd8w9LQ7Y61Bev48HkcjaJqV/iiVOaiDxjVTRMS+QKrZmD8cfPt4uMVnvM+A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.1.5': - resolution: {integrity: sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==} + '@rolldown/binding-linux-ppc64-gnu@1.2.1': + resolution: {integrity: sha512-2x+WhXTGl9yJYPbltW/BSEPTVz9OIWQyER4N+gJEDWkkn904eRcBzELqh/Hf7K0w/ubGbKNMv0ZC+94QK/IFEg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.1.5': - resolution: {integrity: sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==} + '@rolldown/binding-linux-s390x-gnu@1.2.1': + resolution: {integrity: sha512-eEjmQpuRQayHPWWnywaWHkFT3ToPbP3RYy42VVd/B9aBGDA+Ol25EIWHxKQST3IiWJjikCWUF7KtbfqwZrzVwQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.1.5': - resolution: {integrity: sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==} + '@rolldown/binding-linux-x64-gnu@1.2.1': + resolution: {integrity: sha512-/Orga1fZYkLc/56jBICcHrKchl8Z2UKdDSr3LG9ToWO1lQ6a4Livk9Xz+9WN91zsz5QR3XQz2NNoSDEvP6qadw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.1.5': - resolution: {integrity: sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==} + '@rolldown/binding-linux-x64-musl@1.2.1': + resolution: {integrity: sha512-xxBJRL+0q0Kce7orznGWLuylHDY65vuARXZRpX+hPdv+DqK2c3NlCsVA98tlWzWNEE7yPqA/1NQ5nnCrj49Y5A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.1.5': - resolution: {integrity: sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==} + '@rolldown/binding-openharmony-arm64@1.2.1': + resolution: {integrity: sha512-M6AdXIXw3s+/8XpKMzdGDEXGS1S7kwUsy+rcTIUIOx5Ge4nXKCtAFHFV9YKkXvGcC5WMoTjAteLzlsQROVI0Yw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.1.5': - resolution: {integrity: sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [wasm32] + '@rolldown/binding-wasm32-wasi@1.2.1': + resolution: {integrity: sha512-/TX0SoRGojHzSAHpfVBbavRVSazg5U3h3Y3VXfcc0cdugq6kxdqw8LPGFiPr+/7gE/60zRcsOY2Vi9b9eT0jww==} + engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} - '@rolldown/binding-win32-arm64-msvc@1.1.5': - resolution: {integrity: sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==} + '@rolldown/binding-win32-arm64-msvc@1.2.1': + resolution: {integrity: sha512-EvRrivJieyHG+AO9lleZWgq+g0+S7oV2C51yuqlcyU/R9net+sI4Pj0F+lUoP2bEr6TWX3SqFaaS0SzfLxSzkw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.1.5': - resolution: {integrity: sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==} + '@rolldown/binding-win32-x64-msvc@1.2.1': + resolution: {integrity: sha512-Z4eCmn5QJ/5+azF9knpLWKfVd9aidn0mAe9TpJgvBLId9Ax3t0+JVxBmT25Bv7NBbVW1TZyKjQjQReouMeH5UQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -634,16 +640,16 @@ packages: cpu: [x64] os: [win32] - '@sentry/browser-utils@10.68.0': - resolution: {integrity: sha512-be8VtdjCngKc77cstJeV+gO15iH+blyXpBDk8yOehmtX4BkFO33mfTMNCWVR2LA0oOxjIHWRAhf77fIUEhzxPg==} + '@sentry/browser-utils@10.69.0': + resolution: {integrity: sha512-e/u1Abj0zRPwR/deGZAP3GOULrsx67/XXnM5Skniqs4uxTsdNtPek1Nef0tpxwaQJYxwh6pWdhswLPPbbPOgBQ==} engines: {node: '>=18'} - '@sentry/browser@10.68.0': - resolution: {integrity: sha512-8xVgk7oG2lajXnbXF6a7H1xMZ/U6icqSldHGzQu1+bajfrK8Gan9ULG/Xsj1VM1LlNeK6/7znDJ3u1jgvIwznw==} + '@sentry/browser@10.69.0': + resolution: {integrity: sha512-8391tnm96YbR7b8SYfEA/NEIZuyb2r3SZrtAT0bhZtjlujcYWjo7gugQvk8sWLU9cAa/euD00eJoIoJvNfpd7Q==} engines: {node: '>=18'} - '@sentry/bundler-plugins@10.68.0': - resolution: {integrity: sha512-XWv7asJuTTUSlacROvqcIFKuNxAf3PYL/mdjMBhBwp3rJ4vMkA73jqNPjqCTm726cHs/Y4yadbbFA/OZLPWzeg==} + '@sentry/bundler-plugins@10.69.0': + resolution: {integrity: sha512-I1otnSJIH4IOugLp+kcBbT0Kcex+J8xnHuUyzMAwCYSpZ0FMVvA723uNmkMdW0PxRRw0oEhe0qDB+t+ZjHxUiA==} engines: {node: '>= 18'} peerDependencies: rollup: '>=3.2.0' @@ -706,8 +712,8 @@ packages: engines: {node: '>= 10'} hasBin: true - '@sentry/cloudflare@10.68.0': - resolution: {integrity: sha512-IcSjfncUSkisoZy66SbLMb4gtH+Jr9mS02DIqjuz3JsrKnkxFaQIBiosoDTgHlCMWFIEU5M4MpErYZ1dzJpo6Q==} + '@sentry/cloudflare@10.69.0': + resolution: {integrity: sha512-CO6Tr74cw3Wx2DyWSfBCxR6EDcNfBjn52hxWacxHsLurpdfQ7Bp/J32m1KxzezR5CZXngfBTmemK3iZsMgt5uQ==} engines: {node: '>=18'} peerDependencies: '@cloudflare/workers-types': ^4.x || ^5.x @@ -722,16 +728,16 @@ packages: resolution: {integrity: sha512-fO9PLmHdVURcSPUpWCItWAtgKiMwGdJHbovoSEyLplX5sxs2ugvI4CBPTrkkgqhObnZOD0CnWBKDzSVQYBKEyQ==} engines: {node: '>=14'} - '@sentry/core@10.68.0': - resolution: {integrity: sha512-5Amhx8ltVz7vb1bRGyf3c4J69/iHW8R/H+SJxTRILHlsSOBrnVVc/IQEYDC6PTRdRdZ3x2u7RVjxZi2Mhe525g==} + '@sentry/core@10.69.0': + resolution: {integrity: sha512-+uuqVEeiDzYuAKjZLqsROKXvRTbl/QeH0gfGRtpYib1cud4rAFWRIkFmcR7Jb7JGFYwmReyQotiTj/hcDszTZg==} engines: {node: '>=18'} - '@sentry/feedback@10.68.0': - resolution: {integrity: sha512-XbdcXiBnpC3vgw46eHOPeD/ZQ+XzluP75ubdUcaPDW02hCh2nsdXiwjZ2DBImbpvIpTbJgjHf/sIlHWvcZJ2Mg==} + '@sentry/feedback@10.69.0': + resolution: {integrity: sha512-qrGz5Qaw93/IhMjlFN6uIaXeHwgHDaKGa6FkTAP6PonpkvSbGGqan6xfsENxzj9HUVoli1lZ6tMRDnt2qtSPhg==} engines: {node: '>=18'} - '@sentry/node-core@10.68.0': - resolution: {integrity: sha512-VreORXnruy8A2SyprZKENAq3ArGwn35KPewLQSQ5dgDXSFtUj2scLuzZoVsZ/Uyt83td0WPvD6Lv0X7MvSYAMQ==} + '@sentry/node-core@10.69.0': + resolution: {integrity: sha512-IgArHczrZJxkgxoffHscj0NxQrG6kCazgmGQnlf3j58J1ec21YaUu8Tu+7G4Lo5tCiW3teQnwlKW1ttMXSqWRw==} engines: {node: '>=18'} peerDependencies: '@opentelemetry/api': ^1.9.0 @@ -751,38 +757,38 @@ packages: '@opentelemetry/sdk-trace-base': optional: true - '@sentry/node@10.68.0': - resolution: {integrity: sha512-bnvRzEehquG/894DD3BWNCBUbDWsLQfYPU+SNCMd6G4Ext75RthVkRs8R0sRaE6b8Tw9HSEgySHL834Tf8lVsA==} + '@sentry/node@10.69.0': + resolution: {integrity: sha512-xEXA1YGIiTZbrW6MWV34uS6JGQuQg2ijTI0zed+FsJb9JZKPYel/GZK8Km26vfTVb+yCXFmWZNBesKegNcVdzg==} engines: {node: '>=18'} - '@sentry/opentelemetry@10.68.0': - resolution: {integrity: sha512-JDNH9dacX0MSi7FRvabPCJUAXRMTe16bE/Gajc/7Gfcw7Rwvo4DZ0nd162PCSAW9QoEjUT12upDehEHJuFmsXg==} + '@sentry/opentelemetry@10.69.0': + resolution: {integrity: sha512-3FyWV6YcEJuvLrlaKGE1dHXCI+1YO0a62w7PkwlRg8yp6K6YXkmdwu9GjqaYD+Ju4tm7uC7mHIsGFQMm0M7pqQ==} engines: {node: '>=18'} peerDependencies: '@opentelemetry/api': ^1.9.0 '@opentelemetry/core': ^1.30.1 || ^2.1.0 '@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.1.0 - '@sentry/replay-canvas@10.68.0': - resolution: {integrity: sha512-HusYcr+He+ohnUDHunYrc5St6vdDnBXpUAndnT5ReyUMVSCiWKfY3paXowU/0787HwYfxdcpZgwC5u79+XbEIg==} + '@sentry/replay-canvas@10.69.0': + resolution: {integrity: sha512-VF6nXvSninHcc7dC1Zme0RjkC7VgRMCixs6jKaQX5zTNeqTW3dZGSefSOVv+ZteRi3hJvVORq985VjUC9Z/0+A==} engines: {node: '>=18'} - '@sentry/replay@10.68.0': - resolution: {integrity: sha512-ZoG2n16vbkx4GWSCnLIqUUN9xlUmccQFbQ2US2rhruQeHTUnHl/ukr8NHOQXZaEbwKyMkX9bEMwfmZHJm+wSTQ==} + '@sentry/replay@10.69.0': + resolution: {integrity: sha512-uRhmNhtFGPOlM0iniVmWKAX3KVXI0le41yYK/iKdPjinT9jA3ZrmykO/Fv1v/KI5znOtwa9D6eHRnDTTMRxFrg==} engines: {node: '>=18'} - '@sentry/server-utils@10.68.0': - resolution: {integrity: sha512-lp1ZSs1auw7HrCESSYt/n4dOUaKPVUIAKyVYRk6xVr4bMIN3RPub/H5Wm7QPj9CpXVC5bQFvB6+dHZXz809oMg==} + '@sentry/server-utils@10.69.0': + resolution: {integrity: sha512-0MwHrA8+nNvMIsqf8m3cXwCBlUjr6AS7N6CZvHJtY1DkqEvQqEbD5VIrhzEyHN/KMZIgQ8XeDCQRhjnXFQGRhg==} engines: {node: '>=18'} - '@sentry/svelte@10.68.0': - resolution: {integrity: sha512-3PzjhvelZFYmt0WkHF8tPpBRLlbBjb7TFeqUEheiMoQ4L87h+xmIu3QkRxKnp8gtzimNdT5n7arX561Ni7AZ5g==} + '@sentry/svelte@10.69.0': + resolution: {integrity: sha512-ryRAJZIsiRpDrMJTEodSloCuvFSanUs2slWc61KtW2hTMgWw9dj5oA1tMyNJpOfanDZGMWGiGv+W/2szp4GoRg==} engines: {node: '>=18'} peerDependencies: svelte: 3.x || 4.x || 5.x - '@sentry/sveltekit@10.68.0': - resolution: {integrity: sha512-DGYspA0FfAb//OBac2qCZpZ3Z79e7d9HkLwU6wkUvTkluC83si3qb0trskons8hh2p/gEWbPhVLUh2++yQIkpA==} + '@sentry/sveltekit@10.69.0': + resolution: {integrity: sha512-KXXudTYZR1ALycPJpWggti5NvD3XU4iDBmNSC/CsmJQyohZmlQk916RPyx9KfEF5PTtI711KjzM3kFdkWPJpSg==} engines: {node: '>=18'} peerDependencies: '@sveltejs/kit': 2.x || ^3.0.0-0 @@ -808,8 +814,8 @@ packages: peerDependencies: '@sveltejs/kit': ^2.4.0 - '@sveltejs/kit@2.70.1': - resolution: {integrity: sha512-nY9SPHGOZro3doud9vZXDBwl9tCZIouuJztjgSHs6PAIrv9M/z5O7eOhPV5xU7CgVHA976Jwu3BA1hIFvXztkA==} + '@sveltejs/kit@2.70.2': + resolution: {integrity: sha512-RzRoRpuR2KXqc5yMO0akQHDZeT4AslOlznGITURsqHaVbtyYP4Wn3eE3gxj9JcDyNYO0crkxhdwFHc+2vkVm6w==} engines: {node: '>=18.13'} hasBin: true peerDependencies: @@ -940,9 +946,15 @@ packages: '@tybys/wasm-util@0.10.3': resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/cookie@0.6.0': resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/esrecurse@4.3.1': resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} @@ -961,6 +973,35 @@ packages: '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@vitest/expect@4.1.10': + resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} + + '@vitest/mocker@4.1.10': + resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.10': + resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} + + '@vitest/runner@4.1.10': + resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==} + + '@vitest/snapshot@4.1.10': + resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==} + + '@vitest/spy@4.1.10': + resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} + + '@vitest/utils@4.1.10': + resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -971,6 +1012,11 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} + engines: {node: '>=0.4.0'} + hasBin: true + agent-base@6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} @@ -982,6 +1028,10 @@ packages: resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==} engines: {node: '>= 0.4'} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + astring@1.9.0: resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} hasBin: true @@ -989,8 +1039,8 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - axios@1.18.1: - resolution: {integrity: sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==} + axios@1.19.0: + resolution: {integrity: sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==} axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} @@ -1000,8 +1050,8 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} - baseline-browser-mapping@2.11.3: - resolution: {integrity: sha512-sbT0Ui/CZwyAyy7icT1Gw5P1LKRlFaHwaF6tDCW5YHq2X5SeeZFphBuIagopSfwSSZq3sQcbmEL072yphxm7ew==} + baseline-browser-mapping@2.11.11: + resolution: {integrity: sha512-/yImnXwyTvgMkhgekLHok/Rx5vO6E0BmStWlSqKWMVm2a2ITuZ1Tn+9bgLS+gZRdZmWtd8nxuhHpdmCUOWsTQQ==} engines: {node: '>=6.0.0'} hasBin: true @@ -1012,8 +1062,8 @@ packages: '@internationalized/date': ^3.8.1 svelte: ^5.33.0 - brace-expansion@5.0.8: - resolution: {integrity: sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==} + brace-expansion@5.0.9: + resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} engines: {node: 20 || >=22} browserslist@4.28.7: @@ -1028,6 +1078,10 @@ packages: caniuse-lite@1.0.30001806: resolution: {integrity: sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==} + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -1093,6 +1147,9 @@ packages: devalue@5.8.2: resolution: {integrity: sha512-DObPPAfdtFbXjxLqK8s2Xk9ZuWz5+ZoFEhC7J76es4GU/rEiXwHTmbImoCdyoCOcBH1UF3+Cz6Z2sYD4hyl5TA==} + devalue@5.9.0: + resolution: {integrity: sha512-RWrqdArjvPbsATEhOPUo6Wndc/iWnkWKlhIrdlF3zMMYo/c3CVtoaVAyLtWxz5h8nSlkHzxnzV2uLydPXmtF+A==} + dotenv@17.4.2: resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} engines: {node: '>=12'} @@ -1101,8 +1158,8 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} - electron-to-chromium@1.5.396: - resolution: {integrity: sha512-yHiw2Y3C3H9U6TMbOfoWK/BPreiOPXRfTWPBwQBoZG6/8TB6eOPnsy5oaRYuatR7Fw2SJ4kKforgufeo7fq0EQ==} + electron-to-chromium@1.5.399: + resolution: {integrity: sha512-lEcqhErbHjXRvd41rnWLpzbyU/IXfIYo7QwaFWmxGeLiLyY2TBCdHnWY88vB+p3ubnihRypDm66panXl7TylLA==} enhanced-resolve@5.24.3: resolution: {integrity: sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ==} @@ -1215,10 +1272,17 @@ packages: estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1297,8 +1361,8 @@ packages: resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} engines: {node: '>=18'} - globals@17.7.0: - resolution: {integrity: sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==} + globals@17.9.0: + resolution: {integrity: sha512-m/MvAW61QVU5VDNF1Vj8axt016h8w7L5TU1e9zlab7XIttAT2YAlCwl75K1fOqvMM9apmD7lbCIRhpfkhmxhCg==} engines: {node: '>=18'} globalyzer@0.1.0: @@ -1334,8 +1398,8 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} - import-in-the-middle@3.3.2: - resolution: {integrity: sha512-jTd2FfOgOWOdgjkHuk/1Ms8VKFXkPs15ymYBETw1sAOrO/dY3XeGVRWir9qBbw7pXr0T2eTFwfCZ+N02HmiNGA==} + import-in-the-middle@3.3.3: + resolution: {integrity: sha512-AiohS3H80sXO6owEltjGX+glb7qXaDhBoJb9XcQVH4UI207xu/bDLUcadVKp7Qe576reg9yr/PXZjV5qx8gfbA==} engines: {node: '>=18'} imurmurhash@0.1.4: @@ -1409,8 +1473,8 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} - libphonenumber-js@1.13.9: - resolution: {integrity: sha512-VNS5vWMM7r0P66BYv+TQJATxExEgLxN+34hfHDVhDkUsGAE4cRg0shCNSLTXNKm7nIUscC7AfB51TjxEeF7msQ==} + libphonenumber-js@1.13.10: + resolution: {integrity: sha512-xJxrdqvbl2rtn2MaUJrUejz8J7/uZNC0V77oks2LxYrO/+ZtVpRmz+fEQMuu6VusnEB1fmpByiLS1WXecOnAnw==} lightningcss-android-arm64@1.32.0: resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} @@ -1605,6 +1669,10 @@ packages: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} + engines: {node: 18 || 20 || >=22} + minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -1683,6 +1751,9 @@ packages: resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} engines: {node: 18 || 20 || >=22} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1726,6 +1797,10 @@ packages: resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==} engines: {node: ^10 || ^12 || >=14} + postcss@8.5.25: + resolution: {integrity: sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==} + engines: {node: ^10 || ^12 || >=14} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -1825,8 +1900,8 @@ packages: engines: {node: '>= 0.4'} hasBin: true - rolldown@1.1.5: - resolution: {integrity: sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==} + rolldown@1.2.1: + resolution: {integrity: sha512-4FKJhg8d3OiyQOA6Q1Q0hoFFpW9/OoX+VsHzpECsdsIZoOArrAK90gl59YK/Z+gnDel45bgJZK03ozH/9bCqEw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -1886,6 +1961,9 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + sirv@3.0.2: resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} engines: {node: '>=18'} @@ -1902,6 +1980,12 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + style-to-object@1.0.14: resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} @@ -1909,16 +1993,16 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - svelte-check@4.7.3: - resolution: {integrity: sha512-DHdTCGX62R0fCxBEaT+USdASAnoaRBaaNczkRJl0K7o3WyoCeVUbVxo6fKqpOll/B+WMWCsiFK0eFrJSNBKZIg==} + svelte-check@4.7.4: + resolution: {integrity: sha512-IW9ot9YqAoyv8FvyN+eb4ZTe8zgcKZrJLNYU6dzSKkGwEBsSPc4K7lmQ8bKn8W2YMXM6WDfZSSVOaGtekyUfOQ==} engines: {node: '>= 18.0.0'} hasBin: true peerDependencies: svelte: ^4.0.0 || ^5.0.0-next.0 - typescript: '>=5.0.0' + typescript: ^5.0.0 || ^6.0.0 - svelte-dnd-action@0.9.74: - resolution: {integrity: sha512-zn//d8plF73Y05joRdj0g9S7Z8KY5zoTyzjHtnaJMqQP//6U6smHAHi0ArZWi60bxpoOwo+im8gKhK/M7Eq1pQ==} + svelte-dnd-action@0.9.77: + resolution: {integrity: sha512-HEd4gH1OKKH/MzO5HTGldNpergAU4LyhqLvP4lrcmDpzKAGWbeb0FHB+ie1H+LJ1ZRbbjBHU6T/vYQzM80HSug==} peerDependencies: svelte: '>=3.23.0 || ^5.0.0-next.0' @@ -1958,15 +2042,17 @@ packages: tailwind-merge@3.6.0: resolution: {integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==} - tailwind-variants@3.2.2: - resolution: {integrity: sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg==} - engines: {node: '>=16.x', pnpm: '>=7.x'} + tailwind-variants@3.3.0: + resolution: {integrity: sha512-t1QsB42dcwUdaCEArO0tRZcP0nbCcAmJKRYCs7jmwYSFOCXlGqgO8c0TrCYm9OranYmT6i9YOi8LFo2gxmxnow==} + engines: {node: '>=16.9.x', pnpm: '>=7.x'} peerDependencies: tailwind-merge: '>=3.0.0' tailwindcss: '*' peerDependenciesMeta: tailwind-merge: optional: true + tailwindcss: + optional: true tailwindcss@4.3.3: resolution: {integrity: sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==} @@ -1978,10 +2064,21 @@ packages: tiny-glob@0.2.9: resolution: {integrity: sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.3.0: + resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==} + engines: {node: '>=18'} + tinyglobby@0.2.17: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} + tinyrainbow@3.1.1: + resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} + engines: {node: '>=14.0.0'} + totalist@3.0.1: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} @@ -2019,13 +2116,13 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - vite@8.1.5: - resolution: {integrity: sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==} + vite@8.2.0: + resolution: {integrity: sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.3.0 + '@vitejs/devtools': ^0.4.0 esbuild: ^0.27.0 || ^0.28.0 jiti: '>=1.21.0' less: ^4.0.0 @@ -2070,6 +2167,47 @@ packages: vite: optional: true + vitest@4.1.10: + resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + 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.10 + '@vitest/browser-preview': 4.1.10 + '@vitest/browser-webdriverio': 4.1.10 + '@vitest/coverage-istanbul': 4.1.10 + '@vitest/coverage-v8': 4.1.10 + '@vitest/ui': 4.1.10 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -2081,6 +2219,11 @@ packages: engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -2101,7 +2244,7 @@ packages: snapshots: - '@apm-js-collab/code-transformer-bundler-plugins@0.7.1': + '@apm-js-collab/code-transformer-bundler-plugins@0.7.3': dependencies: '@apm-js-collab/code-transformer': 0.18.1 es-module-lexer: 2.3.1 @@ -2136,14 +2279,14 @@ snapshots: '@babel/core@7.29.7': dependencies: '@babel/code-frame': 7.29.7 - '@babel/generator': 7.29.7 + '@babel/generator': 7.29.8 '@babel/helper-compilation-targets': 7.29.7 '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) '@babel/helpers': 7.29.7 - '@babel/parser': 7.29.7 + '@babel/parser': 7.29.8 '@babel/template': 7.29.7 - '@babel/traverse': 7.29.7 - '@babel/types': 7.29.7 + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 debug: 4.4.3 @@ -2153,10 +2296,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/generator@7.29.7': + '@babel/generator@7.29.8': dependencies: - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 @@ -2173,8 +2316,8 @@ snapshots: '@babel/helper-module-imports@7.29.7': dependencies: - '@babel/traverse': 7.29.7 - '@babel/types': 7.29.7 + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 transitivePeerDependencies: - supports-color @@ -2183,7 +2326,7 @@ snapshots: '@babel/core': 7.29.7 '@babel/helper-module-imports': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 - '@babel/traverse': 7.29.7 + '@babel/traverse': 7.29.8 transitivePeerDependencies: - supports-color @@ -2196,47 +2339,47 @@ snapshots: '@babel/helpers@7.29.7': dependencies: '@babel/template': 7.29.7 - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 - '@babel/parser@7.29.7': + '@babel/parser@7.29.8': dependencies: - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 '@babel/template@7.29.7': dependencies: '@babel/code-frame': 7.29.7 - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 - '@babel/traverse@7.29.7': + '@babel/traverse@7.29.8': dependencies: '@babel/code-frame': 7.29.7 - '@babel/generator': 7.29.7 + '@babel/generator': 7.29.8 '@babel/helper-globals': 7.29.7 - '@babel/parser': 7.29.7 + '@babel/parser': 7.29.8 '@babel/template': 7.29.7 - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 debug: 4.4.3 transitivePeerDependencies: - supports-color - '@babel/types@7.29.7': + '@babel/types@7.29.8': dependencies: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 - '@emnapi/core@1.11.1': + '@emnapi/core@2.0.0-alpha.3': dependencies: - '@emnapi/wasi-threads': 1.2.2 + '@emnapi/wasi-threads': 2.0.1 tslib: 2.8.1 optional: true - '@emnapi/runtime@1.11.1': + '@emnapi/runtime@2.0.0-alpha.3': dependencies: tslib: 2.8.1 optional: true - '@emnapi/wasi-threads@1.2.2': + '@emnapi/wasi-threads@2.0.1': dependencies: tslib: 2.8.1 optional: true @@ -2308,7 +2451,7 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} - '@internationalized/date@3.12.2': + '@internationalized/date@3.12.3': dependencies: '@swc/helpers': 0.5.23 @@ -2331,14 +2474,14 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@lucide/svelte@1.27.0(svelte@5.56.8)': + '@lucide/svelte@1.28.0(svelte@5.56.8)': dependencies: svelte: 5.56.8 - '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + '@napi-rs/wasm-runtime@1.2.2(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)': dependencies: - '@emnapi/core': 1.11.1 - '@emnapi/runtime': 1.11.1 + '@emnapi/core': 2.0.0-alpha.3 + '@emnapi/runtime': 2.0.0-alpha.3 '@tybys/wasm-util': 0.10.3 optional: true @@ -2357,7 +2500,7 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.1 '@opentelemetry/api-logs': 0.220.0 - import-in-the-middle: 3.3.2 + import-in-the-middle: 3.3.3 require-in-the-middle: 8.0.1 transitivePeerDependencies: - supports-color @@ -2385,57 +2528,57 @@ snapshots: '@opentelemetry/semantic-conventions@1.43.0': {} - '@oxc-project/types@0.139.0': {} + '@oxc-project/types@0.142.0': {} '@polka/url@1.0.0-next.29': {} - '@rolldown/binding-android-arm64@1.1.5': + '@rolldown/binding-android-arm64@1.2.1': optional: true - '@rolldown/binding-darwin-arm64@1.1.5': + '@rolldown/binding-darwin-arm64@1.2.1': optional: true - '@rolldown/binding-darwin-x64@1.1.5': + '@rolldown/binding-darwin-x64@1.2.1': optional: true - '@rolldown/binding-freebsd-x64@1.1.5': + '@rolldown/binding-freebsd-x64@1.2.1': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.1.5': + '@rolldown/binding-linux-arm-gnueabihf@1.2.1': optional: true - '@rolldown/binding-linux-arm64-gnu@1.1.5': + '@rolldown/binding-linux-arm64-gnu@1.2.1': optional: true - '@rolldown/binding-linux-arm64-musl@1.1.5': + '@rolldown/binding-linux-arm64-musl@1.2.1': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.1.5': + '@rolldown/binding-linux-ppc64-gnu@1.2.1': optional: true - '@rolldown/binding-linux-s390x-gnu@1.1.5': + '@rolldown/binding-linux-s390x-gnu@1.2.1': optional: true - '@rolldown/binding-linux-x64-gnu@1.1.5': + '@rolldown/binding-linux-x64-gnu@1.2.1': optional: true - '@rolldown/binding-linux-x64-musl@1.1.5': + '@rolldown/binding-linux-x64-musl@1.2.1': optional: true - '@rolldown/binding-openharmony-arm64@1.1.5': + '@rolldown/binding-openharmony-arm64@1.2.1': optional: true - '@rolldown/binding-wasm32-wasi@1.1.5': + '@rolldown/binding-wasm32-wasi@1.2.1': dependencies: - '@emnapi/core': 1.11.1 - '@emnapi/runtime': 1.11.1 - '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + '@emnapi/core': 2.0.0-alpha.3 + '@emnapi/runtime': 2.0.0-alpha.3 + '@napi-rs/wasm-runtime': 1.2.2(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3) optional: true - '@rolldown/binding-win32-arm64-msvc@1.1.5': + '@rolldown/binding-win32-arm64-msvc@1.2.1': optional: true - '@rolldown/binding-win32-x64-msvc@1.1.5': + '@rolldown/binding-win32-x64-msvc@1.2.1': optional: true '@rolldown/pluginutils@1.0.1': {} @@ -2558,25 +2701,25 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.62.2': optional: true - '@sentry/browser-utils@10.68.0': + '@sentry/browser-utils@10.69.0': dependencies: '@sentry/conventions': 0.16.0 - '@sentry/core': 10.68.0 + '@sentry/core': 10.69.0 - '@sentry/browser@10.68.0': + '@sentry/browser@10.69.0': dependencies: - '@sentry/browser-utils': 10.68.0 + '@sentry/browser-utils': 10.69.0 '@sentry/conventions': 0.16.0 - '@sentry/core': 10.68.0 - '@sentry/feedback': 10.68.0 - '@sentry/replay': 10.68.0 - '@sentry/replay-canvas': 10.68.0 + '@sentry/core': 10.69.0 + '@sentry/feedback': 10.69.0 + '@sentry/replay': 10.69.0 + '@sentry/replay-canvas': 10.69.0 - '@sentry/bundler-plugins@10.68.0(rollup@4.62.2)': + '@sentry/bundler-plugins@10.69.0(rollup@4.62.2)': dependencies: '@babel/core': 7.29.7 '@sentry/cli': 2.58.6 - '@sentry/core': 10.68.0 + '@sentry/core': 10.69.0 dotenv: 17.4.2 find-up: 5.0.0 glob: 13.0.6 @@ -2631,103 +2774,103 @@ snapshots: - encoding - supports-color - '@sentry/cloudflare@10.68.0': + '@sentry/cloudflare@10.69.0': dependencies: '@opentelemetry/api': 1.9.1 - '@sentry/core': 10.68.0 - '@sentry/server-utils': 10.68.0 + '@sentry/core': 10.69.0 + '@sentry/server-utils': 10.69.0 magic-string: 0.30.21 transitivePeerDependencies: - supports-color '@sentry/conventions@0.16.0': {} - '@sentry/core@10.68.0': + '@sentry/core@10.69.0': dependencies: '@sentry/conventions': 0.16.0 - '@sentry/feedback@10.68.0': + '@sentry/feedback@10.69.0': dependencies: - '@sentry/core': 10.68.0 + '@sentry/core': 10.69.0 - '@sentry/node-core@10.68.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.220.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1))': + '@sentry/node-core@10.69.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.220.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1))': dependencies: '@sentry/conventions': 0.16.0 - '@sentry/core': 10.68.0 - '@sentry/opentelemetry': 10.68.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1)) - import-in-the-middle: 3.3.2 + '@sentry/core': 10.69.0 + '@sentry/opentelemetry': 10.69.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1)) + import-in-the-middle: 3.3.3 optionalDependencies: '@opentelemetry/api': 1.9.1 '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) '@opentelemetry/instrumentation': 0.220.0(@opentelemetry/api@1.9.1) '@opentelemetry/sdk-trace-base': 2.10.0(@opentelemetry/api@1.9.1) - '@sentry/node@10.68.0(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))': + '@sentry/node@10.69.0(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))': dependencies: '@opentelemetry/api': 1.9.1 '@opentelemetry/instrumentation': 0.220.0(@opentelemetry/api@1.9.1) '@opentelemetry/sdk-trace-base': 2.10.0(@opentelemetry/api@1.9.1) '@sentry/conventions': 0.16.0 - '@sentry/core': 10.68.0 - '@sentry/node-core': 10.68.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.220.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1)) - '@sentry/opentelemetry': 10.68.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1)) - '@sentry/server-utils': 10.68.0 - import-in-the-middle: 3.3.2 + '@sentry/core': 10.69.0 + '@sentry/node-core': 10.69.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.220.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1)) + '@sentry/opentelemetry': 10.69.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1)) + '@sentry/server-utils': 10.69.0 + import-in-the-middle: 3.3.3 transitivePeerDependencies: - '@opentelemetry/core' - '@opentelemetry/exporter-trace-otlp-http' - supports-color - '@sentry/opentelemetry@10.68.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1))': + '@sentry/opentelemetry@10.69.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1))': dependencies: '@opentelemetry/api': 1.9.1 '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) '@opentelemetry/sdk-trace-base': 2.10.0(@opentelemetry/api@1.9.1) '@sentry/conventions': 0.16.0 - '@sentry/core': 10.68.0 + '@sentry/core': 10.69.0 - '@sentry/replay-canvas@10.68.0': + '@sentry/replay-canvas@10.69.0': dependencies: - '@sentry/core': 10.68.0 - '@sentry/replay': 10.68.0 + '@sentry/core': 10.69.0 + '@sentry/replay': 10.69.0 - '@sentry/replay@10.68.0': + '@sentry/replay@10.69.0': dependencies: - '@sentry/browser-utils': 10.68.0 - '@sentry/core': 10.68.0 + '@sentry/browser-utils': 10.69.0 + '@sentry/core': 10.69.0 - '@sentry/server-utils@10.68.0': + '@sentry/server-utils@10.69.0': dependencies: - '@apm-js-collab/code-transformer-bundler-plugins': 0.7.1 + '@apm-js-collab/code-transformer-bundler-plugins': 0.7.3 '@apm-js-collab/tracing-hooks': 0.13.0 '@sentry/conventions': 0.16.0 - '@sentry/core': 10.68.0 + '@sentry/core': 10.69.0 meriyah: 6.1.4 transitivePeerDependencies: - supports-color - '@sentry/svelte@10.68.0(svelte@5.56.8)': + '@sentry/svelte@10.69.0(svelte@5.56.8)': dependencies: - '@sentry/browser': 10.68.0 - '@sentry/core': 10.68.0 + '@sentry/browser': 10.69.0 + '@sentry/core': 10.69.0 magic-string: 0.30.21 svelte: 5.56.8 - '@sentry/sveltekit@10.68.0(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@sveltejs/kit@2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8)(vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0)))(svelte@5.56.8)(typescript@6.0.3)(vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0)))(rollup@4.62.2)(svelte@5.56.8)(vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0))': + '@sentry/sveltekit@10.69.0(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@sveltejs/kit@2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8)(vite@8.2.0(@types/node@25.9.5)(jiti@2.7.0)))(svelte@5.56.8)(typescript@6.0.3)(vite@8.2.0(@types/node@25.9.5)(jiti@2.7.0)))(rollup@4.62.2)(svelte@5.56.8)(vite@8.2.0(@types/node@25.9.5)(jiti@2.7.0))': dependencies: - '@sentry/cloudflare': 10.68.0 + '@sentry/cloudflare': 10.69.0 '@sentry/conventions': 0.16.0 - '@sentry/core': 10.68.0 - '@sentry/node': 10.68.0(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1)) - '@sentry/svelte': 10.68.0(svelte@5.56.8) + '@sentry/core': 10.69.0 + '@sentry/node': 10.69.0(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1)) + '@sentry/svelte': 10.69.0(svelte@5.56.8) '@sentry/vite-plugin': 5.4.0(rollup@4.62.2) - '@sveltejs/acorn-typescript': 1.0.11(acorn@8.17.0) - '@sveltejs/kit': 2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8)(vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0)))(svelte@5.56.8)(typescript@6.0.3)(vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0)) - acorn: 8.17.0 + '@sveltejs/acorn-typescript': 1.0.11(acorn@8.18.0) + '@sveltejs/kit': 2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8)(vite@8.2.0(@types/node@25.9.5)(jiti@2.7.0)))(svelte@5.56.8)(typescript@6.0.3)(vite@8.2.0(@types/node@25.9.5)(jiti@2.7.0)) + acorn: 8.18.0 magic-string: 0.30.21 sorcery: 1.0.0 optionalDependencies: - vite: 8.1.5(@types/node@25.9.5)(jiti@2.7.0) + vite: 8.2.0(@types/node@25.9.5)(jiti@2.7.0) transitivePeerDependencies: - '@cloudflare/workers-types' - '@opentelemetry/core' @@ -2741,7 +2884,7 @@ snapshots: '@sentry/vite-plugin@5.4.0(rollup@4.62.2)': dependencies: - '@sentry/bundler-plugins': 10.68.0(rollup@4.62.2) + '@sentry/bundler-plugins': 10.69.0(rollup@4.62.2) transitivePeerDependencies: - encoding - rollup @@ -2754,24 +2897,28 @@ snapshots: dependencies: acorn: 8.17.0 - '@sveltejs/adapter-node@5.5.7(@sveltejs/kit@2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8)(vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0)))(svelte@5.56.8)(typescript@6.0.3)(vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0)))': + '@sveltejs/acorn-typescript@1.0.11(acorn@8.18.0)': + dependencies: + acorn: 8.18.0 + + '@sveltejs/adapter-node@5.5.7(@sveltejs/kit@2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8)(vite@8.2.0(@types/node@25.9.5)(jiti@2.7.0)))(svelte@5.56.8)(typescript@6.0.3)(vite@8.2.0(@types/node@25.9.5)(jiti@2.7.0)))': dependencies: '@rollup/plugin-commonjs': 29.0.3(rollup@4.62.2) '@rollup/plugin-json': 6.1.0(rollup@4.62.2) '@rollup/plugin-node-resolve': 16.0.3(rollup@4.62.2) '@rollup/plugin-replace': 6.0.3(rollup@4.62.2) - '@sveltejs/kit': 2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8)(vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0)))(svelte@5.56.8)(typescript@6.0.3)(vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0)) + '@sveltejs/kit': 2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8)(vite@8.2.0(@types/node@25.9.5)(jiti@2.7.0)))(svelte@5.56.8)(typescript@6.0.3)(vite@8.2.0(@types/node@25.9.5)(jiti@2.7.0)) rollup: 4.62.2 - '@sveltejs/kit@2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8)(vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0)))(svelte@5.56.8)(typescript@6.0.3)(vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0))': + '@sveltejs/kit@2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8)(vite@8.2.0(@types/node@25.9.5)(jiti@2.7.0)))(svelte@5.56.8)(typescript@6.0.3)(vite@8.2.0(@types/node@25.9.5)(jiti@2.7.0))': dependencies: '@standard-schema/spec': 1.1.0 - '@sveltejs/acorn-typescript': 1.0.11(acorn@8.17.0) - '@sveltejs/vite-plugin-svelte': 7.2.0(svelte@5.56.8)(vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0)) + '@sveltejs/acorn-typescript': 1.0.11(acorn@8.18.0) + '@sveltejs/vite-plugin-svelte': 7.2.0(svelte@5.56.8)(vite@8.2.0(@types/node@25.9.5)(jiti@2.7.0)) '@types/cookie': 0.6.0 - acorn: 8.17.0 + acorn: 8.18.0 cookie: 0.6.0 - devalue: 5.8.2 + devalue: 5.9.0 esm-env: 1.2.2 kleur: 4.1.5 magic-string: 0.30.21 @@ -2779,21 +2926,21 @@ snapshots: set-cookie-parser: 3.1.2 sirv: 3.0.2 svelte: 5.56.8 - vite: 8.1.5(@types/node@25.9.5)(jiti@2.7.0) + vite: 8.2.0(@types/node@25.9.5)(jiti@2.7.0) optionalDependencies: '@opentelemetry/api': 1.9.1 typescript: 6.0.3 '@sveltejs/load-config@0.2.1': {} - '@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8)(vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0))': + '@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8)(vite@8.2.0(@types/node@25.9.5)(jiti@2.7.0))': dependencies: deepmerge: 4.3.1 magic-string: 0.30.21 obug: 2.1.4 svelte: 5.56.8 - vite: 8.1.5(@types/node@25.9.5)(jiti@2.7.0) - vitefu: 1.1.3(vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0)) + vite: 8.2.0(@types/node@25.9.5)(jiti@2.7.0) + vitefu: 1.1.3(vite@8.2.0(@types/node@25.9.5)(jiti@2.7.0)) '@swc/helpers@0.5.23': dependencies: @@ -2865,20 +3012,27 @@ snapshots: postcss-selector-parser: 6.0.10 tailwindcss: 4.3.3 - '@tailwindcss/vite@4.3.3(vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0))': + '@tailwindcss/vite@4.3.3(vite@8.2.0(@types/node@25.9.5)(jiti@2.7.0))': dependencies: '@tailwindcss/node': 4.3.3 '@tailwindcss/oxide': 4.3.3 tailwindcss: 4.3.3 - vite: 8.1.5(@types/node@25.9.5)(jiti@2.7.0) + vite: 8.2.0(@types/node@25.9.5)(jiti@2.7.0) '@tybys/wasm-util@0.10.3': dependencies: tslib: 2.8.1 optional: true + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + '@types/cookie@0.6.0': {} + '@types/deep-eql@4.0.2': {} + '@types/esrecurse@4.3.1': {} '@types/estree@1.0.9': {} @@ -2893,12 +3047,55 @@ snapshots: '@types/trusted-types@2.0.7': {} + '@vitest/expect@4.1.10': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + chai: 6.2.2 + tinyrainbow: 3.1.1 + + '@vitest/mocker@4.1.10(vite@8.2.0(@types/node@25.9.5)(jiti@2.7.0))': + dependencies: + '@vitest/spy': 4.1.10 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.2.0(@types/node@25.9.5)(jiti@2.7.0) + + '@vitest/pretty-format@4.1.10': + dependencies: + tinyrainbow: 3.1.1 + + '@vitest/runner@4.1.10': + dependencies: + '@vitest/utils': 4.1.10 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.10': + dependencies: + '@vitest/pretty-format': 4.1.10 + '@vitest/utils': 4.1.10 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.10': {} + + '@vitest/utils@4.1.10': + dependencies: + '@vitest/pretty-format': 4.1.10 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.1 + acorn-jsx@5.3.2(acorn@8.17.0): dependencies: acorn: 8.17.0 acorn@8.17.0: {} + acorn@8.18.0: {} + agent-base@6.0.2: dependencies: debug: 4.4.3 @@ -2914,11 +3111,13 @@ snapshots: aria-query@5.3.1: {} + assertion-error@2.0.1: {} + astring@1.9.0: {} asynckit@0.4.0: {} - axios@1.18.1(debug@4.4.3): + axios@1.19.0(debug@4.4.3): dependencies: follow-redirects: 1.16.0(debug@4.4.3) form-data: 4.0.6 @@ -2932,30 +3131,30 @@ snapshots: balanced-match@4.0.4: {} - baseline-browser-mapping@2.11.3: {} + baseline-browser-mapping@2.11.11: {} - bits-ui@2.18.1(@internationalized/date@3.12.2)(@sveltejs/kit@2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8)(vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0)))(svelte@5.56.8)(typescript@6.0.3)(vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0)))(svelte@5.56.8): + bits-ui@2.18.1(@internationalized/date@3.12.3)(@sveltejs/kit@2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8)(vite@8.2.0(@types/node@25.9.5)(jiti@2.7.0)))(svelte@5.56.8)(typescript@6.0.3)(vite@8.2.0(@types/node@25.9.5)(jiti@2.7.0)))(svelte@5.56.8): dependencies: '@floating-ui/core': 1.8.0 '@floating-ui/dom': 1.8.0 - '@internationalized/date': 3.12.2 + '@internationalized/date': 3.12.3 esm-env: 1.2.2 - runed: 0.35.1(@sveltejs/kit@2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8)(vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0)))(svelte@5.56.8)(typescript@6.0.3)(vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0)))(svelte@5.56.8) + runed: 0.35.1(@sveltejs/kit@2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8)(vite@8.2.0(@types/node@25.9.5)(jiti@2.7.0)))(svelte@5.56.8)(typescript@6.0.3)(vite@8.2.0(@types/node@25.9.5)(jiti@2.7.0)))(svelte@5.56.8) svelte: 5.56.8 - svelte-toolbelt: 0.10.6(@sveltejs/kit@2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8)(vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0)))(svelte@5.56.8)(typescript@6.0.3)(vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0)))(svelte@5.56.8) + svelte-toolbelt: 0.10.6(@sveltejs/kit@2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8)(vite@8.2.0(@types/node@25.9.5)(jiti@2.7.0)))(svelte@5.56.8)(typescript@6.0.3)(vite@8.2.0(@types/node@25.9.5)(jiti@2.7.0)))(svelte@5.56.8) tabbable: 6.5.0 transitivePeerDependencies: - '@sveltejs/kit' - brace-expansion@5.0.8: + brace-expansion@5.0.9: dependencies: balanced-match: 4.0.4 browserslist@4.28.7: dependencies: - baseline-browser-mapping: 2.11.3 + baseline-browser-mapping: 2.11.11 caniuse-lite: 1.0.30001806 - electron-to-chromium: 1.5.396 + electron-to-chromium: 1.5.399 node-releases: 2.0.51 update-browserslist-db: 1.2.3(browserslist@4.28.7) @@ -2966,6 +3165,8 @@ snapshots: caniuse-lite@1.0.30001806: {} + chai@6.2.2: {} + chokidar@4.0.3: dependencies: readdirp: 4.1.2 @@ -3008,6 +3209,8 @@ snapshots: devalue@5.8.2: {} + devalue@5.9.0: {} + dotenv@17.4.2: {} dunder-proto@1.0.1: @@ -3016,7 +3219,7 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 - electron-to-chromium@1.5.396: {} + electron-to-chromium@1.5.399: {} enhanced-resolve@5.24.3: dependencies: @@ -3151,8 +3354,14 @@ snapshots: estree-walker@2.0.2: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + esutils@2.0.3: {} + expect-type@1.4.0: {} + fast-deep-equal@3.1.3: {} fast-json-stable-stringify@2.1.0: {} @@ -3222,13 +3431,13 @@ snapshots: glob@13.0.6: dependencies: - minimatch: 10.2.5 + minimatch: 10.2.6 minipass: 7.1.3 path-scurry: 2.0.2 globals@16.5.0: {} - globals@17.7.0: {} + globals@17.9.0: {} globalyzer@0.1.0: {} @@ -3257,7 +3466,7 @@ snapshots: ignore@5.3.2: {} - import-in-the-middle@3.3.2: + import-in-the-middle@3.3.3: dependencies: cjs-module-lexer: 2.2.0 es-module-lexer: 2.3.1 @@ -3316,7 +3525,7 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - libphonenumber-js@1.13.9: {} + libphonenumber-js@1.13.10: {} lightningcss-android-arm64@1.32.0: optional: true @@ -3448,7 +3657,11 @@ snapshots: minimatch@10.2.5: dependencies: - brace-expansion: 5.0.8 + brace-expansion: 5.0.9 + + minimatch@10.2.6: + dependencies: + brace-expansion: 5.0.9 minimist@1.2.8: {} @@ -3508,6 +3721,8 @@ snapshots: lru-cache: 11.5.2 minipass: 7.1.3 + pathe@2.0.3: {} + picocolors@1.1.1: {} picomatch@4.0.5: {} @@ -3543,6 +3758,12 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postcss@8.5.25: + dependencies: + nanoid: 3.3.16 + picocolors: 1.1.1 + source-map-js: 1.2.1 + prelude-ls@1.2.1: {} prettier-plugin-svelte@4.1.1(prettier@3.9.6)(svelte@5.56.8): @@ -3582,26 +3803,26 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - rolldown@1.1.5: + rolldown@1.2.1: dependencies: - '@oxc-project/types': 0.139.0 + '@oxc-project/types': 0.142.0 '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm64': 1.1.5 - '@rolldown/binding-darwin-arm64': 1.1.5 - '@rolldown/binding-darwin-x64': 1.1.5 - '@rolldown/binding-freebsd-x64': 1.1.5 - '@rolldown/binding-linux-arm-gnueabihf': 1.1.5 - '@rolldown/binding-linux-arm64-gnu': 1.1.5 - '@rolldown/binding-linux-arm64-musl': 1.1.5 - '@rolldown/binding-linux-ppc64-gnu': 1.1.5 - '@rolldown/binding-linux-s390x-gnu': 1.1.5 - '@rolldown/binding-linux-x64-gnu': 1.1.5 - '@rolldown/binding-linux-x64-musl': 1.1.5 - '@rolldown/binding-openharmony-arm64': 1.1.5 - '@rolldown/binding-wasm32-wasi': 1.1.5 - '@rolldown/binding-win32-arm64-msvc': 1.1.5 - '@rolldown/binding-win32-x64-msvc': 1.1.5 + '@rolldown/binding-android-arm64': 1.2.1 + '@rolldown/binding-darwin-arm64': 1.2.1 + '@rolldown/binding-darwin-x64': 1.2.1 + '@rolldown/binding-freebsd-x64': 1.2.1 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.1 + '@rolldown/binding-linux-arm64-gnu': 1.2.1 + '@rolldown/binding-linux-arm64-musl': 1.2.1 + '@rolldown/binding-linux-ppc64-gnu': 1.2.1 + '@rolldown/binding-linux-s390x-gnu': 1.2.1 + '@rolldown/binding-linux-x64-gnu': 1.2.1 + '@rolldown/binding-linux-x64-musl': 1.2.1 + '@rolldown/binding-openharmony-arm64': 1.2.1 + '@rolldown/binding-wasm32-wasi': 1.2.1 + '@rolldown/binding-win32-arm64-msvc': 1.2.1 + '@rolldown/binding-win32-x64-msvc': 1.2.1 rollup@4.62.2: dependencies: @@ -3649,14 +3870,14 @@ snapshots: esm-env: 1.2.2 svelte: 5.56.8 - runed@0.35.1(@sveltejs/kit@2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8)(vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0)))(svelte@5.56.8)(typescript@6.0.3)(vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0)))(svelte@5.56.8): + runed@0.35.1(@sveltejs/kit@2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8)(vite@8.2.0(@types/node@25.9.5)(jiti@2.7.0)))(svelte@5.56.8)(typescript@6.0.3)(vite@8.2.0(@types/node@25.9.5)(jiti@2.7.0)))(svelte@5.56.8): dependencies: dequal: 2.0.3 esm-env: 1.2.2 lz-string: 1.5.0 svelte: 5.56.8 optionalDependencies: - '@sveltejs/kit': 2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8)(vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0)))(svelte@5.56.8)(typescript@6.0.3)(vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0)) + '@sveltejs/kit': 2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8)(vite@8.2.0(@types/node@25.9.5)(jiti@2.7.0)))(svelte@5.56.8)(typescript@6.0.3)(vite@8.2.0(@types/node@25.9.5)(jiti@2.7.0)) sade@1.8.1: dependencies: @@ -3676,6 +3897,8 @@ snapshots: shebang-regex@3.0.0: {} + siginfo@2.0.0: {} + sirv@3.0.2: dependencies: '@polka/url': 1.0.0-next.29 @@ -3692,13 +3915,17 @@ snapshots: source-map@0.6.1: {} + stackback@0.0.2: {} + + std-env@4.2.0: {} + style-to-object@1.0.14: dependencies: inline-style-parser: 0.2.7 supports-preserve-symlinks-flag@1.0.0: {} - svelte-check@4.7.3(picomatch@4.0.5)(svelte@5.56.8)(typescript@6.0.3): + svelte-check@4.7.4(picomatch@4.0.5)(svelte@5.56.8)(typescript@6.0.3): dependencies: '@jridgewell/trace-mapping': 0.3.31 '@sveltejs/load-config': 0.2.1 @@ -3711,7 +3938,7 @@ snapshots: transitivePeerDependencies: - picomatch - svelte-dnd-action@0.9.74(svelte@5.56.8): + svelte-dnd-action@0.9.77(svelte@5.56.8): dependencies: svelte: 5.56.8 @@ -3732,10 +3959,10 @@ snapshots: runed: 0.28.0(svelte@5.56.8) svelte: 5.56.8 - svelte-toolbelt@0.10.6(@sveltejs/kit@2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8)(vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0)))(svelte@5.56.8)(typescript@6.0.3)(vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0)))(svelte@5.56.8): + svelte-toolbelt@0.10.6(@sveltejs/kit@2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8)(vite@8.2.0(@types/node@25.9.5)(jiti@2.7.0)))(svelte@5.56.8)(typescript@6.0.3)(vite@8.2.0(@types/node@25.9.5)(jiti@2.7.0)))(svelte@5.56.8): dependencies: clsx: 2.1.1 - runed: 0.35.1(@sveltejs/kit@2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8)(vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0)))(svelte@5.56.8)(typescript@6.0.3)(vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0)))(svelte@5.56.8) + runed: 0.35.1(@sveltejs/kit@2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8)(vite@8.2.0(@types/node@25.9.5)(jiti@2.7.0)))(svelte@5.56.8)(typescript@6.0.3)(vite@8.2.0(@types/node@25.9.5)(jiti@2.7.0)))(svelte@5.56.8) style-to-object: 1.0.14 svelte: 5.56.8 transitivePeerDependencies: @@ -3773,11 +4000,10 @@ snapshots: tailwind-merge@3.6.0: {} - tailwind-variants@3.2.2(tailwind-merge@3.6.0)(tailwindcss@4.3.3): - dependencies: - tailwindcss: 4.3.3 + tailwind-variants@3.3.0(tailwind-merge@3.6.0)(tailwindcss@4.3.3): optionalDependencies: tailwind-merge: 3.6.0 + tailwindcss: 4.3.3 tailwindcss@4.3.3: {} @@ -3788,11 +4014,17 @@ snapshots: globalyzer: 0.1.0 globrex: 0.1.2 + tinybench@2.9.0: {} + + tinyexec@1.3.0: {} + tinyglobby@0.2.17: dependencies: fdir: 6.5.0(picomatch@4.0.5) picomatch: 4.0.5 + tinyrainbow@3.1.1: {} + totalist@3.0.1: {} tr46@0.0.3: {} @@ -3821,21 +4053,49 @@ snapshots: util-deprecate@1.0.2: {} - vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0): + vite@8.2.0(@types/node@25.9.5)(jiti@2.7.0): dependencies: lightningcss: 1.33.0 picomatch: 4.0.5 - postcss: 8.5.23 - rolldown: 1.1.5 + postcss: 8.5.25 + rolldown: 1.2.1 tinyglobby: 0.2.17 optionalDependencies: '@types/node': 25.9.5 fsevents: 2.3.3 jiti: 2.7.0 - vitefu@1.1.3(vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0)): + vitefu@1.1.3(vite@8.2.0(@types/node@25.9.5)(jiti@2.7.0)): optionalDependencies: - vite: 8.1.5(@types/node@25.9.5)(jiti@2.7.0) + vite: 8.2.0(@types/node@25.9.5)(jiti@2.7.0) + + vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@25.9.5)(vite@8.2.0(@types/node@25.9.5)(jiti@2.7.0)): + dependencies: + '@vitest/expect': 4.1.10 + '@vitest/mocker': 4.1.10(vite@8.2.0(@types/node@25.9.5)(jiti@2.7.0)) + '@vitest/pretty-format': 4.1.10 + '@vitest/runner': 4.1.10 + '@vitest/snapshot': 4.1.10 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + es-module-lexer: 2.3.1 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.1.4 + pathe: 2.0.3 + picomatch: 4.0.5 + std-env: 4.2.0 + tinybench: 2.9.0 + tinyexec: 1.3.0 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.1 + vite: 8.2.0(@types/node@25.9.5)(jiti@2.7.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@opentelemetry/api': 1.9.1 + '@types/node': 25.9.5 + transitivePeerDependencies: + - msw webidl-conversions@3.0.1: {} @@ -3848,6 +4108,11 @@ snapshots: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + word-wrap@1.2.5: {} yallist@3.1.1: {} diff --git a/frontend/pnpm-workspace.yaml b/frontend/pnpm-workspace.yaml index 5a01799a..e97ab424 100644 --- a/frontend/pnpm-workspace.yaml +++ b/frontend/pnpm-workspace.yaml @@ -2,3 +2,8 @@ allowBuilds: '@sentry/cli': true onlyBuiltDependencies: - '@sentry/cli' +overrides: + # GHSA-rgw5-rvv9-x895 (high): DoS via unbounded intermediate arrays. + # Only reachable through dev tooling (eslint, @sentry/vite-plugin), but the + # transitive parents still ask for a vulnerable range, so force the patch. + 'brace-expansion@>=4.0.0 <5.0.9': '^5.0.9' diff --git a/frontend/src/lib/server/v2/business-hours.js b/frontend/src/lib/server/v2/business-hours.js index 77eea31b..9819058f 100644 --- a/frontend/src/lib/server/v2/business-hours.js +++ b/frontend/src/lib/server/v2/business-hours.js @@ -12,11 +12,27 @@ * so fold the flat fields into that array here and trim the seconds. Holidays * already arrive as `{ id, date, name }`. * - * Read-only page: "Edit hours" / "Add holiday" are deferred builders (the - * backend has admin PUT/POST/DELETE endpoints, but no form is wired yet), so - * there is no write path here. + * WRITE PATHS, AND WHO MAY USE THEM + * `updateBusinessHours` runs the fold above backwards: `unfoldDays` turns the + * page's `days` array back into the fourteen flat fields, Monday-first, null + * for a closed day. It PUTs the calendar's OWN url, `/business-hours/calendar/ + * /`, not the collection path this module's read uses; `BusinessCalendarView.put` + * is `partial=True` and admin-only, and `is_default` is read-only on + * `BusinessCalendarSerializer`, so it is never part of the outgoing body. + * `addHoliday` and `removeHoliday` write the holiday sub-resource, + * `/business-hours/calendar//holidays/` and its `/` detail, also + * admin-only. A holiday add is idempotent on date: posting a date already on + * the calendar returns the existing row with 200, not 201, and creates + * nothing new; that is a normal outcome, not a failure to surface. A holiday + * removal is a hard delete, the row is gone and the day counts as working + * time again immediately. All three fail with 403 and + * `{"error": "Only admins can update business hours."}` for a non-admin; + * `can_edit` below only decides whether the edit controls are offered, since + * the backend re-derives admin status from the request's own profile and is + * what actually refuses. */ import { apiRequest } from '$lib/api-helpers.js'; +import { viewerRole } from './organization.js'; /** Monday-first weekday label ↔ the model's field prefix. */ const WEEKDAYS = [ @@ -36,7 +52,7 @@ function toHm(value) { /** * @param {{ cookies: import('@sveltejs/kit').Cookies }} event - * @returns {Promise<{ calendar: any }>} + * @returns {Promise<{ calendar: any, can_edit: boolean }>} */ export async function getBusinessHours({ cookies }) { const cal = await apiRequest('/business-hours/calendar/', {}, { cookies }); @@ -56,6 +72,109 @@ export async function getBusinessHours({ cookies }) { name: h.name, date: h.date })) - } + }, + // A display hint decoded from the JWT `role` claim, deciding only whether + // the page offers "Edit hours" / "Add holiday". `BusinessCalendarView.put` + // and the holiday views re-derive admin status from `request.profile` + // server-side, and that is what actually refuses a non-admin's write. + can_edit: viewerRole(cookies) === 'ADMIN' }; } + +/** + * Fold the page's `days` array back into the model's fourteen flat fields. + * + * `getBusinessHours` above turns `monday_open` and the rest into + * `days: [{ day, open, close }]` with "HH:MM" times; this is that in + * reverse. A closed day is both fields null. One field null and the other + * set stores a half-open day that the SLA clock reads as nonsense. + * `BusinessCalendarSerializer.validate()` is the real backstop: it already + * refuses a half-open day with a 400 (`(open_val is None) != (close_val is + * None)`). This client-side check exists only to name the day in the + * message, instead of surfacing the serializer's generic per-field error. + * + * @param {{ day: string, open: string | null, close: string | null }[]} days + */ +function unfoldDays(days) { + /** @type {Record} */ + const flat = {}; + for (const [label, key] of WEEKDAYS) { + const entry = days.find((d) => d.day === label); + const open = entry?.open || null; + const close = entry?.close || null; + if (Boolean(open) !== Boolean(close)) { + throw new Error(`${label} needs both an opening and a closing time, or neither.`); + } + flat[`${key}_open`] = open; + flat[`${key}_close`] = close; + } + return flat; +} + +/** + * Save the week plus the calendar's name and timezone. + * + * PUTs the calendar's own detail url, not the collection path + * `getBusinessHours` reads from; `calendar.id` survives that read's reshape, + * so the caller always has it. `is_default` is read-only on + * `BusinessCalendarSerializer` and is deliberately never part of the body. + * + * @param {{ cookies: import('@sveltejs/kit').Cookies }} event + * @param {string} calendarId + * @param {{ day: string, open: string | null, close: string | null }[]} days + * @param {{ name: string, timezone: string, [key: string]: unknown }} meta caller may pass + * extra fields (e.g. `is_default`, read off the loaded calendar); only + * `name` and `timezone` are ever read out of it + */ +export async function updateBusinessHours({ cookies }, calendarId, days, meta) { + if (!calendarId) throw new Error('Which calendar? No calendar id was given.'); + const body = { ...unfoldDays(days), name: meta.name, timezone: meta.timezone }; + return await apiRequest( + `/business-hours/calendar/${calendarId}/`, + { method: 'PUT', body }, + { cookies } + ); +} + +/** + * Add a holiday to a calendar. + * + * `POST /business-hours/calendar//holidays/` is idempotent on `date`: + * adding a date already on this calendar returns the existing row with + * HTTP 200, not 201, and creates no duplicate. That is a normal outcome, not + * an error, so callers should not treat it as one and this module does not + * pre-check for an existing date on the client. + * + * @param {{ cookies: import('@sveltejs/kit').Cookies }} event + * @param {string} calendarId + * @param {{ date: string, name: string }} values + */ +export async function addHoliday({ cookies }, calendarId, values) { + if (!values?.date) throw new Error('A holiday needs a date.'); + const body = { date: values.date, name: values.name }; + return await apiRequest( + `/business-hours/calendar/${calendarId}/holidays/`, + { method: 'POST', body }, + { cookies } + ); +} + +/** + * Remove a holiday. + * + * `BusinessHolidayDetailView.delete` is a hard delete: the row is gone, not + * flagged off, and the day it named counts as working time again as soon as + * the request succeeds. + * + * @param {{ cookies: import('@sveltejs/kit').Cookies }} event + * @param {string} calendarId + * @param {string} holidayId + */ +export async function removeHoliday({ cookies }, calendarId, holidayId) { + if (!holidayId) throw new Error('Which holiday? No holiday id was given.'); + return await apiRequest( + `/business-hours/calendar/${calendarId}/holidays/${holidayId}/`, + { method: 'DELETE' }, + { cookies } + ); +} diff --git a/frontend/src/lib/server/v2/business-hours.test.js b/frontend/src/lib/server/v2/business-hours.test.js new file mode 100644 index 00000000..04edcc43 --- /dev/null +++ b/frontend/src/lib/server/v2/business-hours.test.js @@ -0,0 +1,104 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +const apiRequest = vi.fn(); +vi.mock('$lib/api-helpers.js', () => ({ apiRequest: (...a) => apiRequest(...a) })); + +const { updateBusinessHours, addHoliday, removeHoliday } = + await import('$lib/server/v2/business-hours.js'); + +const event = /** @type {any} */ ({ cookies: { get: () => 'token' } }); + +const week = [ + { day: 'Monday', open: '09:00', close: '17:00' }, + { day: 'Tuesday', open: '09:00', close: '17:00' }, + { day: 'Wednesday', open: '09:00', close: '17:00' }, + { day: 'Thursday', open: '09:00', close: '17:00' }, + { day: 'Friday', open: '09:00', close: '17:00' }, + { day: 'Saturday', open: null, close: null }, + { day: 'Sunday', open: null, close: null } +]; + +describe('updateBusinessHours', () => { + beforeEach(() => { + apiRequest.mockReset(); + }); + + it('unfolds the days array back into the fourteen flat fields', async () => { + apiRequest.mockResolvedValue({}); + + await updateBusinessHours(event, 'cal1', week, { name: 'Support', timezone: 'Asia/Kolkata' }); + + const [endpoint, options] = apiRequest.mock.calls[0]; + // The detail URL, not the collection path the read uses. + expect(endpoint).toBe('/business-hours/calendar/cal1/'); + expect(options.method).toBe('PUT'); + expect(options.body.monday_open).toBe('09:00'); + expect(options.body.friday_close).toBe('17:00'); + // A closed day is both fields null, never one of them. + expect(options.body.saturday_open).toBeNull(); + expect(options.body.saturday_close).toBeNull(); + expect(options.body.name).toBe('Support'); + expect(options.body.timezone).toBe('Asia/Kolkata'); + }); + + it('never sends is_default, which the serializer marks read-only', async () => { + apiRequest.mockResolvedValue({}); + await updateBusinessHours(event, 'cal1', week, { + name: 'Support', + timezone: 'UTC', + is_default: false + }); + expect(apiRequest.mock.calls[0][1].body.is_default).toBeUndefined(); + }); + + it('rejects a day with an open but no close, naming the day', async () => { + const broken = week.map((d) => (d.day === 'Wednesday' ? { ...d, close: null } : d)); + await expect( + updateBusinessHours(event, 'cal1', broken, { name: 'S', timezone: 'UTC' }) + ).rejects.toThrow(/Wednesday/); + expect(apiRequest).not.toHaveBeenCalled(); + }); + + it('refuses an empty calendar id before making a request', async () => { + await expect( + updateBusinessHours(event, '', week, { name: 'S', timezone: 'UTC' }) + ).rejects.toThrow(/calendar/i); + expect(apiRequest).not.toHaveBeenCalled(); + }); +}); + +describe('addHoliday', () => { + beforeEach(() => { + apiRequest.mockReset(); + }); + + it('POSTs date and name to the calendar sub-resource', async () => { + apiRequest.mockResolvedValue({ id: 'h1', date: '2026-12-25', name: 'Christmas' }); + + await addHoliday(event, 'cal1', { date: '2026-12-25', name: 'Christmas' }); + + const [endpoint, options] = apiRequest.mock.calls[0]; + expect(endpoint).toBe('/business-hours/calendar/cal1/holidays/'); + expect(options.method).toBe('POST'); + expect(options.body).toEqual({ date: '2026-12-25', name: 'Christmas' }); + }); + + it('rejects a missing date before making a request', async () => { + await expect(addHoliday(event, 'cal1', { date: '', name: 'X' })).rejects.toThrow(/date/i); + expect(apiRequest).not.toHaveBeenCalled(); + }); +}); + +describe('removeHoliday', () => { + beforeEach(() => { + apiRequest.mockReset(); + }); + + it('DELETEs the holiday under its calendar', async () => { + apiRequest.mockResolvedValue(null); + await removeHoliday(event, 'cal1', 'h1'); + const [endpoint, options] = apiRequest.mock.calls[0]; + expect(endpoint).toBe('/business-hours/calendar/cal1/holidays/h1/'); + expect(options.method).toBe('DELETE'); + }); +}); diff --git a/frontend/src/lib/server/v2/custom-fields.js b/frontend/src/lib/server/v2/custom-fields.js index 9c6207fa..489fdb59 100644 --- a/frontend/src/lib/server/v2/custom-fields.js +++ b/frontend/src/lib/server/v2/custom-fields.js @@ -8,15 +8,26 @@ * `totals` block { count, active, models_extended, required_with_gaps } for the * stat cards. Both are org-scoped server-side; the page never counts records. * - * Read-only page: "New field" is a deferred builder, and the rows have no - * inline edit/toggle controls. No write path here. The page groups by - * target_model and orders by display_order itself. + * Create, edit, turn off and turn back on are all wired below. `key`, + * `target_model` and `field_type` are only ever sent on create: values live in + * each record's `custom_fields` JSON keyed by `key` (see the module docstring + * in `backend/common/custom_fields.py`), so changing `key` orphans them, + * changing `target_model` leaves them on the old entity, and changing + * `field_type` reinterprets values written under the old one. + * `CustomFieldDefinitionSerializer.validate()` freezes all three server-side + * too; this module just avoids offering the doomed edit in the first place. + * "Turn off" is `DELETE /custom-fields//`, a soft delete + * (`CustomFieldDefinitionDetailView.delete` flips `is_active`, never removes + * the row or the values on records that already carry it). "Turn on" is the + * same `updateCustomField` PUT with only `is_active: true`, since the + * detail view has no separate reactivate endpoint. */ import { apiRequest } from '$lib/api-helpers.js'; +import { viewerRole } from './organization.js'; /** * @param {{ cookies: import('@sveltejs/kit').Cookies }} event - * @returns {Promise<{ fields: any[], totals: any }>} + * @returns {Promise<{ fields: any[], totals: any, can_edit: boolean }>} */ export async function getCustomFields({ cookies }) { const resp = await apiRequest('/custom-fields/', {}, { cookies }); @@ -27,6 +38,113 @@ export async function getCustomFields({ cookies }) { active: 0, models_extended: 0, required_with_gaps: 0 - } + }, + can_edit: viewerRole(cookies) === 'ADMIN' }; } + +/** The fields the serializer accepts on create. `org` is set from the JWT + * server-side and `records_missing_value` is computed, so neither appears. */ +const CREATE_FIELDS = [ + 'target_model', + 'key', + 'label', + 'field_type', + 'is_required', + 'is_filterable', + 'display_order', + 'is_active' +]; + +/** Edit omits the three fields `CustomFieldDefinitionSerializer.validate()` + * freezes after creation. Values live in each record's `custom_fields` JSON + * keyed by `key` (see `backend/common/custom_fields.py`): changing `key` + * orphans them, changing `target_model` leaves them on the old entity, and + * changing `field_type` reinterprets values written under the old one. The + * backend refuses all three with a 400; not offering them is better than + * teaching it through a failed save. */ +const FROZEN_AFTER_CREATE = ['key', 'target_model', 'field_type']; +const UPDATE_FIELDS = CREATE_FIELDS.filter((f) => !FROZEN_AFTER_CREATE.includes(f)); + +/** "Very High" to "very-high". Only ever applied to a NEW dropdown option: an + * existing option's value is stored on every record that uses it and must + * survive a relabel untouched. */ +function slugifyOptionValue(label) { + return label + .toLowerCase() + .trim() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, ''); +} + +/** + * Shape the request body from an allow-list. + * + * @param {string[]} allowed + * @param {{ [key: string]: any }} values + */ +function buildBody(allowed, values) { + /** @type {Record} */ + const body = {}; + for (const field of allowed) { + if (values[field] === undefined) continue; + body[field] = values[field]; + } + if (body.display_order !== undefined) body.display_order = Number(body.display_order) || 0; + for (const flag of ['is_required', 'is_filterable', 'is_active']) { + if (body[flag] !== undefined) body[flag] = Boolean(body[flag]); + } + + // `options` is only legal on a dropdown. `validate_definition_options` + // rejects a non-empty options list on any other type, so this is not + // defensive noise: sending it turns a valid text field into a 400. + if (values.field_type === 'dropdown') { + const rows = (values.options ?? []) + .map((/** @type {any} */ o) => ({ + value: o.value ? String(o.value) : slugifyOptionValue(String(o.label ?? '')), + label: String(o.label ?? '').trim() + })) + .filter((/** @type {any} */ o) => o.label && o.value); + if (!rows.length) { + throw new Error('A dropdown needs at least one option.'); + } + const seen = new Set(); + for (const row of rows) { + if (seen.has(row.value)) { + throw new Error(`Two options would both be stored as "${row.value}". Rename one.`); + } + seen.add(row.value); + } + body.options = rows; + } + + return body; +} + +/** @param {{ cookies: import('@sveltejs/kit').Cookies }} event */ +export async function createCustomField({ cookies }, values) { + const body = buildBody(CREATE_FIELDS, values); + return await apiRequest('/custom-fields/', { method: 'POST', body }, { cookies }); +} + +/** @param {{ cookies: import('@sveltejs/kit').Cookies }} event */ +export async function updateCustomField({ cookies }, id, values) { + if (!id) throw new Error('Which field? No field id was given.'); + const body = buildBody(UPDATE_FIELDS, values); + return await apiRequest(`/custom-fields/${id}/`, { method: 'PUT', body }, { cookies }); +} + +/** + * Turn a field off. + * + * `CustomFieldDefinitionDetailView.delete` is a soft delete: it flips + * `is_active` and leaves every stored value readable. The UI says "Turn off" + * for that reason; calling it Delete would promise something the backend does + * not do. + * + * @param {{ cookies: import('@sveltejs/kit').Cookies }} event + */ +export async function deactivateCustomField({ cookies }, id) { + if (!id) throw new Error('Which field? No field id was given.'); + return await apiRequest(`/custom-fields/${id}/`, { method: 'DELETE' }, { cookies }); +} diff --git a/frontend/src/lib/server/v2/custom-fields.test.js b/frontend/src/lib/server/v2/custom-fields.test.js new file mode 100644 index 00000000..981e634f --- /dev/null +++ b/frontend/src/lib/server/v2/custom-fields.test.js @@ -0,0 +1,174 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +const apiRequest = vi.fn(); +vi.mock('$lib/api-helpers.js', () => ({ apiRequest: (...a) => apiRequest(...a) })); + +const { createCustomField, updateCustomField, deactivateCustomField } = + await import('$lib/server/v2/custom-fields.js'); + +const event = /** @type {any} */ ({ cookies: { get: () => 'token' } }); + +const base = { + target_model: 'Case', + key: 'severity', + label: 'Severity', + field_type: 'text', + is_required: false, + is_filterable: true, + display_order: 3 +}; + +describe('createCustomField', () => { + beforeEach(() => { + apiRequest.mockReset(); + }); + + it('POSTs the definition to /custom-fields/', async () => { + apiRequest.mockResolvedValue({ id: 'cf1', ...base }); + + const result = await createCustomField(event, base); + + const [endpoint, options] = apiRequest.mock.calls[0]; + expect(endpoint).toBe('/custom-fields/'); + expect(options.method).toBe('POST'); + expect(options.body.key).toBe('severity'); + expect(options.body.target_model).toBe('Case'); + expect(options.body.display_order).toBe(3); + expect(result.id).toBe('cf1'); + }); + + it('omits options entirely for a non-dropdown field', async () => { + // `validate_definition_options` rejects a non-empty `options` on any type + // other than dropdown, so sending [] or null on a text field is not + // harmless noise, it is the difference between 201 and 400. + apiRequest.mockResolvedValue({}); + + await createCustomField(event, { + ...base, + field_type: 'text', + options: [{ value: 'a', label: 'A' }] + }); + + expect(apiRequest.mock.calls[0][1].body.options).toBeUndefined(); + }); + + it('sends dropdown options as {value,label} pairs', async () => { + apiRequest.mockResolvedValue({}); + + await createCustomField(event, { + ...base, + field_type: 'dropdown', + options: [ + { value: '', label: 'Very High' }, + { value: 'low', label: 'Low' } + ] + }); + + // A blank value is a new row; the label is slugified into one. An + // existing value is preserved verbatim, because the stored values on + // every record reference it. + expect(apiRequest.mock.calls[0][1].body.options).toEqual([ + { value: 'very-high', label: 'Very High' }, + { value: 'low', label: 'Low' } + ]); + }); + + it('rejects a dropdown with no options before making a request', async () => { + await expect( + createCustomField(event, { ...base, field_type: 'dropdown', options: [] }) + ).rejects.toThrow(/at least one option/i); + expect(apiRequest).not.toHaveBeenCalled(); + }); + + it('never forwards a client-supplied org', async () => { + apiRequest.mockResolvedValue({}); + await createCustomField(event, { ...base, org: 'attacker-org' }); + expect(apiRequest.mock.calls[0][1].body.org).toBeUndefined(); + }); +}); + +describe('updateCustomField', () => { + beforeEach(() => { + apiRequest.mockReset(); + }); + + it('PUTs to the detail endpoint without key, target_model or field_type', async () => { + // Values live on each record keyed by `key`, and every stored value for a + // field was written and checked against its `field_type`. + // `CustomFieldDefinitionSerializer.validate()` freezes all three + // server-side (a differing value in the PUT body is a 400); this layer is + // what refuses to send any of them in the first place. `field_type` here + // changes to `number`, not `dropdown`, on purpose: a `dropdown` value + // would route through the options-shaping branch, which is a different + // behaviour covered below. + apiRequest.mockResolvedValue({}); + + await updateCustomField(event, 'cf1', { + ...base, + key: 'renamed', + target_model: 'Lead', + field_type: 'number' + }); + + const [endpoint, options] = apiRequest.mock.calls[0]; + expect(endpoint).toBe('/custom-fields/cf1/'); + expect(options.method).toBe('PUT'); + expect(options.body.key).toBeUndefined(); + expect(options.body.target_model).toBeUndefined(); + expect(options.body.field_type).toBeUndefined(); + expect(options.body.label).toBe('Severity'); + }); + + it('still attaches options when editing an existing dropdown field', async () => { + // `field_type` never reaches the outgoing body (it's frozen), but the + // caller still has to say "this definition is a dropdown" so `buildBody` + // knows to shape and attach `options`. Losing that signal on edit would + // silently drop a dropdown's choices from every save. + apiRequest.mockResolvedValue({}); + + await updateCustomField(event, 'cf1', { + ...base, + field_type: 'dropdown', + options: [{ value: 'low', label: 'Low' }] + }); + + const [, options] = apiRequest.mock.calls[0]; + expect(options.body.field_type).toBeUndefined(); + expect(options.body.options).toEqual([{ value: 'low', label: 'Low' }]); + }); + + it('supports a minimal reactivation body of just is_active', async () => { + // The "Turn on" control posts only an id; its action calls this with + // `{ is_active: true }` and nothing else, deliberately bypassing the + // full-form field reader so a field the form never submitted can't read + // back as an empty string and blank something out (see the `activate` + // action's comment in +page.server.js). + apiRequest.mockResolvedValue({}); + + await updateCustomField(event, 'cf1', { is_active: true }); + + const [, options] = apiRequest.mock.calls[0]; + expect(options.body).toEqual({ is_active: true }); + }); +}); + +describe('deactivateCustomField', () => { + beforeEach(() => { + apiRequest.mockReset(); + }); + + it('DELETEs the detail endpoint', async () => { + apiRequest.mockResolvedValue({ error: false, message: 'Custom field deactivated' }); + + await deactivateCustomField(event, 'cf1'); + + const [endpoint, options] = apiRequest.mock.calls[0]; + expect(endpoint).toBe('/custom-fields/cf1/'); + expect(options.method).toBe('DELETE'); + }); + + it('refuses an empty id before making a request', async () => { + await expect(deactivateCustomField(event, '')).rejects.toThrow(/which/i); + expect(apiRequest).not.toHaveBeenCalled(); + }); +}); diff --git a/frontend/src/lib/server/v2/documents.js b/frontend/src/lib/server/v2/documents.js index ac80c42b..ba04fbd4 100644 --- a/frontend/src/lib/server/v2/documents.js +++ b/frontend/src/lib/server/v2/documents.js @@ -49,6 +49,7 @@ */ import { apiRequest } from '$lib/api-helpers.js'; import { env } from '$env/dynamic/public'; +import { getOrgPeopleAndTeams } from './org-people.js'; const API_BASE_URL = `${env.PUBLIC_DJANGO_API_URL}/api`; @@ -173,32 +174,6 @@ export async function listDocuments({ cookies }) { return { results, totals: computeTotals(results) }; } -/** - * In-org people and teams for the share pickers. - * `/users/get-teams-and-users/` filters both to `request.profile.org`, so the - * picker can only ever offer in-org targets, the same boundary the upload/edit - * view enforces when it filters `shared_to`/`teams` by org, surfaced early as - * UX. - * - * @param {import('@sveltejs/kit').Cookies} cookies - */ -async function shareOptions(cookies) { - try { - const resp = await apiRequest('/users/get-teams-and-users/', {}, { cookies }); - const people = (resp?.profiles ?? []).map((/** @type {any} */ p) => ({ - id: p.id, - name: personName(p) - })); - const teams = (resp?.teams ?? []).map((/** @type {any} */ t) => ({ - id: t.id, - name: t.name - })); - return { people, teams }; - } catch { - return { people: [], teams: [] }; - } -} - /** * The share pickers for the upload form. * @@ -212,7 +187,7 @@ async function shareOptions(cookies) { * @param {{ cookies: import('@sveltejs/kit').Cookies }} event */ export async function getUploadOptions({ cookies }) { - const { people, teams } = await shareOptions(cookies); + const { people, teams } = await getOrgPeopleAndTeams(cookies); return { people, teams }; } @@ -233,7 +208,7 @@ export async function getDocumentForEdit({ cookies }, id) { if (!doc.can_write) return { can_edit: false }; - const { people, teams } = await shareOptions(cookies); + const { people, teams } = await getOrgPeopleAndTeams(cookies); return { can_edit: true, people, diff --git a/frontend/src/lib/server/v2/escalation.js b/frontend/src/lib/server/v2/escalation.js index 0797a453..6e5cb5d0 100644 --- a/frontend/src/lib/server/v2/escalation.js +++ b/frontend/src/lib/server/v2/escalation.js @@ -3,19 +3,27 @@ * * Server-only. Reads `GET /cases/escalation-policies/`, at most one row per * priority. Each row carries the policy's two halves (first_response / - * resolution action + target), the team to notify, and. The reason the page - * exists; `breaches_last_30d`, a server-side count of SLA breaches per + * resolution action + target), the team to notify, and the reason the page + * exists: `breaches_last_30d`, a server-side count of SLA breaches per * priority over the last 30 days. That count is condition-based, not sourced * from the escalation event log, so it still surfaces breaches under a dead * policy (off, or reassigning to nobody), which is the page's whole point. * - * Read-only page: "Edit policy" is a deferred builder, so there is no write - * path to wire here. The backend returns targets as full profile objects - * (`user_details.name`) and the team as a full team object; the page only wants - * `{ id, name }`, so flatten to that here rather than teach the component the - * backend's nesting. + * The backend returns targets as full profile objects (`user_details.name`) + * and the team as a full team object; the page only wants `{ id, name }`, so + * flatten to that here rather than teach the component the backend's nesting. + * The same flattened shape is what the edit form uses to preselect a target. + * + * Create, edit, turn off, turn on and delete are wired below. The picker + * lists (`people`, `teams`) are NOT fetched here: `settings.js`'s + * `getSettingsHub` calls `getEscalationPolicies` and reads only `.policies` + * off it, so a picker fetch in this function would be a redundant request on + * every settings-hub load. The route's `load` fetches `getOrgPeopleAndTeams` + * itself, in parallel with this. */ import { apiRequest } from '$lib/api-helpers.js'; +import { ESCALATION_PRIORITIES } from '$lib/v2/enums.js'; +import { viewerRole } from './organization.js'; /** Profile → the `{ id, name }` the card renders, or null when unset. */ function shapeTarget(target) { @@ -31,7 +39,7 @@ function shapeTeam(team) { /** * @param {{ cookies: import('@sveltejs/kit').Cookies }} event - * @returns {Promise<{ policies: any[] }>} + * @returns {Promise<{ policies: any[], can_edit: boolean }>} */ export async function getEscalationPolicies({ cookies }) { const { policies } = await apiRequest('/cases/escalation-policies/', {}, { cookies }); @@ -46,6 +54,75 @@ export async function getEscalationPolicies({ cookies }) { resolution_target: shapeTarget(p.resolution_target), notify_team: shapeTeam(p.notify_team), breaches_last_30d: p.breaches_last_30d ?? { first_response: 0, resolution: 0 } - })) + })), + // A display hint: POST/PUT/DELETE on `/cases/escalation-policies/` each + // start with `_is_admin(request.profile)` server-side and 403 regardless + // of what this says. This only decides whether the page offers the + // controls. + can_edit: viewerRole(cookies) === 'ADMIN' }; } + +export const CREATE_FIELDS = [ + 'priority', + 'first_response_action', + 'resolution_action', + 'first_response_target_id', + 'resolution_target_id', + 'notify_team_id', + 'is_active' +]; + +/** `priority` is the natural key and is frozen after create. It is not merely + * unwise to change: `EscalationPolicyDetailView.put` deletes the key from the + * body before the serializer sees it, so a PUT carrying a new priority + * returns 200 and changes nothing. Omitting it here means the page never + * offers a control whose success is a lie. */ +export const UPDATE_FIELDS = CREATE_FIELDS.filter((f) => f !== 'priority'); + +/** @param {string[]} allowed @param {{ [key: string]: any }} values */ +function buildBody(allowed, values) { + /** @type {Record} */ + const body = {}; + for (const field of allowed) { + if (values[field] === undefined) continue; + body[field] = values[field]; + } + for (const nullable of ['first_response_target_id', 'resolution_target_id', 'notify_team_id']) { + if (body[nullable] === '') body[nullable] = null; + } + if (body.is_active !== undefined) body.is_active = Boolean(body.is_active); + return body; +} + +/** @param {{ cookies: import('@sveltejs/kit').Cookies }} event */ +export async function createEscalationPolicy({ cookies }, values) { + const body = buildBody(CREATE_FIELDS, values); + if (!ESCALATION_PRIORITIES.includes(body.priority)) { + throw new Error('Pick a priority for the policy.'); + } + return await apiRequest('/cases/escalation-policies/', { method: 'POST', body }, { cookies }); +} + +/** @param {{ cookies: import('@sveltejs/kit').Cookies }} event */ +export async function updateEscalationPolicy({ cookies }, id, values) { + if (!id) throw new Error('Which policy? No policy id was given.'); + const body = buildBody(UPDATE_FIELDS, values); + return await apiRequest( + `/cases/escalation-policies/${id}/`, + { method: 'PUT', body }, + { cookies } + ); +} + +/** + * Delete a policy, permanently. `EscalationPolicyDetailView.delete` calls + * `obj.delete()`. The priority it covered then escalates to nobody, which is + * what the confirm line has to say. + * + * @param {{ cookies: import('@sveltejs/kit').Cookies }} event + */ +export async function deleteEscalationPolicy({ cookies }, id) { + if (!id) throw new Error('Which policy? No policy id was given.'); + return await apiRequest(`/cases/escalation-policies/${id}/`, { method: 'DELETE' }, { cookies }); +} diff --git a/frontend/src/lib/server/v2/escalation.test.js b/frontend/src/lib/server/v2/escalation.test.js new file mode 100644 index 00000000..1010a69e --- /dev/null +++ b/frontend/src/lib/server/v2/escalation.test.js @@ -0,0 +1,190 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +const apiRequest = vi.fn(); +vi.mock('$lib/api-helpers.js', () => ({ apiRequest: (...a) => apiRequest(...a) })); + +const { + createEscalationPolicy, + updateEscalationPolicy, + deleteEscalationPolicy, + UPDATE_FIELDS +} = await import('./escalation.js'); + +const cookies = /** @type {any} */ ({ get: () => 'token' }); +const event = /** @type {any} */ ({ cookies }); + +const base = { + priority: 'Urgent', + first_response_action: 'notify', + resolution_action: 'reassign', + first_response_target_id: 'p1', + resolution_target_id: 'p2', + notify_team_id: 't1', + is_active: true +}; + +describe('UPDATE_FIELDS', () => { + it('excludes priority, the frozen natural key', () => { + expect(UPDATE_FIELDS).not.toContain('priority'); + }); +}); + +describe('createEscalationPolicy', () => { + beforeEach(() => { + apiRequest.mockReset(); + }); + + it('includes priority on create, along with the rest of the allow-list, and drops a hostile org', async () => { + apiRequest.mockResolvedValue({}); + await createEscalationPolicy(event, { + ...base, + org: 'ATTACKER-ORG', + created_by: 'ATTACKER' + }); + const [url, opts] = apiRequest.mock.calls[0]; + expect(url).toBe('/cases/escalation-policies/'); + expect(opts.method).toBe('POST'); + expect(opts.body.priority).toBe('Urgent'); + expect(Object.keys(opts.body).sort()).toEqual([ + 'first_response_action', + 'first_response_target_id', + 'is_active', + 'notify_team_id', + 'priority', + 'resolution_action', + 'resolution_target_id' + ]); + expect(opts.body.org).toBeUndefined(); + expect(opts.body.created_by).toBeUndefined(); + }); + + it('sends null, not an empty string, for an unselected first_response_target_id', async () => { + apiRequest.mockResolvedValue({}); + await createEscalationPolicy(event, { ...base, first_response_target_id: '' }); + const { body } = apiRequest.mock.calls[0][1]; + expect(body.first_response_target_id).toBeNull(); + }); + + it('sends null, not an empty string, for an unselected resolution_target_id', async () => { + apiRequest.mockResolvedValue({}); + await createEscalationPolicy(event, { ...base, resolution_target_id: '' }); + const { body } = apiRequest.mock.calls[0][1]; + expect(body.resolution_target_id).toBeNull(); + }); + + it('sends null, not an empty string, for an unselected notify_team_id', async () => { + apiRequest.mockResolvedValue({}); + await createEscalationPolicy(event, { ...base, notify_team_id: '' }); + const { body } = apiRequest.mock.calls[0][1]; + expect(body.notify_team_id).toBeNull(); + }); + + it('coerces is_active to a boolean', async () => { + apiRequest.mockResolvedValue({}); + await createEscalationPolicy(event, { ...base, is_active: 'true' }); + const { body } = apiRequest.mock.calls[0][1]; + expect(body.is_active).toBe(true); + expect(typeof body.is_active).toBe('boolean'); + }); + + it('rejects a priority outside the four choices before the round trip', async () => { + await expect(createEscalationPolicy(event, { ...base, priority: 'Critical' })).rejects.toThrow( + /priority/i + ); + expect(apiRequest).not.toHaveBeenCalled(); + }); + + it('rejects a missing priority before the round trip', async () => { + await expect(createEscalationPolicy(event, { ...base, priority: '' })).rejects.toThrow( + /priority/i + ); + expect(apiRequest).not.toHaveBeenCalled(); + }); +}); + +describe('updateEscalationPolicy', () => { + beforeEach(() => { + apiRequest.mockReset(); + }); + + it('PUTs to the detail endpoint', async () => { + apiRequest.mockResolvedValue({}); + await updateEscalationPolicy(event, 'e1', base); + const [url, opts] = apiRequest.mock.calls[0]; + expect(url).toBe('/cases/escalation-policies/e1/'); + expect(opts.method).toBe('PUT'); + }); + + it('never sends priority, even when the caller passes one', async () => { + apiRequest.mockResolvedValue({}); + await updateEscalationPolicy(event, 'e1', base); + const { body } = apiRequest.mock.calls[0][1]; + expect(body.priority).toBeUndefined(); + expect(Object.keys(body)).not.toContain('priority'); + }); + + it('sends only what it is given, with no org/created_by', async () => { + apiRequest.mockResolvedValue({}); + await updateEscalationPolicy(event, 'e1', { + ...base, + org: 'ATTACKER-ORG', + created_by: 'ATTACKER' + }); + const { body } = apiRequest.mock.calls[0][1]; + expect(Object.keys(body).sort()).toEqual([ + 'first_response_action', + 'first_response_target_id', + 'is_active', + 'notify_team_id', + 'resolution_action', + 'resolution_target_id' + ]); + expect(body.org).toBeUndefined(); + expect(body.created_by).toBeUndefined(); + }); + + it('sends exactly one key for a minimal { is_active: true } body (the turn-on path)', async () => { + apiRequest.mockResolvedValue({}); + await updateEscalationPolicy(event, 'e1', { is_active: true }); + const { body } = apiRequest.mock.calls[0][1]; + expect(body).toEqual({ is_active: true }); + }); + + it('sends exactly one key for a minimal { is_active: false } body (the turn-off path)', async () => { + apiRequest.mockResolvedValue({}); + await updateEscalationPolicy(event, 'e1', { is_active: false }); + const { body } = apiRequest.mock.calls[0][1]; + expect(body).toEqual({ is_active: false }); + }); + + it('sends null, not an empty string, for an unselected target on update', async () => { + apiRequest.mockResolvedValue({}); + await updateEscalationPolicy(event, 'e1', { ...base, first_response_target_id: '' }); + const { body } = apiRequest.mock.calls[0][1]; + expect(body.first_response_target_id).toBeNull(); + }); + + it('throws when the id is missing', async () => { + await expect(updateEscalationPolicy(event, '', base)).rejects.toThrow(/which policy/i); + expect(apiRequest).not.toHaveBeenCalled(); + }); +}); + +describe('deleteEscalationPolicy', () => { + beforeEach(() => { + apiRequest.mockReset(); + }); + + it('DELETEs the detail endpoint', async () => { + apiRequest.mockResolvedValue({}); + await deleteEscalationPolicy(event, 'e1'); + const [url, opts] = apiRequest.mock.calls[0]; + expect(url).toBe('/cases/escalation-policies/e1/'); + expect(opts.method).toBe('DELETE'); + }); + + it('throws when the id is missing', async () => { + await expect(deleteEscalationPolicy(event, '')).rejects.toThrow(/which policy/i); + expect(apiRequest).not.toHaveBeenCalled(); + }); +}); diff --git a/frontend/src/lib/server/v2/goals.js b/frontend/src/lib/server/v2/goals.js index 1ee5114c..b3baba4c 100644 --- a/frontend/src/lib/server/v2/goals.js +++ b/frontend/src/lib/server/v2/goals.js @@ -34,6 +34,7 @@ * fields already in the payload, never a second aggregate query. */ import { apiRequest } from '$lib/api-helpers.js'; +import { getOrgPeopleAndTeams } from './org-people.js'; /** The two goal kinds the backend accepts (common/utils.py GOAL_TYPES). */ export const GOAL_TYPES = ['REVENUE', 'DEALS_CLOSED']; @@ -191,31 +192,6 @@ export async function listGoals({ cookies }) { }; } -/** - * In-org people and teams for the create/edit pickers. - * `/users/get-teams-and-users/` filters both to `request.profile.org`, so the - * picker can only ever offer in-org targets. The same boundary the serializer - * now enforces, surfaced early as UX. - * - * @param {import('@sveltejs/kit').Cookies} cookies - */ -async function targetOptions(cookies) { - try { - const resp = await apiRequest('/users/get-teams-and-users/', {}, { cookies }); - const people = (resp?.profiles ?? []).map((/** @type {any} */ p) => ({ - id: p.id, - name: personName(p) - })); - const teams = (resp?.teams ?? []).map((/** @type {any} */ t) => ({ - id: t.id, - name: t.name - })); - return { people, teams }; - } catch { - return { people: [], teams: [] }; - } -} - /** * What the create form needs. `can_edit` gates the whole route: creating a goal * is admin-only server-side, and a non-admin who reaches `/goals/new` gets @@ -225,7 +201,7 @@ async function targetOptions(cookies) { */ export async function getGoalFormOptions({ cookies }) { if (viewerRole(cookies) !== 'ADMIN') return { can_edit: false }; - const { people, teams } = await targetOptions(cookies); + const { people, teams } = await getOrgPeopleAndTeams(cookies); return { can_edit: true, people, teams }; } @@ -242,7 +218,7 @@ export async function getGoalForEdit({ cookies }, id) { const [raw, { people, teams }] = await Promise.all([ apiRequest(`/opportunities/goals/${id}/`, {}, { cookies }), - targetOptions(cookies) + getOrgPeopleAndTeams(cookies) ]); const target = raw.assigned_to diff --git a/frontend/src/lib/server/v2/inbound-email.js b/frontend/src/lib/server/v2/inbound-email.js index 04314c62..a5056d98 100644 --- a/frontend/src/lib/server/v2/inbound-email.js +++ b/frontend/src/lib/server/v2/inbound-email.js @@ -12,14 +12,22 @@ * came from the provider. Anything holding it can forge tickets into this org. * The backend already strips it for non-admins, but this layer drops it for * everyone: it is never needed to render the page, so it never reaches the - * browser, admin or not. Rotation is an explicit action elsewhere, not a field - * on a page you can browse to. + * browser, admin or not. `topic_arn` embeds the AWS account id and is held to + * the same bar. Rotation and a one-time reveal on create are an explicit, + * separate feature this task does not build, not a field on a page you can + * browse to. * - * Read-only page: "Add address" / "Edit" are deferred builders, so there is no - * write path here. The backend returns `default_assignee` as a full profile - * object (name under `user_details.name`); the page wants `{ id, name }`. + * The rule extends to the write side, below: `CREATE_FIELDS` and + * `UPDATE_FIELDS` exclude both fields too, on purpose, not by oversight. See + * the comment above `CREATE_FIELDS` for why omitting them is strictly better + * than any form could do. + * + * Create, edit, turn off, turn on and delete are wired below. The backend + * returns `default_assignee` as a full profile object (name under + * `user_details.name`); the page wants `{ id, name }`. */ import { apiRequest } from '$lib/api-helpers.js'; +import { viewerRole } from './organization.js'; /** Profile → the `{ id, name }` the card reads, or null. */ function shapeAssignee(p) { @@ -29,7 +37,7 @@ function shapeAssignee(p) { /** * @param {{ cookies: import('@sveltejs/kit').Cookies }} event - * @returns {Promise<{ mailboxes: any[], totals: any }>} + * @returns {Promise<{ mailboxes: any[], totals: any, can_edit: boolean }>} */ export async function getMailboxes({ cookies }) { const { mailboxes, totals } = await apiRequest('/cases/mailboxes/', {}, { cookies }); @@ -48,6 +56,82 @@ export async function getMailboxes({ cookies }) { cases_last_30d: m.cases_last_30d ?? 0, last_received_at: m.last_received_at ?? null })), - totals: totals ?? { count: 0, active: 0, cases_last_30d: 0 } + totals: totals ?? { count: 0, active: 0, cases_last_30d: 0 }, + // A display hint: POST/PUT/DELETE on `/cases/mailboxes/` each start with + // `_is_admin(request.profile)` server-side and 403 regardless of what this + // says. This only decides whether the page offers the controls. GET itself + // is not role-gated, so a member can still see this list. + can_edit: viewerRole(cookies) === 'ADMIN' }; } + +/** No `webhook_secret`, no `topic_arn`. `InboundMailboxListCreateView.post` + * generates a `secrets.token_urlsafe(32)` secret when the body carries none, + * which is a better secret than a pasted one, and `put` is `partial=True`, + * so omitting the key leaves the live credential untouched. A form field for + * either would mean the credential travels to a browser and back on every + * edit, and an empty one would blank it. */ +const CREATE_FIELDS = [ + 'address', + 'provider', + 'default_priority', + 'default_case_type', + 'default_assignee_id', + 'is_active' +]; + +/** `address` is the mailbox's identity and its uniqueness key. It is not + * frozen by the backend (`validate_address` only checks duplicates on + * create, and `put` is partial), so an edit may change it. It stays in the + * update allow-list for that reason, with a hint on the form saying mail to + * the old address stops becoming tickets. */ +const UPDATE_FIELDS = CREATE_FIELDS; + +/** @param {string[]} allowed @param {{ [key: string]: any }} values */ +function buildBody(allowed, values) { + /** @type {Record} */ + const body = {}; + for (const field of allowed) { + if (values[field] === undefined) continue; + body[field] = values[field]; + } + // Two nullable fields whose "none selected" option is an empty `