From 35bcb7ed261bf6e3b3a58db631446fd141539c90 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 12 Jun 2026 23:01:51 +0000 Subject: [PATCH 01/53] chore(deps-dev): bump esbuild from 0.28.0 to 0.28.1 in /frontend Bumps [esbuild](https://github.com/evanw/esbuild) from 0.28.0 to 0.28.1. - [Release notes](https://github.com/evanw/esbuild/releases) - [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md) - [Commits](https://github.com/evanw/esbuild/compare/v0.28.0...v0.28.1) --- updated-dependencies: - dependency-name: esbuild dependency-version: 0.28.1 dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- frontend/package.json | 2 +- frontend/pnpm-lock.yaml | 469 +++++++++++++++++++++------------------- 2 files changed, 253 insertions(+), 218 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index b5243e44f..9331c6032 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -128,7 +128,7 @@ "browserslist": "4.28.2", "caniuse-lite": "1.0.30001799", "csstype": "3.2.3", - "esbuild": "0.28.0", + "esbuild": "0.28.1", "eslint": "9.39.4", "eslint-plugin-depend": "1.5.0", "eslint-plugin-vue": "10.9.2", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 2369c33e4..5ddc180be 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -245,8 +245,8 @@ importers: specifier: 3.2.3 version: 3.2.3 esbuild: - specifier: 0.28.0 - version: 0.28.0 + specifier: 0.28.1 + version: 0.28.1 eslint: specifier: 9.39.4 version: 9.39.4(jiti@2.6.1) @@ -1341,14 +1341,14 @@ packages: cpu: [ppc64] os: [aix] - '@esbuild/aix-ppc64@0.27.5': - resolution: {integrity: sha512-nGsF/4C7uzUj+Nj/4J+Zt0bYQ6bz33Phz8Lb2N80Mti1HjGclTJdXZ+9APC4kLvONbjxN1zfvYNd8FEcbBK/MQ==} + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/aix-ppc64@0.28.0': - resolution: {integrity: sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==} + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] @@ -1359,14 +1359,14 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm64@0.27.5': - resolution: {integrity: sha512-Oeghq+XFgh1pUGd1YKs4DDoxzxkoUkvko+T/IVKwlghKLvvjbGFB3ek8VEDBmNvqhwuL0CQS3cExdzpmUyIrgA==} + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm64@0.28.0': - resolution: {integrity: sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==} + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} engines: {node: '>=18'} cpu: [arm64] os: [android] @@ -1377,14 +1377,14 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-arm@0.27.5': - resolution: {integrity: sha512-Cv781jd0Rfj/paoNrul1/r4G0HLvuFKYh7C9uHZ2Pl8YXstzvCyyeWENTFR9qFnRzNMCjXmsulZuvosDg10Mog==} + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-arm@0.28.0': - resolution: {integrity: sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==} + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} engines: {node: '>=18'} cpu: [arm] os: [android] @@ -1395,14 +1395,14 @@ packages: cpu: [x64] os: [android] - '@esbuild/android-x64@0.27.5': - resolution: {integrity: sha512-nQD7lspbzerlmtNOxYMFAGmhxgzn8Z7m9jgFkh6kpkjsAhZee1w8tJW3ZlW+N9iRePz0oPUDrYrXidCPSImD0Q==} + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/android-x64@0.28.0': - resolution: {integrity: sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==} + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} engines: {node: '>=18'} cpu: [x64] os: [android] @@ -1413,14 +1413,14 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-arm64@0.27.5': - resolution: {integrity: sha512-I+Ya/MgC6rr8oRWGRDF3BXDfP8K1BVUggHqN6VI2lUZLdDi1IM1v2cy0e3lCPbP+pVcK3Tv8cgUhHse1kaNZZw==} + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-arm64@0.28.0': - resolution: {integrity: sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==} + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] @@ -1431,14 +1431,14 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/darwin-x64@0.27.5': - resolution: {integrity: sha512-MCjQUtC8wWJn/pIPM7vQaO69BFgwPD1jriEdqwTCKzWjGgkMbcg+M5HzrOhPhuYe1AJjXlHmD142KQf+jnYj8A==} + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/darwin-x64@0.28.0': - resolution: {integrity: sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==} + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] @@ -1449,14 +1449,14 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-arm64@0.27.5': - resolution: {integrity: sha512-X6xVS+goSH0UelYXnuf4GHLwpOdc8rgK/zai+dKzBMnncw7BTQIwquOodE7EKvY2UVUetSqyAfyZC1D+oqLQtg==} + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-arm64@0.28.0': - resolution: {integrity: sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==} + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] @@ -1467,14 +1467,14 @@ packages: cpu: [x64] os: [freebsd] - '@esbuild/freebsd-x64@0.27.5': - resolution: {integrity: sha512-233X1FGo3a8x1ekLB6XT69LfZ83vqz+9z3TSEQCTYfMNY880A97nr81KbPcAMl9rmOFp11wO0dP+eB18KU/Ucg==} + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/freebsd-x64@0.28.0': - resolution: {integrity: sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==} + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] @@ -1485,14 +1485,14 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm64@0.27.5': - resolution: {integrity: sha512-euKkilsNOv7x/M1NKsx5znyprbpsRFIzTV6lWziqJch7yWYayfLtZzDxDTl+LSQDJYAjd9TVb/Kt5UKIrj2e4A==} + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm64@0.28.0': - resolution: {integrity: sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==} + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} engines: {node: '>=18'} cpu: [arm64] os: [linux] @@ -1503,14 +1503,14 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-arm@0.27.5': - resolution: {integrity: sha512-0wkVrYHG4sdCCN/bcwQ7yYMXACkaHc3UFeaEOwSVW6e5RycMageYAFv+JS2bKLwHyeKVUvtoVH+5/RHq0fgeFw==} + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-arm@0.28.0': - resolution: {integrity: sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==} + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} engines: {node: '>=18'} cpu: [arm] os: [linux] @@ -1521,14 +1521,14 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-ia32@0.27.5': - resolution: {integrity: sha512-hVRQX4+P3MS36NxOy24v/Cdsimy/5HYePw+tmPqnNN1fxV0bPrFWR6TMqwXPwoTM2VzbkA+4lbHWUKDd5ZDA/w==} + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-ia32@0.28.0': - resolution: {integrity: sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==} + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} engines: {node: '>=18'} cpu: [ia32] os: [linux] @@ -1539,14 +1539,14 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-loong64@0.27.5': - resolution: {integrity: sha512-mKqqRuOPALI8nDzhOBmIS0INvZOOFGGg5n1osGIXAx8oersceEbKd4t1ACNTHM3sJBXGFAlEgqM+svzjPot+ZQ==} + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-loong64@0.28.0': - resolution: {integrity: sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==} + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] @@ -1557,14 +1557,14 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-mips64el@0.27.5': - resolution: {integrity: sha512-EE/QXH9IyaAj1qeuIV5+/GZkBTipgGO782Ff7Um3vPS9cvLhJJeATy4Ggxikz2inZ46KByamMn6GqtqyVjhenA==} + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-mips64el@0.28.0': - resolution: {integrity: sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==} + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] @@ -1575,14 +1575,14 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-ppc64@0.27.5': - resolution: {integrity: sha512-0V2iF1RGxBf1b7/BjurA5jfkl7PtySjom1r6xOK2q9KWw/XCpAdtB6KNMO+9xx69yYfSCRR9FE0TyKfHA2eQMw==} + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-ppc64@0.28.0': - resolution: {integrity: sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==} + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] @@ -1593,14 +1593,14 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-riscv64@0.27.5': - resolution: {integrity: sha512-rYxThBx6G9HN6tFNuvB/vykeLi4VDsm5hE5pVwzqbAjZEARQrWu3noZSfbEnPZ/CRXP3271GyFk/49up2W190g==} + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-riscv64@0.28.0': - resolution: {integrity: sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==} + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] @@ -1611,14 +1611,14 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-s390x@0.27.5': - resolution: {integrity: sha512-uEP2q/4qgd8goEUc4QIdU/1P2NmEtZ/zX5u3OpLlCGhJIuBIv0s0wr7TB2nBrd3/A5XIdEkkS5ZLF0ULuvaaYQ==} + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-s390x@0.28.0': - resolution: {integrity: sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==} + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} engines: {node: '>=18'} cpu: [s390x] os: [linux] @@ -1629,14 +1629,14 @@ packages: cpu: [x64] os: [linux] - '@esbuild/linux-x64@0.27.5': - resolution: {integrity: sha512-+Gq47Wqq6PLOOZuBzVSII2//9yyHNKZLuwfzCemqexqOQCSz0zy0O26kIzyp9EMNMK+nZ0tFHBZrCeVUuMs/ew==} + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/linux-x64@0.28.0': - resolution: {integrity: sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==} + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} engines: {node: '>=18'} cpu: [x64] os: [linux] @@ -1647,14 +1647,14 @@ packages: cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-arm64@0.27.5': - resolution: {integrity: sha512-3F/5EG8VHfN/I+W5cO1/SV2H9Q/5r7vcHabMnBqhHK2lTWOh3F8vixNzo8lqxrlmBtZVFpW8pmITHnq54+Tq4g==} + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-arm64@0.28.0': - resolution: {integrity: sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==} + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] @@ -1665,14 +1665,14 @@ packages: cpu: [x64] os: [netbsd] - '@esbuild/netbsd-x64@0.27.5': - resolution: {integrity: sha512-28t+Sj3CPN8vkMOlZotOmDgilQwVvxWZl7b8rxpn73Tt/gCnvrHxQUMng4uu3itdFvrtba/1nHejvxqz8xgEMA==} + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/netbsd-x64@0.28.0': - resolution: {integrity: sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==} + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] @@ -1683,14 +1683,14 @@ packages: cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-arm64@0.27.5': - resolution: {integrity: sha512-Doz/hKtiuVAi9hMsBMpwBANhIZc8l238U2Onko3t2xUp8xtM0ZKdDYHMnm/qPFVthY8KtxkXaocwmMh6VolzMA==} + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-arm64@0.28.0': - resolution: {integrity: sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==} + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] @@ -1701,14 +1701,14 @@ packages: cpu: [x64] os: [openbsd] - '@esbuild/openbsd-x64@0.27.5': - resolution: {integrity: sha512-WfGVaa1oz5A7+ZFPkERIbIhKT4olvGl1tyzTRaB5yoZRLqC0KwaO95FeZtOdQj/oKkjW57KcVF944m62/0GYtA==} + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openbsd-x64@0.28.0': - resolution: {integrity: sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==} + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] @@ -1719,14 +1719,14 @@ packages: cpu: [arm64] os: [openharmony] - '@esbuild/openharmony-arm64@0.27.5': - resolution: {integrity: sha512-Xh+VRuh6OMh3uJ0JkCjI57l+DVe7VRGBYymen8rFPnTVgATBwA6nmToxM2OwTlSvrnWpPKkrQUj93+K9huYC6A==} + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/openharmony-arm64@0.28.0': - resolution: {integrity: sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==} + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] @@ -1737,14 +1737,14 @@ packages: cpu: [x64] os: [sunos] - '@esbuild/sunos-x64@0.27.5': - resolution: {integrity: sha512-aC1gpJkkaUADHuAdQfuVTnqVUTLqqUNhAvEwHwVWcnVVZvNlDPGA0UveZsfXJJ9T6k9Po4eHi3c02gbdwO3g6w==} + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/sunos-x64@0.28.0': - resolution: {integrity: sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==} + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} engines: {node: '>=18'} cpu: [x64] os: [sunos] @@ -1755,14 +1755,14 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-arm64@0.27.5': - resolution: {integrity: sha512-0UNx2aavV0fk6UpZcwXFLztA2r/k9jTUa7OW7SAea1VYUhkug99MW1uZeXEnPn5+cHOd0n8myQay6TlFnBR07w==} + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-arm64@0.28.0': - resolution: {integrity: sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==} + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] @@ -1773,14 +1773,14 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-ia32@0.27.5': - resolution: {integrity: sha512-5nlJ3AeJWCTSzR7AEqVjT/faWyqKU86kCi1lLmxVqmNR+j4HrYdns+eTGjS/vmrzCIe8inGQckUadvS0+JkKdQ==} + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-ia32@0.28.0': - resolution: {integrity: sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==} + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} engines: {node: '>=18'} cpu: [ia32] os: [win32] @@ -1791,14 +1791,14 @@ packages: cpu: [x64] os: [win32] - '@esbuild/win32-x64@0.27.5': - resolution: {integrity: sha512-PWypQR+d4FLfkhBIV+/kHsUELAnMpx1bRvvsn3p+/sAERbnCzFrtDRG2Xw5n+2zPxBK2+iaP+vetsRl4Ti7WgA==} + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} engines: {node: '>=18'} cpu: [x64] os: [win32] - '@esbuild/win32-x64@0.28.0': - resolution: {integrity: sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==} + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -2147,36 +2147,42 @@ packages: engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm-musl@2.5.1': resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [musl] '@parcel/watcher-linux-arm64-glibc@2.5.1': resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm64-musl@2.5.1': resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [musl] '@parcel/watcher-linux-x64-glibc@2.5.1': resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-x64-musl@2.5.1': resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [musl] '@parcel/watcher-win32-arm64@2.5.1': resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} @@ -2336,66 +2342,79 @@ packages: resolution: {integrity: sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.61.1': resolution: {integrity: sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.61.1': resolution: {integrity: sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.61.1': resolution: {integrity: sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.61.1': resolution: {integrity: sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.61.1': resolution: {integrity: sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.61.1': resolution: {integrity: sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.61.1': resolution: {integrity: sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.61.1': resolution: {integrity: sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.61.1': resolution: {integrity: sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.61.1': resolution: {integrity: sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.61.1': resolution: {integrity: sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.61.1': resolution: {integrity: sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.61.1': resolution: {integrity: sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==} @@ -2598,24 +2617,28 @@ packages: engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.3.0': resolution: {integrity: sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.3.0': resolution: {integrity: sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.3.0': resolution: {integrity: sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.3.0': resolution: {integrity: sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==} @@ -4014,13 +4037,13 @@ packages: engines: {node: '>=18'} hasBin: true - esbuild@0.27.5: - resolution: {integrity: sha512-zdQoHBjuDqKsvV5OPaWansOwfSQ0Js+Uj9J85TBvj3bFW1JjWTSULMRwdQAc8qMeIScbClxeMK0jlrtB9linhA==} + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} engines: {node: '>=18'} hasBin: true - esbuild@0.28.0: - resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==} + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} engines: {node: '>=18'} hasBin: true @@ -4955,24 +4978,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} @@ -5977,48 +6004,56 @@ packages: engines: {node: '>=14.0.0'} cpu: [arm64] os: [linux] + libc: glibc sass-embedded-linux-arm@1.100.0: resolution: {integrity: sha512-9Ul7O1eKrc5YlhwWjkp8tZPSe3UEwSZ1uwUZOQom1HL0pRlBA6F/IlGZYFTLwnHMIP1fc77MMNaBRfc05mKMpw==} engines: {node: '>=14.0.0'} cpu: [arm] os: [linux] + libc: glibc sass-embedded-linux-musl-arm64@1.100.0: resolution: {integrity: sha512-XpACJB2KjSLjf2e9uuvGVdOURsoNrFqgRiihhXyUHK9W0t3LIHb7z5MA/7XGPIT9bWSOO2zyw+rH/FHtDV/Yrg==} engines: {node: '>=14.0.0'} cpu: [arm64] os: [linux] + libc: musl sass-embedded-linux-musl-arm@1.100.0: resolution: {integrity: sha512-sl0JgbGloPyJg66XXx5UDSDScZ0oU85DpMQU4JU/sCUCFj1Z8zZ69SJWKTCNE4/jwnce7WI2zPCV5AG+RHOZJw==} engines: {node: '>=14.0.0'} cpu: [arm] os: [linux] + libc: musl sass-embedded-linux-musl-riscv64@1.100.0: resolution: {integrity: sha512-ShvI0Kx04mwoCARwZ0UjiT97isQvzO80tAt91zmFyHLN9kelc/IrQi940farSm2xQVPCKdeVyeG0ekBsokSpYQ==} engines: {node: '>=14.0.0'} cpu: [riscv64] os: [linux] + libc: musl sass-embedded-linux-musl-x64@1.100.0: resolution: {integrity: sha512-TDBCRWNuS4RDLQXvRc1gjZlWiWTWaWGp0Bwu/IKwJxov81lsvrCs3TihTyNXtW7V5aoN4Ky3r0QOkNb3mwmBnA==} engines: {node: '>=14.0.0'} cpu: [x64] os: [linux] + libc: musl sass-embedded-linux-riscv64@1.100.0: resolution: {integrity: sha512-j4ENJGOheO+fm3j/yorLxCjBP6/XskrZx7dTLlT+lXYwN/qqCqoA/gsNLI0McS3DFM6GBwPiffzWsdWS8t6sEQ==} engines: {node: '>=14.0.0'} cpu: [riscv64] os: [linux] + libc: glibc sass-embedded-linux-x64@1.100.0: resolution: {integrity: sha512-0vUSN8j0WGtCJIOPh//EmUvYGHW0QOe5iul8qyhPk50MAcw49MA0r34AhftjDdx94ILPF6vApFs0gwHPQRlpVA==} engines: {node: '>=14.0.0'} cpu: [x64] os: [linux] + libc: glibc sass-embedded-unknown-all@1.100.0: resolution: {integrity: sha512-c+naBgWId4MIpToXcI0DgqetjdAkwTTAxFAuOaBz7HUXLdyG1oZRrEvSsbe41nEdQOKH0vgofVFCeSQgoXOG9A==} @@ -8392,235 +8427,235 @@ snapshots: '@esbuild/aix-ppc64@0.25.12': optional: true - '@esbuild/aix-ppc64@0.27.5': + '@esbuild/aix-ppc64@0.27.7': optional: true - '@esbuild/aix-ppc64@0.28.0': + '@esbuild/aix-ppc64@0.28.1': optional: true '@esbuild/android-arm64@0.25.12': optional: true - '@esbuild/android-arm64@0.27.5': + '@esbuild/android-arm64@0.27.7': optional: true - '@esbuild/android-arm64@0.28.0': + '@esbuild/android-arm64@0.28.1': optional: true '@esbuild/android-arm@0.25.12': optional: true - '@esbuild/android-arm@0.27.5': + '@esbuild/android-arm@0.27.7': optional: true - '@esbuild/android-arm@0.28.0': + '@esbuild/android-arm@0.28.1': optional: true '@esbuild/android-x64@0.25.12': optional: true - '@esbuild/android-x64@0.27.5': + '@esbuild/android-x64@0.27.7': optional: true - '@esbuild/android-x64@0.28.0': + '@esbuild/android-x64@0.28.1': optional: true '@esbuild/darwin-arm64@0.25.12': optional: true - '@esbuild/darwin-arm64@0.27.5': + '@esbuild/darwin-arm64@0.27.7': optional: true - '@esbuild/darwin-arm64@0.28.0': + '@esbuild/darwin-arm64@0.28.1': optional: true '@esbuild/darwin-x64@0.25.12': optional: true - '@esbuild/darwin-x64@0.27.5': + '@esbuild/darwin-x64@0.27.7': optional: true - '@esbuild/darwin-x64@0.28.0': + '@esbuild/darwin-x64@0.28.1': optional: true '@esbuild/freebsd-arm64@0.25.12': optional: true - '@esbuild/freebsd-arm64@0.27.5': + '@esbuild/freebsd-arm64@0.27.7': optional: true - '@esbuild/freebsd-arm64@0.28.0': + '@esbuild/freebsd-arm64@0.28.1': optional: true '@esbuild/freebsd-x64@0.25.12': optional: true - '@esbuild/freebsd-x64@0.27.5': + '@esbuild/freebsd-x64@0.27.7': optional: true - '@esbuild/freebsd-x64@0.28.0': + '@esbuild/freebsd-x64@0.28.1': optional: true '@esbuild/linux-arm64@0.25.12': optional: true - '@esbuild/linux-arm64@0.27.5': + '@esbuild/linux-arm64@0.27.7': optional: true - '@esbuild/linux-arm64@0.28.0': + '@esbuild/linux-arm64@0.28.1': optional: true '@esbuild/linux-arm@0.25.12': optional: true - '@esbuild/linux-arm@0.27.5': + '@esbuild/linux-arm@0.27.7': optional: true - '@esbuild/linux-arm@0.28.0': + '@esbuild/linux-arm@0.28.1': optional: true '@esbuild/linux-ia32@0.25.12': optional: true - '@esbuild/linux-ia32@0.27.5': + '@esbuild/linux-ia32@0.27.7': optional: true - '@esbuild/linux-ia32@0.28.0': + '@esbuild/linux-ia32@0.28.1': optional: true '@esbuild/linux-loong64@0.25.12': optional: true - '@esbuild/linux-loong64@0.27.5': + '@esbuild/linux-loong64@0.27.7': optional: true - '@esbuild/linux-loong64@0.28.0': + '@esbuild/linux-loong64@0.28.1': optional: true '@esbuild/linux-mips64el@0.25.12': optional: true - '@esbuild/linux-mips64el@0.27.5': + '@esbuild/linux-mips64el@0.27.7': optional: true - '@esbuild/linux-mips64el@0.28.0': + '@esbuild/linux-mips64el@0.28.1': optional: true '@esbuild/linux-ppc64@0.25.12': optional: true - '@esbuild/linux-ppc64@0.27.5': + '@esbuild/linux-ppc64@0.27.7': optional: true - '@esbuild/linux-ppc64@0.28.0': + '@esbuild/linux-ppc64@0.28.1': optional: true '@esbuild/linux-riscv64@0.25.12': optional: true - '@esbuild/linux-riscv64@0.27.5': + '@esbuild/linux-riscv64@0.27.7': optional: true - '@esbuild/linux-riscv64@0.28.0': + '@esbuild/linux-riscv64@0.28.1': optional: true '@esbuild/linux-s390x@0.25.12': optional: true - '@esbuild/linux-s390x@0.27.5': + '@esbuild/linux-s390x@0.27.7': optional: true - '@esbuild/linux-s390x@0.28.0': + '@esbuild/linux-s390x@0.28.1': optional: true '@esbuild/linux-x64@0.25.12': optional: true - '@esbuild/linux-x64@0.27.5': + '@esbuild/linux-x64@0.27.7': optional: true - '@esbuild/linux-x64@0.28.0': + '@esbuild/linux-x64@0.28.1': optional: true '@esbuild/netbsd-arm64@0.25.12': optional: true - '@esbuild/netbsd-arm64@0.27.5': + '@esbuild/netbsd-arm64@0.27.7': optional: true - '@esbuild/netbsd-arm64@0.28.0': + '@esbuild/netbsd-arm64@0.28.1': optional: true '@esbuild/netbsd-x64@0.25.12': optional: true - '@esbuild/netbsd-x64@0.27.5': + '@esbuild/netbsd-x64@0.27.7': optional: true - '@esbuild/netbsd-x64@0.28.0': + '@esbuild/netbsd-x64@0.28.1': optional: true '@esbuild/openbsd-arm64@0.25.12': optional: true - '@esbuild/openbsd-arm64@0.27.5': + '@esbuild/openbsd-arm64@0.27.7': optional: true - '@esbuild/openbsd-arm64@0.28.0': + '@esbuild/openbsd-arm64@0.28.1': optional: true '@esbuild/openbsd-x64@0.25.12': optional: true - '@esbuild/openbsd-x64@0.27.5': + '@esbuild/openbsd-x64@0.27.7': optional: true - '@esbuild/openbsd-x64@0.28.0': + '@esbuild/openbsd-x64@0.28.1': optional: true '@esbuild/openharmony-arm64@0.25.12': optional: true - '@esbuild/openharmony-arm64@0.27.5': + '@esbuild/openharmony-arm64@0.27.7': optional: true - '@esbuild/openharmony-arm64@0.28.0': + '@esbuild/openharmony-arm64@0.28.1': optional: true '@esbuild/sunos-x64@0.25.12': optional: true - '@esbuild/sunos-x64@0.27.5': + '@esbuild/sunos-x64@0.27.7': optional: true - '@esbuild/sunos-x64@0.28.0': + '@esbuild/sunos-x64@0.28.1': optional: true '@esbuild/win32-arm64@0.25.12': optional: true - '@esbuild/win32-arm64@0.27.5': + '@esbuild/win32-arm64@0.27.7': optional: true - '@esbuild/win32-arm64@0.28.0': + '@esbuild/win32-arm64@0.28.1': optional: true '@esbuild/win32-ia32@0.25.12': optional: true - '@esbuild/win32-ia32@0.27.5': + '@esbuild/win32-ia32@0.27.7': optional: true - '@esbuild/win32-ia32@0.28.0': + '@esbuild/win32-ia32@0.28.1': optional: true '@esbuild/win32-x64@0.25.12': optional: true - '@esbuild/win32-x64@0.27.5': + '@esbuild/win32-x64@0.27.7': optional: true - '@esbuild/win32-x64@0.28.0': + '@esbuild/win32-x64@0.28.1': optional: true '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.6.1))': @@ -11006,63 +11041,63 @@ snapshots: '@esbuild/win32-ia32': 0.25.12 '@esbuild/win32-x64': 0.25.12 - esbuild@0.27.5: + esbuild@0.27.7: optionalDependencies: - '@esbuild/aix-ppc64': 0.27.5 - '@esbuild/android-arm': 0.27.5 - '@esbuild/android-arm64': 0.27.5 - '@esbuild/android-x64': 0.27.5 - '@esbuild/darwin-arm64': 0.27.5 - '@esbuild/darwin-x64': 0.27.5 - '@esbuild/freebsd-arm64': 0.27.5 - '@esbuild/freebsd-x64': 0.27.5 - '@esbuild/linux-arm': 0.27.5 - '@esbuild/linux-arm64': 0.27.5 - '@esbuild/linux-ia32': 0.27.5 - '@esbuild/linux-loong64': 0.27.5 - '@esbuild/linux-mips64el': 0.27.5 - '@esbuild/linux-ppc64': 0.27.5 - '@esbuild/linux-riscv64': 0.27.5 - '@esbuild/linux-s390x': 0.27.5 - '@esbuild/linux-x64': 0.27.5 - '@esbuild/netbsd-arm64': 0.27.5 - '@esbuild/netbsd-x64': 0.27.5 - '@esbuild/openbsd-arm64': 0.27.5 - '@esbuild/openbsd-x64': 0.27.5 - '@esbuild/openharmony-arm64': 0.27.5 - '@esbuild/sunos-x64': 0.27.5 - '@esbuild/win32-arm64': 0.27.5 - '@esbuild/win32-ia32': 0.27.5 - '@esbuild/win32-x64': 0.27.5 + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 - esbuild@0.28.0: + esbuild@0.28.1: optionalDependencies: - '@esbuild/aix-ppc64': 0.28.0 - '@esbuild/android-arm': 0.28.0 - '@esbuild/android-arm64': 0.28.0 - '@esbuild/android-x64': 0.28.0 - '@esbuild/darwin-arm64': 0.28.0 - '@esbuild/darwin-x64': 0.28.0 - '@esbuild/freebsd-arm64': 0.28.0 - '@esbuild/freebsd-x64': 0.28.0 - '@esbuild/linux-arm': 0.28.0 - '@esbuild/linux-arm64': 0.28.0 - '@esbuild/linux-ia32': 0.28.0 - '@esbuild/linux-loong64': 0.28.0 - '@esbuild/linux-mips64el': 0.28.0 - '@esbuild/linux-ppc64': 0.28.0 - '@esbuild/linux-riscv64': 0.28.0 - '@esbuild/linux-s390x': 0.28.0 - '@esbuild/linux-x64': 0.28.0 - '@esbuild/netbsd-arm64': 0.28.0 - '@esbuild/netbsd-x64': 0.28.0 - '@esbuild/openbsd-arm64': 0.28.0 - '@esbuild/openbsd-x64': 0.28.0 - '@esbuild/openharmony-arm64': 0.28.0 - '@esbuild/sunos-x64': 0.28.0 - '@esbuild/win32-arm64': 0.28.0 - '@esbuild/win32-ia32': 0.28.0 - '@esbuild/win32-x64': 0.28.0 + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 escalade@3.2.0: {} @@ -14134,7 +14169,7 @@ snapshots: vite@7.3.5(@types/node@24.13.2)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.31.6)(yaml@2.8.3): dependencies: - esbuild: 0.27.5 + esbuild: 0.27.7 fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 postcss: 8.5.14 From 85b820fa7ca08d9c8a8569372f3576d2f28aa98c Mon Sep 17 00:00:00 2001 From: "Frederick [Bot]" Date: Tue, 16 Jun 2026 00:40:29 +0000 Subject: [PATCH 02/53] chore(i18n): update translations via Crowdin --- frontend/src/i18n/lang/el-GR.json | 33 ++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/frontend/src/i18n/lang/el-GR.json b/frontend/src/i18n/lang/el-GR.json index 4848afad7..14837735b 100644 --- a/frontend/src/i18n/lang/el-GR.json +++ b/frontend/src/i18n/lang/el-GR.json @@ -172,6 +172,7 @@ "yyyy/mm/dd": "ΕΕΕΕ/ΜΜ/ΗΗ" }, "timeFormat": "Μορφή ώρας", + "timeTrackingDefaultStart": "Ώρα έναρξης παρακολούθησης χρόνου με έξυπνο-γέμισμα", "timeFormatOptions": { "12h": "12 ώρες (ΠΜ/ΜΜ)", "24h": "24 ώρες (ΩΩ:ΛΛ)" @@ -781,7 +782,10 @@ "closeDialog": "Κλείσμο του διαλόγου", "closeQuickActions": "Κλείσιμο των γρήγορων ενεργειών", "skipToContent": "Μετάβαση στο κύριο περιεχόμενο", - "sortBy": "Ταξινόμηση ανά" + "sortBy": "Ταξινόμηση ανά", + "dateRange": "Εύρος ημερομηνιών", + "notSet": "Μη ορισμένο", + "user": "Χρήστης" }, "input": { "projectColor": "Χρώμα έργου", @@ -991,6 +995,7 @@ "repeatAfter": "Ορισμός Επαναλαμβανόμενου Διαστήματος", "percentDone": "Ορισμός Προόδου", "attachments": "Προσθήκη Συνημμένων", + "timeTracking": "Χρόνος ίχνους", "relatedTasks": "Προσθήκη Συσχέτισης", "moveProject": "Μετακίνηση", "duplicate": "Αντιγραφή", @@ -1460,6 +1465,32 @@ "frontendVersion": "Έκδοση frontend: {version}", "apiVersion": "Έκδοση API: {version}" }, + "timeTracking": { + "title": "Ιχνηλάτηση χρόνου", + "stop": "Διακοπή χρονομέτρου", + "logTime": "Καταγραφή χρόνου", + "editEntry": "Επεξεργασία εγγραφής", + "form": { + "task": "Εργασία", + "taskSearch": "Αναζήτηση για μια εργασία…", + "commentPlaceholder": "Σε τι δουλέψατε;", + "save": "Αποθήκευση εγγραφής", + "startTimer": "Έναρξη χρονοµέτρου", + "update": "Ενημέρωση εγγραφής", + "smartFill": "Συμπλήρωση από την τελευταία καταχώριση" + }, + "list": { + "emptyTask": "Δεν καταγράφηκε ακόμη χρόνος για αυτήν την εργασία.", + "emptyFiltered": "Δεν καταγράφηκε χρόνος με βάση τα επιλεγμένα φίλτρα.", + "total": "Σύνολο", + "time": "Ώρα", + "duration": "Διάρκεια" + }, + "browse": { + "selectRange": "Επιλέξτε ένα εύρος", + "userSearch": "Αναζήτηση για ένα χρήστη…" + } + }, "time": { "units": { "seconds": "δευτερόλεπτο|δευτερόλεπτα", From 1d6d332c189047ce598a9b45b99c5964a23690f3 Mon Sep 17 00:00:00 2001 From: kolaente Date: Tue, 16 Jun 2026 08:17:51 +0200 Subject: [PATCH 03/53] fix(deps): bump tmp to >=0.2.7 to fix path traversal advisory Resolves GHSA-7c78-jf6q-g5cm (type-confusion bypass of _assertPath allowing path traversal). tmp was pinned to >=0.2.6 via pnpm overrides in both the frontend and desktop workspaces, which resolved to the vulnerable 0.2.6. Dependabot alerts #243 (desktop) and #244 (frontend). --- desktop/package.json | 2 +- desktop/pnpm-lock.yaml | 10 ++++----- frontend/package.json | 2 +- frontend/pnpm-lock.yaml | 45 +++++------------------------------------ 4 files changed, 12 insertions(+), 47 deletions(-) diff --git a/desktop/package.json b/desktop/package.json index 765d91054..8a85d63df 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -77,7 +77,7 @@ "tar": "^7.5.11", "@tootallnate/once": "^3.0.1", "picomatch": ">=4.0.4", - "tmp": ">=0.2.6", + "tmp": ">=0.2.7", "ip-address": ">=10.1.1" } } diff --git a/desktop/pnpm-lock.yaml b/desktop/pnpm-lock.yaml index 77a74681b..6a15d26ef 100644 --- a/desktop/pnpm-lock.yaml +++ b/desktop/pnpm-lock.yaml @@ -9,7 +9,7 @@ overrides: tar: ^7.5.11 '@tootallnate/once': ^3.0.1 picomatch: '>=4.0.4' - tmp: '>=0.2.6' + tmp: '>=0.2.7' ip-address: '>=10.1.1' importers: @@ -1315,8 +1315,8 @@ packages: tmp-promise@3.0.3: resolution: {integrity: sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==} - tmp@0.2.6: - resolution: {integrity: sha512-5sJPdPjfI5Kx+qbrDesxkglRBxW//g7hCsqspEjwkewGvBMGIKMOTKzLt1hFVJzyadba3lDUN20O9qhvbQUSTA==} + tmp@0.2.7: + resolution: {integrity: sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==} engines: {node: '>=14.14'} toidentifier@1.0.1: @@ -3032,9 +3032,9 @@ snapshots: tmp-promise@3.0.3: dependencies: - tmp: 0.2.6 + tmp: 0.2.7 - tmp@0.2.6: {} + tmp@0.2.7: {} toidentifier@1.0.1: {} diff --git a/frontend/package.json b/frontend/package.json index 9331c6032..7ed2c55d4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -176,7 +176,7 @@ "flatted": "^3.4.1", "ip-address": ">=10.1.1", "postcss": ">=8.5.10", - "tmp": ">=0.2.6" + "tmp": ">=0.2.7" } } } diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 5ddc180be..548944d60 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -12,7 +12,7 @@ overrides: flatted: ^3.4.1 ip-address: '>=10.1.1' postcss: '>=8.5.10' - tmp: '>=0.2.6' + tmp: '>=0.2.7' importers: @@ -2147,42 +2147,36 @@ packages: engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] - libc: [glibc] '@parcel/watcher-linux-arm-musl@2.5.1': resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] - libc: [musl] '@parcel/watcher-linux-arm64-glibc@2.5.1': resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] '@parcel/watcher-linux-arm64-musl@2.5.1': resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] - libc: [musl] '@parcel/watcher-linux-x64-glibc@2.5.1': resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] - libc: [glibc] '@parcel/watcher-linux-x64-musl@2.5.1': resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] - libc: [musl] '@parcel/watcher-win32-arm64@2.5.1': resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} @@ -2342,79 +2336,66 @@ packages: resolution: {integrity: sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.61.1': resolution: {integrity: sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.61.1': resolution: {integrity: sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.61.1': resolution: {integrity: sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.61.1': resolution: {integrity: sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.61.1': resolution: {integrity: sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==} cpu: [loong64] os: [linux] - libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.61.1': resolution: {integrity: sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.61.1': resolution: {integrity: sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==} cpu: [ppc64] os: [linux] - libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.61.1': resolution: {integrity: sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.61.1': resolution: {integrity: sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.61.1': resolution: {integrity: sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.61.1': resolution: {integrity: sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.61.1': resolution: {integrity: sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-openbsd-x64@4.61.1': resolution: {integrity: sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==} @@ -2617,28 +2598,24 @@ packages: engines: {node: '>= 20'} cpu: [arm64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.3.0': resolution: {integrity: sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] - libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.3.0': resolution: {integrity: sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==} engines: {node: '>= 20'} cpu: [x64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.3.0': resolution: {integrity: sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==} engines: {node: '>= 20'} cpu: [x64] os: [linux] - libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.3.0': resolution: {integrity: sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==} @@ -4978,28 +4955,24 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} @@ -6004,56 +5977,48 @@ packages: engines: {node: '>=14.0.0'} cpu: [arm64] os: [linux] - libc: glibc sass-embedded-linux-arm@1.100.0: resolution: {integrity: sha512-9Ul7O1eKrc5YlhwWjkp8tZPSe3UEwSZ1uwUZOQom1HL0pRlBA6F/IlGZYFTLwnHMIP1fc77MMNaBRfc05mKMpw==} engines: {node: '>=14.0.0'} cpu: [arm] os: [linux] - libc: glibc sass-embedded-linux-musl-arm64@1.100.0: resolution: {integrity: sha512-XpACJB2KjSLjf2e9uuvGVdOURsoNrFqgRiihhXyUHK9W0t3LIHb7z5MA/7XGPIT9bWSOO2zyw+rH/FHtDV/Yrg==} engines: {node: '>=14.0.0'} cpu: [arm64] os: [linux] - libc: musl sass-embedded-linux-musl-arm@1.100.0: resolution: {integrity: sha512-sl0JgbGloPyJg66XXx5UDSDScZ0oU85DpMQU4JU/sCUCFj1Z8zZ69SJWKTCNE4/jwnce7WI2zPCV5AG+RHOZJw==} engines: {node: '>=14.0.0'} cpu: [arm] os: [linux] - libc: musl sass-embedded-linux-musl-riscv64@1.100.0: resolution: {integrity: sha512-ShvI0Kx04mwoCARwZ0UjiT97isQvzO80tAt91zmFyHLN9kelc/IrQi940farSm2xQVPCKdeVyeG0ekBsokSpYQ==} engines: {node: '>=14.0.0'} cpu: [riscv64] os: [linux] - libc: musl sass-embedded-linux-musl-x64@1.100.0: resolution: {integrity: sha512-TDBCRWNuS4RDLQXvRc1gjZlWiWTWaWGp0Bwu/IKwJxov81lsvrCs3TihTyNXtW7V5aoN4Ky3r0QOkNb3mwmBnA==} engines: {node: '>=14.0.0'} cpu: [x64] os: [linux] - libc: musl sass-embedded-linux-riscv64@1.100.0: resolution: {integrity: sha512-j4ENJGOheO+fm3j/yorLxCjBP6/XskrZx7dTLlT+lXYwN/qqCqoA/gsNLI0McS3DFM6GBwPiffzWsdWS8t6sEQ==} engines: {node: '>=14.0.0'} cpu: [riscv64] os: [linux] - libc: glibc sass-embedded-linux-x64@1.100.0: resolution: {integrity: sha512-0vUSN8j0WGtCJIOPh//EmUvYGHW0QOe5iul8qyhPk50MAcw49MA0r34AhftjDdx94ILPF6vApFs0gwHPQRlpVA==} engines: {node: '>=14.0.0'} cpu: [x64] os: [linux] - libc: glibc sass-embedded-unknown-all@1.100.0: resolution: {integrity: sha512-c+naBgWId4MIpToXcI0DgqetjdAkwTTAxFAuOaBz7HUXLdyG1oZRrEvSsbe41nEdQOKH0vgofVFCeSQgoXOG9A==} @@ -6523,8 +6488,8 @@ packages: resolution: {integrity: sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==} hasBin: true - tmp@0.2.6: - resolution: {integrity: sha512-5sJPdPjfI5Kx+qbrDesxkglRBxW//g7hCsqspEjwkewGvBMGIKMOTKzLt1hFVJzyadba3lDUN20O9qhvbQUSTA==} + tmp@0.2.7: + resolution: {integrity: sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==} engines: {node: '>=14.14'} to-regex-range@5.0.1: @@ -11233,7 +11198,7 @@ snapshots: dependencies: chardet: 0.7.0 iconv-lite: 0.4.24 - tmp: 0.2.6 + tmp: 0.2.7 extract-zip@2.0.1: dependencies: @@ -13829,7 +13794,7 @@ snapshots: dependencies: tldts-core: 7.0.19 - tmp@0.2.6: {} + tmp@0.2.7: {} to-regex-range@5.0.1: dependencies: From b42a7fdcc45c7406eb5785a7e7f80e6ba1b9b207 Mon Sep 17 00:00:00 2001 From: kolaente Date: Tue, 16 Jun 2026 08:18:18 +0200 Subject: [PATCH 04/53] fix(deps): force esbuild >=0.28.1 to fix transitive advisories The frontend pins esbuild 0.28.1 directly, but vite/histoire and @intlify/bundle-utils pulled in transitive copies (0.27.7 and 0.25.12) still affected by GHSA-gv7w-rqvm-qjhr (RCE via missing binary integrity verification) and GHSA-g7r4-m6w7-qqqr (dev-server file read on Windows). A pnpm override forces all copies to the patched 0.28.1. Dependabot alerts #239 and #241. --- frontend/package.json | 3 +- frontend/pnpm-lock.yaml | 543 +--------------------------------------- 2 files changed, 6 insertions(+), 540 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 7ed2c55d4..dfc9386cd 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -176,7 +176,8 @@ "flatted": "^3.4.1", "ip-address": ">=10.1.1", "postcss": ">=8.5.10", - "tmp": ">=0.2.7" + "tmp": ">=0.2.7", + "esbuild": ">=0.28.1" } } } diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 548944d60..3c17e6cd5 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -13,6 +13,7 @@ overrides: ip-address: '>=10.1.1' postcss: '>=8.5.10' tmp: '>=0.2.7' + esbuild: '>=0.28.1' importers: @@ -245,7 +246,7 @@ importers: specifier: 3.2.3 version: 3.2.3 esbuild: - specifier: 0.28.1 + specifier: '>=0.28.1' version: 0.28.1 eslint: specifier: 9.39.4 @@ -1335,468 +1336,156 @@ packages: peerDependencies: postcss: '>=8.5.10' - '@esbuild/aix-ppc64@0.25.12': - resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - - '@esbuild/aix-ppc64@0.27.7': - resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - '@esbuild/aix-ppc64@0.28.1': resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.25.12': - resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm64@0.27.7': - resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - '@esbuild/android-arm64@0.28.1': resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.25.12': - resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - - '@esbuild/android-arm@0.27.7': - resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - '@esbuild/android-arm@0.28.1': resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.25.12': - resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - - '@esbuild/android-x64@0.27.7': - resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - '@esbuild/android-x64@0.28.1': resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.25.12': - resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-arm64@0.27.7': - resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - '@esbuild/darwin-arm64@0.28.1': resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.25.12': - resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - - '@esbuild/darwin-x64@0.27.7': - resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - '@esbuild/darwin-x64@0.28.1': resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.25.12': - resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-arm64@0.27.7': - resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - '@esbuild/freebsd-arm64@0.28.1': resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.25.12': - resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.27.7': - resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - '@esbuild/freebsd-x64@0.28.1': resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.25.12': - resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm64@0.27.7': - resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - '@esbuild/linux-arm64@0.28.1': resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.25.12': - resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-arm@0.27.7': - resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - '@esbuild/linux-arm@0.28.1': resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.25.12': - resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-ia32@0.27.7': - resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - '@esbuild/linux-ia32@0.28.1': resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.25.12': - resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-loong64@0.27.7': - resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - '@esbuild/linux-loong64@0.28.1': resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.25.12': - resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-mips64el@0.27.7': - resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - '@esbuild/linux-mips64el@0.28.1': resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.25.12': - resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-ppc64@0.27.7': - resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - '@esbuild/linux-ppc64@0.28.1': resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.25.12': - resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-riscv64@0.27.7': - resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - '@esbuild/linux-riscv64@0.28.1': resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.25.12': - resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-s390x@0.27.7': - resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - '@esbuild/linux-s390x@0.28.1': resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.25.12': - resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - - '@esbuild/linux-x64@0.27.7': - resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - '@esbuild/linux-x64@0.28.1': resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.25.12': - resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - - '@esbuild/netbsd-arm64@0.27.7': - resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - '@esbuild/netbsd-arm64@0.28.1': resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.25.12': - resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.27.7': - resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - '@esbuild/netbsd-x64@0.28.1': resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.25.12': - resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - - '@esbuild/openbsd-arm64@0.27.7': - resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - '@esbuild/openbsd-arm64@0.28.1': resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.12': - resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.27.7': - resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - '@esbuild/openbsd-x64@0.28.1': resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.25.12': - resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - - '@esbuild/openharmony-arm64@0.27.7': - resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - '@esbuild/openharmony-arm64@0.28.1': resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.25.12': - resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - - '@esbuild/sunos-x64@0.27.7': - resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - '@esbuild/sunos-x64@0.28.1': resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.25.12': - resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-arm64@0.27.7': - resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - '@esbuild/win32-arm64@0.28.1': resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.25.12': - resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-ia32@0.27.7': - resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - '@esbuild/win32-ia32@0.28.1': resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.25.12': - resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - - '@esbuild/win32-x64@0.27.7': - resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - '@esbuild/win32-x64@0.28.1': resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} engines: {node: '>=18'} @@ -4009,16 +3698,6 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} - esbuild@0.25.12: - resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} - engines: {node: '>=18'} - hasBin: true - - esbuild@0.27.7: - resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} - engines: {node: '>=18'} - hasBin: true - esbuild@0.28.1: resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} engines: {node: '>=18'} @@ -8389,237 +8068,81 @@ snapshots: dependencies: postcss: 8.5.14 - '@esbuild/aix-ppc64@0.25.12': - optional: true - - '@esbuild/aix-ppc64@0.27.7': - optional: true - '@esbuild/aix-ppc64@0.28.1': optional: true - '@esbuild/android-arm64@0.25.12': - optional: true - - '@esbuild/android-arm64@0.27.7': - optional: true - '@esbuild/android-arm64@0.28.1': optional: true - '@esbuild/android-arm@0.25.12': - optional: true - - '@esbuild/android-arm@0.27.7': - optional: true - '@esbuild/android-arm@0.28.1': optional: true - '@esbuild/android-x64@0.25.12': - optional: true - - '@esbuild/android-x64@0.27.7': - optional: true - '@esbuild/android-x64@0.28.1': optional: true - '@esbuild/darwin-arm64@0.25.12': - optional: true - - '@esbuild/darwin-arm64@0.27.7': - optional: true - '@esbuild/darwin-arm64@0.28.1': optional: true - '@esbuild/darwin-x64@0.25.12': - optional: true - - '@esbuild/darwin-x64@0.27.7': - optional: true - '@esbuild/darwin-x64@0.28.1': optional: true - '@esbuild/freebsd-arm64@0.25.12': - optional: true - - '@esbuild/freebsd-arm64@0.27.7': - optional: true - '@esbuild/freebsd-arm64@0.28.1': optional: true - '@esbuild/freebsd-x64@0.25.12': - optional: true - - '@esbuild/freebsd-x64@0.27.7': - optional: true - '@esbuild/freebsd-x64@0.28.1': optional: true - '@esbuild/linux-arm64@0.25.12': - optional: true - - '@esbuild/linux-arm64@0.27.7': - optional: true - '@esbuild/linux-arm64@0.28.1': optional: true - '@esbuild/linux-arm@0.25.12': - optional: true - - '@esbuild/linux-arm@0.27.7': - optional: true - '@esbuild/linux-arm@0.28.1': optional: true - '@esbuild/linux-ia32@0.25.12': - optional: true - - '@esbuild/linux-ia32@0.27.7': - optional: true - '@esbuild/linux-ia32@0.28.1': optional: true - '@esbuild/linux-loong64@0.25.12': - optional: true - - '@esbuild/linux-loong64@0.27.7': - optional: true - '@esbuild/linux-loong64@0.28.1': optional: true - '@esbuild/linux-mips64el@0.25.12': - optional: true - - '@esbuild/linux-mips64el@0.27.7': - optional: true - '@esbuild/linux-mips64el@0.28.1': optional: true - '@esbuild/linux-ppc64@0.25.12': - optional: true - - '@esbuild/linux-ppc64@0.27.7': - optional: true - '@esbuild/linux-ppc64@0.28.1': optional: true - '@esbuild/linux-riscv64@0.25.12': - optional: true - - '@esbuild/linux-riscv64@0.27.7': - optional: true - '@esbuild/linux-riscv64@0.28.1': optional: true - '@esbuild/linux-s390x@0.25.12': - optional: true - - '@esbuild/linux-s390x@0.27.7': - optional: true - '@esbuild/linux-s390x@0.28.1': optional: true - '@esbuild/linux-x64@0.25.12': - optional: true - - '@esbuild/linux-x64@0.27.7': - optional: true - '@esbuild/linux-x64@0.28.1': optional: true - '@esbuild/netbsd-arm64@0.25.12': - optional: true - - '@esbuild/netbsd-arm64@0.27.7': - optional: true - '@esbuild/netbsd-arm64@0.28.1': optional: true - '@esbuild/netbsd-x64@0.25.12': - optional: true - - '@esbuild/netbsd-x64@0.27.7': - optional: true - '@esbuild/netbsd-x64@0.28.1': optional: true - '@esbuild/openbsd-arm64@0.25.12': - optional: true - - '@esbuild/openbsd-arm64@0.27.7': - optional: true - '@esbuild/openbsd-arm64@0.28.1': optional: true - '@esbuild/openbsd-x64@0.25.12': - optional: true - - '@esbuild/openbsd-x64@0.27.7': - optional: true - '@esbuild/openbsd-x64@0.28.1': optional: true - '@esbuild/openharmony-arm64@0.25.12': - optional: true - - '@esbuild/openharmony-arm64@0.27.7': - optional: true - '@esbuild/openharmony-arm64@0.28.1': optional: true - '@esbuild/sunos-x64@0.25.12': - optional: true - - '@esbuild/sunos-x64@0.27.7': - optional: true - '@esbuild/sunos-x64@0.28.1': optional: true - '@esbuild/win32-arm64@0.25.12': - optional: true - - '@esbuild/win32-arm64@0.27.7': - optional: true - '@esbuild/win32-arm64@0.28.1': optional: true - '@esbuild/win32-ia32@0.25.12': - optional: true - - '@esbuild/win32-ia32@0.27.7': - optional: true - '@esbuild/win32-ia32@0.28.1': optional: true - '@esbuild/win32-x64@0.25.12': - optional: true - - '@esbuild/win32-x64@0.27.7': - optional: true - '@esbuild/win32-x64@0.28.1': optional: true @@ -8827,7 +8350,7 @@ snapshots: '@intlify/message-compiler': 11.2.8 '@intlify/shared': 11.2.8 acorn: 8.15.0 - esbuild: 0.25.12 + esbuild: 0.28.1 escodegen: 2.1.0 estree-walker: 2.0.2 jsonc-eslint-parser: 2.4.0 @@ -10977,64 +10500,6 @@ snapshots: is-date-object: 1.0.5 is-symbol: 1.0.4 - esbuild@0.25.12: - optionalDependencies: - '@esbuild/aix-ppc64': 0.25.12 - '@esbuild/android-arm': 0.25.12 - '@esbuild/android-arm64': 0.25.12 - '@esbuild/android-x64': 0.25.12 - '@esbuild/darwin-arm64': 0.25.12 - '@esbuild/darwin-x64': 0.25.12 - '@esbuild/freebsd-arm64': 0.25.12 - '@esbuild/freebsd-x64': 0.25.12 - '@esbuild/linux-arm': 0.25.12 - '@esbuild/linux-arm64': 0.25.12 - '@esbuild/linux-ia32': 0.25.12 - '@esbuild/linux-loong64': 0.25.12 - '@esbuild/linux-mips64el': 0.25.12 - '@esbuild/linux-ppc64': 0.25.12 - '@esbuild/linux-riscv64': 0.25.12 - '@esbuild/linux-s390x': 0.25.12 - '@esbuild/linux-x64': 0.25.12 - '@esbuild/netbsd-arm64': 0.25.12 - '@esbuild/netbsd-x64': 0.25.12 - '@esbuild/openbsd-arm64': 0.25.12 - '@esbuild/openbsd-x64': 0.25.12 - '@esbuild/openharmony-arm64': 0.25.12 - '@esbuild/sunos-x64': 0.25.12 - '@esbuild/win32-arm64': 0.25.12 - '@esbuild/win32-ia32': 0.25.12 - '@esbuild/win32-x64': 0.25.12 - - esbuild@0.27.7: - optionalDependencies: - '@esbuild/aix-ppc64': 0.27.7 - '@esbuild/android-arm': 0.27.7 - '@esbuild/android-arm64': 0.27.7 - '@esbuild/android-x64': 0.27.7 - '@esbuild/darwin-arm64': 0.27.7 - '@esbuild/darwin-x64': 0.27.7 - '@esbuild/freebsd-arm64': 0.27.7 - '@esbuild/freebsd-x64': 0.27.7 - '@esbuild/linux-arm': 0.27.7 - '@esbuild/linux-arm64': 0.27.7 - '@esbuild/linux-ia32': 0.27.7 - '@esbuild/linux-loong64': 0.27.7 - '@esbuild/linux-mips64el': 0.27.7 - '@esbuild/linux-ppc64': 0.27.7 - '@esbuild/linux-riscv64': 0.27.7 - '@esbuild/linux-s390x': 0.27.7 - '@esbuild/linux-x64': 0.27.7 - '@esbuild/netbsd-arm64': 0.27.7 - '@esbuild/netbsd-x64': 0.27.7 - '@esbuild/openbsd-arm64': 0.27.7 - '@esbuild/openbsd-x64': 0.27.7 - '@esbuild/openharmony-arm64': 0.27.7 - '@esbuild/sunos-x64': 0.27.7 - '@esbuild/win32-arm64': 0.27.7 - '@esbuild/win32-ia32': 0.27.7 - '@esbuild/win32-x64': 0.27.7 - esbuild@0.28.1: optionalDependencies: '@esbuild/aix-ppc64': 0.28.1 @@ -14134,7 +13599,7 @@ snapshots: vite@7.3.5(@types/node@24.13.2)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.31.6)(yaml@2.8.3): dependencies: - esbuild: 0.27.7 + esbuild: 0.28.1 fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 postcss: 8.5.14 From 652f61da50ce7caa8e89db1dac3cbe70eeb750b2 Mon Sep 17 00:00:00 2001 From: kolaente Date: Tue, 16 Jun 2026 08:30:00 +0200 Subject: [PATCH 05/53] fix(deps): bump dompurify to 3.4.9 to fix XSS advisories dompurify 3.4.0 was affected by several stacked advisories (mXSS / sanitizer bypasses). 3.4.9 is past all vulnerable ranges. Resolves Dependabot alerts #248-#254 (package.json) and #259-#265 (lockfile). --- frontend/package.json | 2 +- frontend/pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index dfc9386cd..2add08f14 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -82,7 +82,7 @@ "bulma-css-variables": "0.9.33", "change-case": "5.4.4", "dayjs": "1.11.19", - "dompurify": "3.4.0", + "dompurify": "3.4.9", "fast-deep-equal": "3.1.3", "flatpickr": "4.6.13", "floating-vue": "5.2.2", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 3c17e6cd5..09db0f9f9 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -113,8 +113,8 @@ importers: specifier: 1.11.19 version: 1.11.19 dompurify: - specifier: 3.4.0 - version: 3.4.0 + specifier: 3.4.9 + version: 3.4.9 fast-deep-equal: specifier: 3.1.3 version: 3.1.3 @@ -3569,8 +3569,8 @@ packages: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} - dompurify@3.4.0: - resolution: {integrity: sha512-nolgK9JcaUXMSmW+j1yaSvaEaoXYHwWyGJlkoCTghc97KgGDDSnpoU/PlEnw63Ah+TGKFOyY+X5LnxaWbCSfXg==} + dompurify@3.4.9: + resolution: {integrity: sha512-4dPSRMRDqHvs0V4YDFCsaIZo4if5u0xM+llyxiM2fwuZFdKArUBAF3VtI2+n8NKg9P870WMdYk0UhqQNoWXbfQ==} domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} @@ -10334,7 +10334,7 @@ snapshots: dependencies: domelementtype: 2.3.0 - dompurify@3.4.0: + dompurify@3.4.9: optionalDependencies: '@types/trusted-types': 2.0.7 From 460e8f3ab16aca9c08aba9c51caa619d652ab876 Mon Sep 17 00:00:00 2001 From: kolaente Date: Tue, 16 Jun 2026 08:30:33 +0200 Subject: [PATCH 06/53] fix(deps): force form-data >=4.0.6 to fix unsafe boundary advisory Resolves the form-data <4.0.6 advisory (predictable multipart boundary). Transitive in both workspaces; pinned via pnpm overrides. Dependabot alerts #247 (desktop) and #258 (frontend). --- desktop/package.json | 3 ++- desktop/pnpm-lock.yaml | 23 ++++++++++++++++------- frontend/package.json | 3 ++- frontend/pnpm-lock.yaml | 9 +++++---- 4 files changed, 25 insertions(+), 13 deletions(-) diff --git a/desktop/package.json b/desktop/package.json index 8a85d63df..a0f2a6de9 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -78,7 +78,8 @@ "@tootallnate/once": "^3.0.1", "picomatch": ">=4.0.4", "tmp": ">=0.2.7", - "ip-address": ">=10.1.1" + "ip-address": ">=10.1.1", + "form-data": ">=4.0.6" } } } diff --git a/desktop/pnpm-lock.yaml b/desktop/pnpm-lock.yaml index 6a15d26ef..24c81fb9c 100644 --- a/desktop/pnpm-lock.yaml +++ b/desktop/pnpm-lock.yaml @@ -11,6 +11,7 @@ overrides: picomatch: '>=4.0.4' tmp: '>=0.2.7' ip-address: '>=10.1.1' + form-data: '>=4.0.6' importers: @@ -632,8 +633,8 @@ packages: resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} engines: {node: '>=14'} - form-data@4.0.5: - resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + form-data@4.0.6: + resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} engines: {node: '>= 6'} forwarded@0.2.0: @@ -736,6 +737,10 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + hosted-git-info@4.1.0: resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} engines: {node: '>=10'} @@ -1729,7 +1734,7 @@ snapshots: ejs: 3.1.10 electron-builder-squirrel-windows: 24.13.3(dmg-builder@26.15.2) electron-publish: 24.13.1 - form-data: 4.0.5 + form-data: 4.0.6 fs-extra: 10.1.0 hosted-git-info: 4.1.0 is-ci: 3.0.1 @@ -2171,7 +2176,7 @@ snapshots: builder-util: 26.15.0 builder-util-runtime: 9.7.0 chalk: 4.1.2 - form-data: 4.0.5 + form-data: 4.0.6 fs-extra: 10.1.0 lazy-val: 1.0.5 mime: 2.6.0 @@ -2215,7 +2220,7 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.3.0 has-tostringtag: 1.0.2 - hasown: 2.0.2 + hasown: 2.0.4 es6-error@4.1.1: optional: true @@ -2294,12 +2299,12 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - form-data@4.0.5: + form-data@4.0.6: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 es-set-tostringtag: 2.1.0 - hasown: 2.0.2 + hasown: 2.0.4 mime-types: 2.1.35 forwarded@0.2.0: {} @@ -2436,6 +2441,10 @@ snapshots: dependencies: function-bind: 1.1.2 + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + hosted-git-info@4.1.0: dependencies: lru-cache: 6.0.0 diff --git a/frontend/package.json b/frontend/package.json index 2add08f14..fce48f874 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -177,7 +177,8 @@ "ip-address": ">=10.1.1", "postcss": ">=8.5.10", "tmp": ">=0.2.7", - "esbuild": ">=0.28.1" + "esbuild": ">=0.28.1", + "form-data": ">=4.0.6" } } } diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 09db0f9f9..25fd9b891 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -14,6 +14,7 @@ overrides: postcss: '>=8.5.10' tmp: '>=0.2.7' esbuild: '>=0.28.1' + form-data: '>=4.0.6' importers: @@ -3942,8 +3943,8 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} - form-data@4.0.5: - resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + form-data@4.0.6: + resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} engines: {node: '>= 6'} fraction.js@5.3.4: @@ -9830,7 +9831,7 @@ snapshots: axios@1.16.0: dependencies: follow-redirects: 1.16.0 - form-data: 4.0.5 + form-data: 4.0.6 proxy-from-env: 2.1.0 transitivePeerDependencies: - debug @@ -10787,7 +10788,7 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - form-data@4.0.5: + form-data@4.0.6: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 From 340be305f8b1f5776f14fe86d5f93fbb6575a8cf Mon Sep 17 00:00:00 2001 From: kolaente Date: Tue, 16 Jun 2026 08:31:02 +0200 Subject: [PATCH 07/53] fix(deps): tighten tar override to >=7.5.16 The ^7.5.11 override resolved to the vulnerable 7.5.15. Pin to >=7.5.16. Resolves Dependabot alert #246 (desktop). --- desktop/package.json | 2 +- desktop/pnpm-lock.yaml | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/desktop/package.json b/desktop/package.json index a0f2a6de9..73e1c510a 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -74,7 +74,7 @@ ], "overrides": { "minimatch": "^10.2.3", - "tar": "^7.5.11", + "tar": ">=7.5.16", "@tootallnate/once": "^3.0.1", "picomatch": ">=4.0.4", "tmp": ">=0.2.7", diff --git a/desktop/pnpm-lock.yaml b/desktop/pnpm-lock.yaml index 24c81fb9c..866cbbb3a 100644 --- a/desktop/pnpm-lock.yaml +++ b/desktop/pnpm-lock.yaml @@ -6,7 +6,7 @@ settings: overrides: minimatch: ^10.2.3 - tar: ^7.5.11 + tar: '>=7.5.16' '@tootallnate/once': ^3.0.1 picomatch: '>=4.0.4' tmp: '>=0.2.7' @@ -1303,8 +1303,8 @@ packages: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} - tar@7.5.15: - resolution: {integrity: sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==} + tar@7.5.16: + resolution: {integrity: sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w==} engines: {node: '>=18'} temp-file@3.4.0: @@ -1745,7 +1745,7 @@ snapshots: read-config-file: 6.3.2 sanitize-filename: 1.6.4 semver: 7.8.1 - tar: 7.5.15 + tar: 7.5.16 temp-file: 3.4.0 transitivePeerDependencies: - supports-color @@ -1790,7 +1790,7 @@ snapshots: proper-lockfile: 4.1.2 resedit: 1.7.2 semver: 7.7.4 - tar: 7.5.15 + tar: 7.5.16 temp-file: 3.4.0 tiny-async-pool: 1.3.0 unzipper: 0.12.3 @@ -2665,7 +2665,7 @@ snapshots: nopt: 9.0.0 proc-log: 6.1.0 semver: 7.8.1 - tar: 7.5.15 + tar: 7.5.16 tinyglobby: 0.2.15 undici: 6.26.0 which: 6.0.1 @@ -3017,7 +3017,7 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 - tar@7.5.15: + tar@7.5.16: dependencies: '@isaacs/fs-minipass': 4.0.1 chownr: 3.0.0 From be5858aafe4cb0258ea5ed8bfbd44177792c0518 Mon Sep 17 00:00:00 2001 From: kolaente Date: Tue, 16 Jun 2026 08:31:46 +0200 Subject: [PATCH 08/53] fix(deps): force markdown-it >=14.2.0 to fix ReDoS advisory Resolves the markdown-it <=14.1.1 advisory. Transitive; pinned via pnpm override. Dependabot alert #266 (frontend). --- frontend/package.json | 3 ++- frontend/pnpm-lock.yaml | 35 ++++++++++++++++++----------------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index fce48f874..bfcd786e5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -178,7 +178,8 @@ "postcss": ">=8.5.10", "tmp": ">=0.2.7", "esbuild": ">=0.28.1", - "form-data": ">=4.0.6" + "form-data": ">=4.0.6", + "markdown-it": ">=14.2.0" } } } diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 25fd9b891..94aea2a04 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -15,6 +15,7 @@ overrides: tmp: '>=0.2.7' esbuild: '>=0.28.1' form-data: '>=4.0.6' + markdown-it: '>=14.2.0' importers: @@ -4673,8 +4674,8 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - linkify-it@5.0.0: - resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + linkify-it@5.0.1: + resolution: {integrity: sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg==} linkifyjs@4.3.2: resolution: {integrity: sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==} @@ -4742,19 +4743,19 @@ packages: resolution: {integrity: sha512-sa2ErMQ6kKOA4l31gLGYliFQrMKkqSO0ZJgGhDHKijPf0pNFM9vghjAh3gn26pS4JDRs7Iwa9S36gxm3vgZTzg==} peerDependencies: '@types/markdown-it': '*' - markdown-it: '*' + markdown-it: '>=14.2.0' markdown-it-attrs@4.3.1: resolution: {integrity: sha512-/ko6cba+H6gdZ0DOw7BbNMZtfuJTRp9g/IrGIuz8lYc/EfnmWRpaR3CFPnNbVz0LDvF8Gf1hFGPqrQqq7De0rg==} engines: {node: '>=6'} peerDependencies: - markdown-it: '>= 9.0.0' + markdown-it: '>=14.2.0' markdown-it-emoji@3.0.0: resolution: {integrity: sha512-+rUD93bXHubA4arpEZO3q80so0qgoFJEKRkRbjKX8RTdca89v2kfyF+xR3i2sQTwql9tpPZPOQN5B+PunspXRg==} - markdown-it@14.1.1: - resolution: {integrity: sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==} + markdown-it@14.2.0: + resolution: {integrity: sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ==} hasBin: true marked@17.0.1: @@ -11051,9 +11052,9 @@ snapshots: gray-matter: 4.0.3 jiti: 2.6.1 jsdom: 27.4.0 - markdown-it: 14.1.1 - markdown-it-anchor: 9.2.0(@types/markdown-it@14.1.2)(markdown-it@14.1.1) - markdown-it-attrs: 4.3.1(markdown-it@14.1.1) + markdown-it: 14.2.0 + markdown-it-anchor: 9.2.0(@types/markdown-it@14.1.2)(markdown-it@14.2.0) + markdown-it-attrs: 4.3.1(markdown-it@14.2.0) markdown-it-emoji: 3.0.0 micromatch: 4.0.8 mrmime: 2.0.0 @@ -11561,7 +11562,7 @@ snapshots: lines-and-columns@1.2.4: {} - linkify-it@5.0.0: + linkify-it@5.0.1: dependencies: uc.micro: 2.1.0 @@ -11618,22 +11619,22 @@ snapshots: map-obj@4.3.0: {} - markdown-it-anchor@9.2.0(@types/markdown-it@14.1.2)(markdown-it@14.1.1): + markdown-it-anchor@9.2.0(@types/markdown-it@14.1.2)(markdown-it@14.2.0): dependencies: '@types/markdown-it': 14.1.2 - markdown-it: 14.1.1 + markdown-it: 14.2.0 - markdown-it-attrs@4.3.1(markdown-it@14.1.1): + markdown-it-attrs@4.3.1(markdown-it@14.2.0): dependencies: - markdown-it: 14.1.1 + markdown-it: 14.2.0 markdown-it-emoji@3.0.0: {} - markdown-it@14.1.1: + markdown-it@14.2.0: dependencies: argparse: 2.0.1 entities: 4.5.0 - linkify-it: 5.0.0 + linkify-it: 5.0.1 mdurl: 2.0.0 punycode.js: 2.3.1 uc.micro: 2.1.0 @@ -12311,7 +12312,7 @@ snapshots: prosemirror-markdown@1.13.1: dependencies: '@types/markdown-it': 14.1.2 - markdown-it: 14.1.1 + markdown-it: 14.2.0 prosemirror-model: 1.25.0 prosemirror-menu@1.2.4: From d054fb7a5babb0b5956f1cff8a393a473e33b5e9 Mon Sep 17 00:00:00 2001 From: kolaente Date: Tue, 16 Jun 2026 08:32:20 +0200 Subject: [PATCH 09/53] fix(deps): force launch-editor >=2.14.1 Resolves the launch-editor <=2.14.0 advisory. Transitive (via vite-plugin-vue-devtools); pinned via pnpm override. Dependabot alert #257 (frontend). --- frontend/package.json | 3 ++- frontend/pnpm-lock.yaml | 9 +++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index bfcd786e5..0fd71fc1f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -179,7 +179,8 @@ "tmp": ">=0.2.7", "esbuild": ">=0.28.1", "form-data": ">=4.0.6", - "markdown-it": ">=14.2.0" + "markdown-it": ">=14.2.0", + "launch-editor": ">=2.14.1" } } } diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 94aea2a04..d0401d76d 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -16,6 +16,7 @@ overrides: esbuild: '>=0.28.1' form-data: '>=4.0.6' markdown-it: '>=14.2.0' + launch-editor: '>=2.14.1' importers: @@ -4590,8 +4591,8 @@ packages: resolution: {integrity: sha512-7W0vV3rqv5tokqkBAFV1LbR7HPOWzXQDpDgEuib/aJ1jsZZx6x3c2mBI+TJhJzOhkGeaLbCKEHXEXLfirtG2JA==} engines: {node: '>=18'} - launch-editor@2.10.0: - resolution: {integrity: sha512-D7dBRJo/qcGX9xlvt/6wUYzQxjh5G1RvZPgPv8vi4KRU99DVQL/oW7tnVOCCTm2HGeo3C5HvGE5Yrh6UBoZ0vA==} + launch-editor@2.14.1: + resolution: {integrity: sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA==} leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} @@ -8316,7 +8317,7 @@ snapshots: change-case: 5.4.4 globby: 14.1.0 histoire: 1.0.0-beta.1(@types/node@24.13.2)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.31.6)(vite@7.3.5(@types/node@24.13.2)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.31.6)(yaml@2.8.3))(yaml@2.8.3) - launch-editor: 2.10.0 + launch-editor: 2.14.1 pathe: 1.1.2 vue: 3.5.27(typescript@5.9.3) transitivePeerDependencies: @@ -11499,7 +11500,7 @@ snapshots: dependencies: package-json: 10.0.1 - launch-editor@2.10.0: + launch-editor@2.14.1: dependencies: picocolors: 1.1.1 shell-quote: 1.8.4 From 9cc47a3da43a454551fe8b75d159387e1f2ba25a Mon Sep 17 00:00:00 2001 From: kolaente Date: Tue, 16 Jun 2026 08:32:36 +0200 Subject: [PATCH 10/53] fix(deps): force @babel/core >=7.29.6 Resolves the @babel/core <=7.29.0 advisory. Transitive; pinned via pnpm override. Dependabot alert #255 (frontend). --- frontend/package.json | 3 +- frontend/pnpm-lock.yaml | 880 +++++++++++++++++++++++----------------- 2 files changed, 500 insertions(+), 383 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 0fd71fc1f..7a70177a5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -180,7 +180,8 @@ "esbuild": ">=0.28.1", "form-data": ">=4.0.6", "markdown-it": ">=14.2.0", - "launch-editor": ">=2.14.1" + "launch-editor": ">=2.14.1", + "@babel/core": ">=7.29.6" } } } diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index d0401d76d..1dc79da14 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -17,6 +17,7 @@ overrides: form-data: '>=4.0.6' markdown-it: '>=14.2.0' launch-editor: '>=2.14.1' + '@babel/core': '>=7.29.6' importers: @@ -351,10 +352,6 @@ packages: resolution: {integrity: sha512-nznEC1ZA/m3hQDEnrGQ4c5gkaa9pcaVnw4LFJyzBAaR7E3nfiAPEHS3otnSafpZouVnoKeITl5D+2LsnwlnK8g==} engines: {node: '>=14.0.0'} - '@ampproject/remapping@2.3.0': - resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} - engines: {node: '>=6.0.0'} - '@apideck/better-ajv-errors@0.3.6': resolution: {integrity: sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==} engines: {node: '>=10'} @@ -370,20 +367,24 @@ packages: '@asamuzakjp/nwsapi@2.3.9': resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} - '@babel/code-frame@7.26.2': - resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} - engines: {node: '>=6.9.0'} - '@babel/code-frame@7.29.0': resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + '@babel/compat-data@7.26.0': resolution: {integrity: sha512-qETICbZSLe7uXv9VE8T/RWOdIE5qqyTucOt4zLYMafj2MRO271VGgLd4RACJMeBO37UPWhXiKMBk7YlJ0fOzQA==} engines: {node: '>=6.9.0'} - '@babel/core@7.26.0': - resolution: {integrity: sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==} + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} engines: {node: '>=6.9.0'} '@babel/generator@7.26.0': @@ -394,6 +395,10 @@ packages: resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} engines: {node: '>=6.9.0'} + '@babel/generator@7.29.7': + resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} + engines: {node: '>=6.9.0'} + '@babel/helper-annotate-as-pure@7.25.9': resolution: {integrity: sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==} engines: {node: '>=6.9.0'} @@ -406,27 +411,35 @@ packages: resolution: {integrity: sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==} engines: {node: '>=6.9.0'} + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + '@babel/helper-create-class-features-plugin@7.25.9': resolution: {integrity: sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0 + '@babel/core': '>=7.29.6' '@babel/helper-create-regexp-features-plugin@7.25.9': resolution: {integrity: sha512-ORPNZ3h6ZRkOyAa/SaHU+XsLZr0UQzRwuDQ0cczIA17nAzZ+85G5cVkOJIj7QavLZGSe8QXUmNFxSZzjcZF9bw==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0 + '@babel/core': '>=7.29.6' '@babel/helper-define-polyfill-provider@0.6.2': resolution: {integrity: sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==} peerDependencies: - '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + '@babel/core': '>=7.29.6' '@babel/helper-globals@7.28.0': resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} engines: {node: '>=6.9.0'} + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + '@babel/helper-member-expression-to-functions@7.25.9': resolution: {integrity: sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==} engines: {node: '>=6.9.0'} @@ -439,17 +452,27 @@ packages: resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} engines: {node: '>=6.9.0'} + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + '@babel/helper-module-transforms@7.26.0': resolution: {integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0 + '@babel/core': '>=7.29.6' '@babel/helper-module-transforms@7.28.6': resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0 + '@babel/core': '>=7.29.6' + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': '>=7.29.6' '@babel/helper-optimise-call-expression@7.25.9': resolution: {integrity: sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==} @@ -467,13 +490,13 @@ packages: resolution: {integrity: sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0 + '@babel/core': '>=7.29.6' '@babel/helper-replace-supers@7.25.9': resolution: {integrity: sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0 + '@babel/core': '>=7.29.6' '@babel/helper-simple-access@7.25.9': resolution: {integrity: sha512-c6WHXuiaRsJTyHYLJV75t9IqsmTbItYfdj99PnzYGQZkYKvan5/2jKJ7gu31J3/BJ/A18grImSPModuyG/Eo0Q==} @@ -487,20 +510,32 @@ packages: resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.28.5': resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.25.9': resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + '@babel/helper-wrap-function@7.25.9': resolution: {integrity: sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==} engines: {node: '>=6.9.0'} - '@babel/helpers@7.26.10': - resolution: {integrity: sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==} + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} engines: {node: '>=6.9.0'} '@babel/parser@7.28.5': @@ -513,405 +548,410 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.9': resolution: {integrity: sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0 + '@babel/core': '>=7.29.6' '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.25.9': resolution: {integrity: sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0 + '@babel/core': '>=7.29.6' '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.25.9': resolution: {integrity: sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0 + '@babel/core': '>=7.29.6' '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.25.9': resolution: {integrity: sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.13.0 + '@babel/core': '>=7.29.6' '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.25.9': resolution: {integrity: sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0 + '@babel/core': '>=7.29.6' '@babel/plugin-proposal-decorators@7.25.9': resolution: {integrity: sha512-smkNLL/O1ezy9Nhy4CNosc4Va+1wo5w4gzSZeLe6y6dM4mmHfYOCPolXQPHQxonZCF+ZyebxN9vqOolkYrSn5g==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2': resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-syntax-decorators@7.25.9': resolution: {integrity: sha512-ryzI0McXUPJnRCvMo4lumIKZUzhYUO/ScI+Mz4YVaTLt04DHNSjEUjKVvbzQjZFLuod/cYEc07mJWhzl6v4DPg==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-syntax-import-assertions@7.26.0': resolution: {integrity: sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-syntax-import-attributes@7.26.0': resolution: {integrity: sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-syntax-import-meta@7.10.4': resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-syntax-jsx@7.25.9': resolution: {integrity: sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-syntax-typescript@7.25.9': resolution: {integrity: sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-syntax-unicode-sets-regex@7.18.6': resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0 + '@babel/core': '>=7.29.6' '@babel/plugin-transform-arrow-functions@7.25.9': resolution: {integrity: sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-transform-async-generator-functions@7.25.9': resolution: {integrity: sha512-RXV6QAzTBbhDMO9fWwOmwwTuYaiPbggWQ9INdZqAYeSHyG7FzQ+nOZaUUjNwKv9pV3aE4WFqFm1Hnbci5tBCAw==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-transform-async-to-generator@7.25.9': resolution: {integrity: sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-transform-block-scoped-functions@7.25.9': resolution: {integrity: sha512-toHc9fzab0ZfenFpsyYinOX0J/5dgJVA2fm64xPewu7CoYHWEivIWKxkK2rMi4r3yQqLnVmheMXRdG+k239CgA==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-transform-block-scoping@7.25.9': resolution: {integrity: sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-transform-class-properties@7.25.9': resolution: {integrity: sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-transform-class-static-block@7.26.0': resolution: {integrity: sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.12.0 + '@babel/core': '>=7.29.6' '@babel/plugin-transform-classes@7.25.9': resolution: {integrity: sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-transform-computed-properties@7.25.9': resolution: {integrity: sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-transform-destructuring@7.25.9': resolution: {integrity: sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-transform-dotall-regex@7.25.9': resolution: {integrity: sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-transform-duplicate-keys@7.25.9': resolution: {integrity: sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.25.9': resolution: {integrity: sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0 + '@babel/core': '>=7.29.6' '@babel/plugin-transform-dynamic-import@7.25.9': resolution: {integrity: sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-transform-exponentiation-operator@7.25.9': resolution: {integrity: sha512-KRhdhlVk2nObA5AYa7QMgTMTVJdfHprfpAk4DjZVtllqRg9qarilstTKEhpVjyt+Npi8ThRyiV8176Am3CodPA==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-transform-export-namespace-from@7.25.9': resolution: {integrity: sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-transform-for-of@7.25.9': resolution: {integrity: sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-transform-function-name@7.25.9': resolution: {integrity: sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-transform-json-strings@7.25.9': resolution: {integrity: sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-transform-literals@7.25.9': resolution: {integrity: sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-transform-logical-assignment-operators@7.25.9': resolution: {integrity: sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-transform-member-expression-literals@7.25.9': resolution: {integrity: sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-transform-modules-amd@7.25.9': resolution: {integrity: sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-transform-modules-commonjs@7.25.9': resolution: {integrity: sha512-dwh2Ol1jWwL2MgkCzUSOvfmKElqQcuswAZypBSUsScMXvgdT8Ekq5YA6TtqpTVWH+4903NmboMuH1o9i8Rxlyg==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-transform-modules-systemjs@7.29.4': resolution: {integrity: sha512-N7QmZ0xRZfjHOfZeQLJjwgX2zS9pdGHSVl/cjSGlo4dXMqvurfxXDMKY4RqEKzPozV78VMcd0lxyG13mlbKc4w==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-transform-modules-umd@7.25.9': resolution: {integrity: sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-transform-named-capturing-groups-regex@7.25.9': resolution: {integrity: sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0 + '@babel/core': '>=7.29.6' '@babel/plugin-transform-new-target@7.25.9': resolution: {integrity: sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-transform-nullish-coalescing-operator@7.25.9': resolution: {integrity: sha512-ENfftpLZw5EItALAD4WsY/KUWvhUlZndm5GC7G3evUsVeSJB6p0pBeLQUnRnBCBx7zV0RKQjR9kCuwrsIrjWog==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-transform-numeric-separator@7.25.9': resolution: {integrity: sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-transform-object-rest-spread@7.25.9': resolution: {integrity: sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-transform-object-super@7.25.9': resolution: {integrity: sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-transform-optional-catch-binding@7.25.9': resolution: {integrity: sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-transform-optional-chaining@7.25.9': resolution: {integrity: sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-transform-parameters@7.25.9': resolution: {integrity: sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-transform-private-methods@7.25.9': resolution: {integrity: sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-transform-private-property-in-object@7.25.9': resolution: {integrity: sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-transform-property-literals@7.25.9': resolution: {integrity: sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-transform-regenerator@7.25.9': resolution: {integrity: sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-transform-regexp-modifiers@7.26.0': resolution: {integrity: sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0 + '@babel/core': '>=7.29.6' '@babel/plugin-transform-reserved-words@7.25.9': resolution: {integrity: sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-transform-shorthand-properties@7.25.9': resolution: {integrity: sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-transform-spread@7.25.9': resolution: {integrity: sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-transform-sticky-regex@7.25.9': resolution: {integrity: sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-transform-template-literals@7.25.9': resolution: {integrity: sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-transform-typeof-symbol@7.25.9': resolution: {integrity: sha512-v61XqUMiueJROUv66BVIOi0Fv/CUuZuZMl5NkRoCVxLAnMexZ0A3kMe7vvZ0nulxMuMp0Mk6S5hNh48yki08ZA==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-transform-typescript@7.25.9': resolution: {integrity: sha512-7PbZQZP50tzv2KGGnhh82GSyMB01yKY9scIjf1a+GfZCtInOWqUH5+1EBU4t9fyR5Oykkkc9vFTs4OHrhHXljQ==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-transform-unicode-escapes@7.25.9': resolution: {integrity: sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-transform-unicode-property-regex@7.25.9': resolution: {integrity: sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-transform-unicode-regex@7.25.9': resolution: {integrity: sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/plugin-transform-unicode-sets-regex@7.25.9': resolution: {integrity: sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0 + '@babel/core': '>=7.29.6' '@babel/preset-env@7.26.0': resolution: {integrity: sha512-H84Fxq0CQJNdPFT2DrfnylZ3cf5K43rGfWK4LJGPpjKHiZlk0/RzwEus3PDDZZg+/Er7lCA03MVacueUuXdzfw==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@babel/preset-modules@0.1.6-no-external-plugins': resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==} peerDependencies: - '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 + '@babel/core': '>=7.29.6' '@babel/runtime@7.25.4': resolution: {integrity: sha512-DSgLeL/FNcpXuzav5wfYvHCGvynXkJbn3Zvc3823AEe9nPwW9IK4UoCSS5yGymmQzN0pCPvivtgS6/8U2kkm1w==} @@ -925,6 +965,10 @@ packages: resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} engines: {node: '>=6.9.0'} + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + '@babel/traverse@7.25.9': resolution: {integrity: sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==} engines: {node: '>=6.9.0'} @@ -933,6 +977,10 @@ packages: resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} engines: {node: '>=6.9.0'} + '@babel/traverse@7.29.7': + resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} + engines: {node: '>=6.9.0'} + '@babel/types@7.28.5': resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} engines: {node: '>=6.9.0'} @@ -941,6 +989,10 @@ packages: resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + engines: {node: '>=6.9.0'} + '@bufbuild/protobuf@2.5.2': resolution: {integrity: sha512-foZ7qr0IsUBjzWIq+SuBLfdQCpJ1j8cTuNNT4owngTHoN5KsJb8L9t65fzz7SCeSWzescoOil/0ldqiL041ABg==} @@ -1949,7 +2001,7 @@ packages: resolution: {integrity: sha512-dFZNuFD2YRcoomP4oYf+DvQNSUA9ih+A3vUqopQx5EdtPGo3WBnQcI/S8pwpz91UsGfL0HsMSOlaMld8HrbubA==} engines: {node: '>=14.0.0'} peerDependencies: - '@babel/core': ^7.0.0 + '@babel/core': '>=7.29.6' '@types/babel__core': ^7.1.9 rollup: 4.61.1 peerDependenciesMeta: @@ -2818,7 +2870,7 @@ packages: '@vue/babel-plugin-jsx@1.2.5': resolution: {integrity: sha512-zTrNmOd4939H9KsRIGmmzn3q2zvv1mjxkYZHgqHZgDrXz5B1Q3WyGEjO2f+JrmKghvl1JIRcvo63LgM1kH5zFg==} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' peerDependenciesMeta: '@babel/core': optional: true @@ -2826,7 +2878,7 @@ packages: '@vue/babel-plugin-resolve-type@1.2.5': resolution: {integrity: sha512-U/ibkQrf5sx0XXRnUZD1mo5F7PkpKyTbfXM3a3rC4YnUz6crHEz9Jg09jzzL6QYlXNto/9CePdOg/c87O4Nlfg==} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': '>=7.29.6' '@vue/compiler-core@3.5.27': resolution: {integrity: sha512-gnSBQjZA+//qDZen+6a2EdHqJ68Z7uybrMf3SPjEGgG4dicklwDVmMC1AeIHxtLVPT7sn6sH1KOO+tS6gwOUeQ==} @@ -3064,17 +3116,17 @@ packages: babel-plugin-polyfill-corejs2@0.4.11: resolution: {integrity: sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==} peerDependencies: - '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + '@babel/core': '>=7.29.6' babel-plugin-polyfill-corejs3@0.10.6: resolution: {integrity: sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==} peerDependencies: - '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + '@babel/core': '>=7.29.6' babel-plugin-polyfill-regenerator@0.6.2: resolution: {integrity: sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg==} peerDependencies: - '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + '@babel/core': '>=7.29.6' balanced-match@4.0.3: resolution: {integrity: sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==} @@ -6863,11 +6915,6 @@ snapshots: '@akryum/tinypool@0.3.1': {} - '@ampproject/remapping@2.3.0': - dependencies: - '@jridgewell/gen-mapping': 0.3.5 - '@jridgewell/trace-mapping': 0.3.25 - '@apideck/better-ajv-errors@0.3.6(ajv@8.18.0)': dependencies: ajv: 8.18.0 @@ -6893,32 +6940,34 @@ snapshots: '@asamuzakjp/nwsapi@2.3.9': {} - '@babel/code-frame@7.26.2': - dependencies: - '@babel/helper-validator-identifier': 7.28.5 - js-tokens: 4.0.0 - picocolors: 1.1.1 - '@babel/code-frame@7.29.0': dependencies: '@babel/helper-validator-identifier': 7.28.5 js-tokens: 4.0.0 picocolors: 1.1.1 + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + '@babel/compat-data@7.26.0': {} - '@babel/core@7.26.0': + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7': dependencies: - '@ampproject/remapping': 2.3.0 - '@babel/code-frame': 7.26.2 - '@babel/generator': 7.26.0 - '@babel/helper-compilation-targets': 7.25.9 - '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) - '@babel/helpers': 7.26.10 - '@babel/parser': 7.28.5 - '@babel/template': 7.26.9 - '@babel/traverse': 7.25.9 - '@babel/types': 7.28.5 + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@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/template': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 debug: 4.4.3 gensync: 1.0.0-beta.2 @@ -6943,6 +6992,14 @@ snapshots: '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 + '@babel/generator@7.29.7': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + '@babel/helper-annotate-as-pure@7.25.9': dependencies: '@babel/types': 7.28.5 @@ -6962,29 +7019,37 @@ snapshots: lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-create-class-features-plugin@7.25.9(@babel/core@7.26.0)': + '@babel/helper-compilation-targets@7.29.7': dependencies: - '@babel/core': 7.26.0 + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.28.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.25.9(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 '@babel/helper-annotate-as-pure': 7.25.9 '@babel/helper-member-expression-to-functions': 7.25.9 '@babel/helper-optimise-call-expression': 7.25.9 - '@babel/helper-replace-supers': 7.25.9(@babel/core@7.26.0) + '@babel/helper-replace-supers': 7.25.9(@babel/core@7.29.7) '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 '@babel/traverse': 7.25.9 semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/helper-create-regexp-features-plugin@7.25.9(@babel/core@7.26.0)': + '@babel/helper-create-regexp-features-plugin@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-annotate-as-pure': 7.25.9 regexpu-core: 6.1.1 semver: 6.3.1 - '@babel/helper-define-polyfill-provider@0.6.2(@babel/core@7.26.0)': + '@babel/helper-define-polyfill-provider@0.6.2(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-compilation-targets': 7.25.9 '@babel/helper-plugin-utils': 7.25.9 debug: 4.4.3 @@ -6995,6 +7060,8 @@ snapshots: '@babel/helper-globals@7.28.0': {} + '@babel/helper-globals@7.29.7': {} + '@babel/helper-member-expression-to-functions@7.25.9': dependencies: '@babel/traverse': 7.25.9 @@ -7016,24 +7083,40 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.26.0(@babel/core@7.26.0)': + '@babel/helper-module-imports@7.29.7': dependencies: - '@babel/core': 7.26.0 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.26.0(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 '@babel/helper-module-imports': 7.25.9 '@babel/helper-validator-identifier': 7.28.5 '@babel/traverse': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.28.6(@babel/core@7.26.0)': + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-module-imports': 7.28.6 '@babel/helper-validator-identifier': 7.28.5 '@babel/traverse': 7.29.0 transitivePeerDependencies: - supports-color + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color + '@babel/helper-optimise-call-expression@7.25.9': dependencies: '@babel/types': 7.28.5 @@ -7042,18 +7125,18 @@ snapshots: '@babel/helper-plugin-utils@7.28.6': {} - '@babel/helper-remap-async-to-generator@7.25.9(@babel/core@7.26.0)': + '@babel/helper-remap-async-to-generator@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-annotate-as-pure': 7.25.9 '@babel/helper-wrap-function': 7.25.9 '@babel/traverse': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/helper-replace-supers@7.25.9(@babel/core@7.26.0)': + '@babel/helper-replace-supers@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-member-expression-to-functions': 7.25.9 '@babel/helper-optimise-call-expression': 7.25.9 '@babel/traverse': 7.25.9 @@ -7076,10 +7159,16 @@ snapshots: '@babel/helper-string-parser@7.27.1': {} + '@babel/helper-string-parser@7.29.7': {} + '@babel/helper-validator-identifier@7.28.5': {} + '@babel/helper-validator-identifier@7.29.7': {} + '@babel/helper-validator-option@7.25.9': {} + '@babel/helper-validator-option@7.29.7': {} + '@babel/helper-wrap-function@7.25.9': dependencies: '@babel/template': 7.26.9 @@ -7088,10 +7177,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helpers@7.26.10': + '@babel/helpers@7.29.7': dependencies: - '@babel/template': 7.26.9 - '@babel/types': 7.28.5 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 '@babel/parser@7.28.5': dependencies: @@ -7101,502 +7190,506 @@ snapshots: dependencies: '@babel/types': 7.29.0 - '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.9(@babel/core@7.26.0)': + '@babel/parser@7.29.7': dependencies: - '@babel/core': 7.26.0 + '@babel/types': 7.29.7 + + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.9(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.25.9 '@babel/traverse': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.25.9 '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 - '@babel/plugin-transform-optional-chaining': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-optional-chaining': 7.25.9(@babel/core@7.29.7) transitivePeerDependencies: - supports-color - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.25.9 '@babel/traverse': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/plugin-proposal-decorators@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-proposal-decorators@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 - '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/core': 7.29.7 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.29.7) '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-syntax-decorators': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-syntax-decorators': 7.25.9(@babel/core@7.29.7) transitivePeerDependencies: - supports-color - '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.26.0)': + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 - '@babel/plugin-syntax-decorators@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-syntax-decorators@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-syntax-import-assertions@7.26.0(@babel/core@7.26.0)': + '@babel/plugin-syntax-import-assertions@7.26.0(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-syntax-import-attributes@7.26.0(@babel/core@7.26.0)': + '@babel/plugin-syntax-import-attributes@7.26.0(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.26.0)': + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-syntax-jsx@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-syntax-jsx@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-syntax-typescript@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-syntax-typescript@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.26.0)': + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 - '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/core': 7.29.7 + '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.29.7) '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-arrow-functions@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-arrow-functions@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-async-generator-functions@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-async-generator-functions@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.25.9 - '@babel/helper-remap-async-to-generator': 7.25.9(@babel/core@7.26.0) + '@babel/helper-remap-async-to-generator': 7.25.9(@babel/core@7.29.7) '@babel/traverse': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-async-to-generator@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-async-to-generator@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-module-imports': 7.25.9 '@babel/helper-plugin-utils': 7.25.9 - '@babel/helper-remap-async-to-generator': 7.25.9(@babel/core@7.26.0) + '@babel/helper-remap-async-to-generator': 7.25.9(@babel/core@7.29.7) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-block-scoped-functions@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-block-scoped-functions@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-block-scoping@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-block-scoping@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-class-properties@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-class-properties@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 - '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/core': 7.29.7 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.29.7) '@babel/helper-plugin-utils': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-class-static-block@7.26.0(@babel/core@7.26.0)': + '@babel/plugin-transform-class-static-block@7.26.0(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 - '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/core': 7.29.7 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.29.7) '@babel/helper-plugin-utils': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-classes@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-classes@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-annotate-as-pure': 7.25.9 '@babel/helper-compilation-targets': 7.25.9 '@babel/helper-plugin-utils': 7.25.9 - '@babel/helper-replace-supers': 7.25.9(@babel/core@7.26.0) + '@babel/helper-replace-supers': 7.25.9(@babel/core@7.29.7) '@babel/traverse': 7.25.9 globals: 11.12.0 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-computed-properties@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-computed-properties@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.25.9 '@babel/template': 7.26.9 - '@babel/plugin-transform-destructuring@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-destructuring@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-dotall-regex@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-dotall-regex@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 - '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/core': 7.29.7 + '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.29.7) '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-duplicate-keys@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-duplicate-keys@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 - '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/core': 7.29.7 + '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.29.7) '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-dynamic-import@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-dynamic-import@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-exponentiation-operator@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-exponentiation-operator@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-builder-binary-assignment-operator-visitor': 7.25.9 '@babel/helper-plugin-utils': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-export-namespace-from@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-export-namespace-from@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-for-of@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-for-of@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.25.9 '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-function-name@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-function-name@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-compilation-targets': 7.25.9 '@babel/helper-plugin-utils': 7.25.9 '@babel/traverse': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-json-strings@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-json-strings@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-literals@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-literals@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-logical-assignment-operators@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-logical-assignment-operators@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-member-expression-literals@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-member-expression-literals@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-modules-amd@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-modules-amd@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 - '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/core': 7.29.7 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.29.7) '@babel/helper-plugin-utils': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-commonjs@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-modules-commonjs@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 - '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/core': 7.29.7 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.29.7) '@babel/helper-plugin-utils': 7.25.9 '@babel/helper-simple-access': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-systemjs@7.29.4(@babel/core@7.26.0)': + '@babel/plugin-transform-modules-systemjs@7.29.4(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.26.0) + '@babel/core': 7.29.7 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.7) '@babel/helper-plugin-utils': 7.28.6 '@babel/helper-validator-identifier': 7.28.5 '@babel/traverse': 7.29.0 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-umd@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-modules-umd@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 - '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/core': 7.29.7 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.29.7) '@babel/helper-plugin-utils': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-named-capturing-groups-regex@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-named-capturing-groups-regex@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 - '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/core': 7.29.7 + '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.29.7) '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-new-target@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-new-target@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-nullish-coalescing-operator@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-nullish-coalescing-operator@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-numeric-separator@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-numeric-separator@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-object-rest-spread@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-object-rest-spread@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-compilation-targets': 7.25.9 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-parameters': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-parameters': 7.25.9(@babel/core@7.29.7) - '@babel/plugin-transform-object-super@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-object-super@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.25.9 - '@babel/helper-replace-supers': 7.25.9(@babel/core@7.26.0) + '@babel/helper-replace-supers': 7.25.9(@babel/core@7.29.7) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-optional-catch-binding@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-optional-catch-binding@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-optional-chaining@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-optional-chaining@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.25.9 '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-parameters@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-parameters@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-private-methods@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-private-methods@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 - '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/core': 7.29.7 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.29.7) '@babel/helper-plugin-utils': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-private-property-in-object@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-private-property-in-object@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-annotate-as-pure': 7.25.9 - '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.29.7) '@babel/helper-plugin-utils': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-property-literals@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-property-literals@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-regenerator@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-regenerator@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.25.9 regenerator-transform: 0.15.2 - '@babel/plugin-transform-regexp-modifiers@7.26.0(@babel/core@7.26.0)': + '@babel/plugin-transform-regexp-modifiers@7.26.0(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 - '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/core': 7.29.7 + '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.29.7) '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-reserved-words@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-reserved-words@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-shorthand-properties@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-shorthand-properties@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-spread@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-spread@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.25.9 '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-sticky-regex@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-sticky-regex@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-template-literals@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-template-literals@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-typeof-symbol@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-typeof-symbol@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-typescript@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-typescript@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-annotate-as-pure': 7.25.9 - '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.29.7) '@babel/helper-plugin-utils': 7.25.9 '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 - '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.29.7) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-unicode-escapes@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-unicode-escapes@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-unicode-property-regex@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-unicode-property-regex@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 - '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/core': 7.29.7 + '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.29.7) '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-unicode-regex@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-unicode-regex@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 - '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/core': 7.29.7 + '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.29.7) '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-unicode-sets-regex@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-unicode-sets-regex@7.25.9(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 - '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/core': 7.29.7 + '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.29.7) '@babel/helper-plugin-utils': 7.25.9 - '@babel/preset-env@7.26.0(@babel/core@7.26.0)': + '@babel/preset-env@7.26.0(@babel/core@7.29.7)': dependencies: '@babel/compat-data': 7.26.0 - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-compilation-targets': 7.25.9 '@babel/helper-plugin-utils': 7.25.9 '@babel/helper-validator-option': 7.25.9 - '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.26.0) - '@babel/plugin-syntax-import-assertions': 7.26.0(@babel/core@7.26.0) - '@babel/plugin-syntax-import-attributes': 7.26.0(@babel/core@7.26.0) - '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.26.0) - '@babel/plugin-transform-arrow-functions': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-async-generator-functions': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-async-to-generator': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-block-scoped-functions': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-block-scoping': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-class-properties': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-class-static-block': 7.26.0(@babel/core@7.26.0) - '@babel/plugin-transform-classes': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-computed-properties': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-destructuring': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-dotall-regex': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-duplicate-keys': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-dynamic-import': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-exponentiation-operator': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-export-namespace-from': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-for-of': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-function-name': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-json-strings': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-literals': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-logical-assignment-operators': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-member-expression-literals': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-modules-amd': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-modules-commonjs': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-modules-systemjs': 7.29.4(@babel/core@7.26.0) - '@babel/plugin-transform-modules-umd': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-named-capturing-groups-regex': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-new-target': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-nullish-coalescing-operator': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-numeric-separator': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-object-rest-spread': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-object-super': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-optional-catch-binding': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-optional-chaining': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-parameters': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-private-methods': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-private-property-in-object': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-property-literals': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-regenerator': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-regexp-modifiers': 7.26.0(@babel/core@7.26.0) - '@babel/plugin-transform-reserved-words': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-shorthand-properties': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-spread': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-sticky-regex': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-template-literals': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-typeof-symbol': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-unicode-escapes': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-unicode-property-regex': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-unicode-regex': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-unicode-sets-regex': 7.25.9(@babel/core@7.26.0) - '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.26.0) - babel-plugin-polyfill-corejs2: 0.4.11(@babel/core@7.26.0) - babel-plugin-polyfill-corejs3: 0.10.6(@babel/core@7.26.0) - babel-plugin-polyfill-regenerator: 0.6.2(@babel/core@7.26.0) + '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.25.9(@babel/core@7.29.7) + '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.25.9(@babel/core@7.29.7) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.25.9(@babel/core@7.29.7) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.25.9(@babel/core@7.29.7) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.25.9(@babel/core@7.29.7) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.7) + '@babel/plugin-syntax-import-assertions': 7.26.0(@babel/core@7.29.7) + '@babel/plugin-syntax-import-attributes': 7.26.0(@babel/core@7.29.7) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.29.7) + '@babel/plugin-transform-arrow-functions': 7.25.9(@babel/core@7.29.7) + '@babel/plugin-transform-async-generator-functions': 7.25.9(@babel/core@7.29.7) + '@babel/plugin-transform-async-to-generator': 7.25.9(@babel/core@7.29.7) + '@babel/plugin-transform-block-scoped-functions': 7.25.9(@babel/core@7.29.7) + '@babel/plugin-transform-block-scoping': 7.25.9(@babel/core@7.29.7) + '@babel/plugin-transform-class-properties': 7.25.9(@babel/core@7.29.7) + '@babel/plugin-transform-class-static-block': 7.26.0(@babel/core@7.29.7) + '@babel/plugin-transform-classes': 7.25.9(@babel/core@7.29.7) + '@babel/plugin-transform-computed-properties': 7.25.9(@babel/core@7.29.7) + '@babel/plugin-transform-destructuring': 7.25.9(@babel/core@7.29.7) + '@babel/plugin-transform-dotall-regex': 7.25.9(@babel/core@7.29.7) + '@babel/plugin-transform-duplicate-keys': 7.25.9(@babel/core@7.29.7) + '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.25.9(@babel/core@7.29.7) + '@babel/plugin-transform-dynamic-import': 7.25.9(@babel/core@7.29.7) + '@babel/plugin-transform-exponentiation-operator': 7.25.9(@babel/core@7.29.7) + '@babel/plugin-transform-export-namespace-from': 7.25.9(@babel/core@7.29.7) + '@babel/plugin-transform-for-of': 7.25.9(@babel/core@7.29.7) + '@babel/plugin-transform-function-name': 7.25.9(@babel/core@7.29.7) + '@babel/plugin-transform-json-strings': 7.25.9(@babel/core@7.29.7) + '@babel/plugin-transform-literals': 7.25.9(@babel/core@7.29.7) + '@babel/plugin-transform-logical-assignment-operators': 7.25.9(@babel/core@7.29.7) + '@babel/plugin-transform-member-expression-literals': 7.25.9(@babel/core@7.29.7) + '@babel/plugin-transform-modules-amd': 7.25.9(@babel/core@7.29.7) + '@babel/plugin-transform-modules-commonjs': 7.25.9(@babel/core@7.29.7) + '@babel/plugin-transform-modules-systemjs': 7.29.4(@babel/core@7.29.7) + '@babel/plugin-transform-modules-umd': 7.25.9(@babel/core@7.29.7) + '@babel/plugin-transform-named-capturing-groups-regex': 7.25.9(@babel/core@7.29.7) + '@babel/plugin-transform-new-target': 7.25.9(@babel/core@7.29.7) + '@babel/plugin-transform-nullish-coalescing-operator': 7.25.9(@babel/core@7.29.7) + '@babel/plugin-transform-numeric-separator': 7.25.9(@babel/core@7.29.7) + '@babel/plugin-transform-object-rest-spread': 7.25.9(@babel/core@7.29.7) + '@babel/plugin-transform-object-super': 7.25.9(@babel/core@7.29.7) + '@babel/plugin-transform-optional-catch-binding': 7.25.9(@babel/core@7.29.7) + '@babel/plugin-transform-optional-chaining': 7.25.9(@babel/core@7.29.7) + '@babel/plugin-transform-parameters': 7.25.9(@babel/core@7.29.7) + '@babel/plugin-transform-private-methods': 7.25.9(@babel/core@7.29.7) + '@babel/plugin-transform-private-property-in-object': 7.25.9(@babel/core@7.29.7) + '@babel/plugin-transform-property-literals': 7.25.9(@babel/core@7.29.7) + '@babel/plugin-transform-regenerator': 7.25.9(@babel/core@7.29.7) + '@babel/plugin-transform-regexp-modifiers': 7.26.0(@babel/core@7.29.7) + '@babel/plugin-transform-reserved-words': 7.25.9(@babel/core@7.29.7) + '@babel/plugin-transform-shorthand-properties': 7.25.9(@babel/core@7.29.7) + '@babel/plugin-transform-spread': 7.25.9(@babel/core@7.29.7) + '@babel/plugin-transform-sticky-regex': 7.25.9(@babel/core@7.29.7) + '@babel/plugin-transform-template-literals': 7.25.9(@babel/core@7.29.7) + '@babel/plugin-transform-typeof-symbol': 7.25.9(@babel/core@7.29.7) + '@babel/plugin-transform-unicode-escapes': 7.25.9(@babel/core@7.29.7) + '@babel/plugin-transform-unicode-property-regex': 7.25.9(@babel/core@7.29.7) + '@babel/plugin-transform-unicode-regex': 7.25.9(@babel/core@7.29.7) + '@babel/plugin-transform-unicode-sets-regex': 7.25.9(@babel/core@7.29.7) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.29.7) + babel-plugin-polyfill-corejs2: 0.4.11(@babel/core@7.29.7) + babel-plugin-polyfill-corejs3: 0.10.6(@babel/core@7.29.7) + babel-plugin-polyfill-regenerator: 0.6.2(@babel/core@7.29.7) core-js-compat: 3.38.1 semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.26.0)': + '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.25.9 '@babel/types': 7.28.5 esutils: 2.0.3 @@ -7617,6 +7710,12 @@ snapshots: '@babel/parser': 7.29.3 '@babel/types': 7.29.0 + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@babel/traverse@7.25.9': dependencies: '@babel/code-frame': 7.29.0 @@ -7641,6 +7740,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/traverse@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + '@babel/types@7.28.5': dependencies: '@babel/helper-string-parser': 7.27.1 @@ -7651,6 +7762,11 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@babel/types@7.29.7': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@bufbuild/protobuf@2.5.2': {} '@cacheable/memory@2.0.9': @@ -8637,9 +8753,9 @@ snapshots: '@rolldown/pluginutils@1.0.1': {} - '@rollup/plugin-babel@6.1.0(@babel/core@7.26.0)(rollup@4.61.1)': + '@rollup/plugin-babel@6.1.0(@babel/core@7.29.7)(rollup@4.61.1)': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-module-imports': 7.25.9 '@rollup/pluginutils': 5.1.3(rollup@4.61.1) optionalDependencies: @@ -8785,7 +8901,7 @@ snapshots: '@sentry/bundler-plugin-core@3.6.1': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@sentry/babel-plugin-component-annotate': 3.6.1 '@sentry/cli': 2.58.5 dotenv: 16.6.1 @@ -9541,27 +9657,27 @@ snapshots: '@vue/babel-helper-vue-transform-on@1.2.5': {} - '@vue/babel-plugin-jsx@1.2.5(@babel/core@7.26.0)': + '@vue/babel-plugin-jsx@1.2.5(@babel/core@7.29.7)': dependencies: '@babel/helper-module-imports': 7.25.9 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.29.7) '@babel/template': 7.26.9 '@babel/traverse': 7.25.9 '@babel/types': 7.28.5 '@vue/babel-helper-vue-transform-on': 1.2.5 - '@vue/babel-plugin-resolve-type': 1.2.5(@babel/core@7.26.0) + '@vue/babel-plugin-resolve-type': 1.2.5(@babel/core@7.29.7) html-tags: 3.3.1 svg-tags: 1.0.0 optionalDependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 transitivePeerDependencies: - supports-color - '@vue/babel-plugin-resolve-type@1.2.5(@babel/core@7.26.0)': + '@vue/babel-plugin-resolve-type@1.2.5(@babel/core@7.29.7)': dependencies: '@babel/code-frame': 7.29.0 - '@babel/core': 7.26.0 + '@babel/core': 7.29.7 '@babel/helper-module-imports': 7.25.9 '@babel/helper-plugin-utils': 7.25.9 '@babel/parser': 7.28.5 @@ -9840,27 +9956,27 @@ snapshots: b4a@1.6.7: {} - babel-plugin-polyfill-corejs2@0.4.11(@babel/core@7.26.0): + babel-plugin-polyfill-corejs2@0.4.11(@babel/core@7.29.7): dependencies: '@babel/compat-data': 7.26.0 - '@babel/core': 7.26.0 - '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.26.0) + '@babel/core': 7.29.7 + '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.29.7) semver: 6.3.1 transitivePeerDependencies: - supports-color - babel-plugin-polyfill-corejs3@0.10.6(@babel/core@7.26.0): + babel-plugin-polyfill-corejs3@0.10.6(@babel/core@7.29.7): dependencies: - '@babel/core': 7.26.0 - '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.26.0) + '@babel/core': 7.29.7 + '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.29.7) core-js-compat: 3.38.1 transitivePeerDependencies: - supports-color - babel-plugin-polyfill-regenerator@0.6.2(@babel/core@7.26.0): + babel-plugin-polyfill-regenerator@0.6.2(@babel/core@7.29.7): dependencies: - '@babel/core': 7.26.0 - '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.26.0) + '@babel/core': 7.29.7 + '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.29.7) transitivePeerDependencies: - supports-color @@ -13579,12 +13695,12 @@ snapshots: vite-plugin-vue-inspector@6.0.0(vite@7.3.5(@types/node@24.13.2)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.31.6)(yaml@2.8.3)): dependencies: - '@babel/core': 7.26.0 - '@babel/plugin-proposal-decorators': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-syntax-import-attributes': 7.26.0(@babel/core@7.26.0) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.26.0) - '@babel/plugin-transform-typescript': 7.25.9(@babel/core@7.26.0) - '@vue/babel-plugin-jsx': 1.2.5(@babel/core@7.26.0) + '@babel/core': 7.29.7 + '@babel/plugin-proposal-decorators': 7.25.9(@babel/core@7.29.7) + '@babel/plugin-syntax-import-attributes': 7.26.0(@babel/core@7.29.7) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.7) + '@babel/plugin-transform-typescript': 7.25.9(@babel/core@7.29.7) + '@vue/babel-plugin-jsx': 1.2.5(@babel/core@7.29.7) '@vue/compiler-dom': 3.5.27 kolorist: 1.8.0 magic-string: 0.30.21 @@ -13843,10 +13959,10 @@ snapshots: workbox-build@7.4.1: dependencies: '@apideck/better-ajv-errors': 0.3.6(ajv@8.18.0) - '@babel/core': 7.26.0 - '@babel/preset-env': 7.26.0(@babel/core@7.26.0) + '@babel/core': 7.29.7 + '@babel/preset-env': 7.26.0(@babel/core@7.29.7) '@babel/runtime': 7.25.4 - '@rollup/plugin-babel': 6.1.0(@babel/core@7.26.0)(rollup@4.61.1) + '@rollup/plugin-babel': 6.1.0(@babel/core@7.29.7)(rollup@4.61.1) '@rollup/plugin-node-resolve': 16.0.3(rollup@4.61.1) '@rollup/plugin-replace': 6.0.3(rollup@4.61.1) '@rollup/plugin-terser': 1.0.0(rollup@4.61.1) From e13d3f537c61902037651177ff883a3ade16134a Mon Sep 17 00:00:00 2001 From: kolaente Date: Tue, 16 Jun 2026 08:33:16 +0200 Subject: [PATCH 11/53] fix(deps): bump js-yaml to >=4.2.0 where possible Desktop only has the v4 copy, so a plain override pins it to >=4.2.0 (resolves alert #245). The frontend also pulls js-yaml v3 via gray-matter (histoire story tooling), which has no v4-compatible release, so a scoped 'js-yaml@4' override bumps only the v4 copies (eslint/cosmiconfig) and leaves gray-matter on 3.14.2. Alert #256 stays open for that dev-only, trusted-input path. --- desktop/package.json | 3 ++- desktop/pnpm-lock.yaml | 19 ++++++++++--------- frontend/package.json | 3 ++- frontend/pnpm-lock.yaml | 11 ++++++----- 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/desktop/package.json b/desktop/package.json index 73e1c510a..20465374b 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -79,7 +79,8 @@ "picomatch": ">=4.0.4", "tmp": ">=0.2.7", "ip-address": ">=10.1.1", - "form-data": ">=4.0.6" + "form-data": ">=4.0.6", + "js-yaml": ">=4.2.0" } } } diff --git a/desktop/pnpm-lock.yaml b/desktop/pnpm-lock.yaml index 866cbbb3a..4f2e92832 100644 --- a/desktop/pnpm-lock.yaml +++ b/desktop/pnpm-lock.yaml @@ -12,6 +12,7 @@ overrides: tmp: '>=0.2.7' ip-address: '>=10.1.1' form-data: '>=4.0.6' + js-yaml: '>=4.2.0' importers: @@ -835,8 +836,8 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true - js-yaml@4.1.1: - resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + js-yaml@4.2.0: + resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==} hasBin: true json-buffer@3.0.1: @@ -1739,7 +1740,7 @@ snapshots: hosted-git-info: 4.1.0 is-ci: 3.0.1 isbinaryfile: 5.0.7 - js-yaml: 4.1.1 + js-yaml: 4.2.0 lazy-val: 1.0.5 minimatch: 10.2.5 read-config-file: 6.3.2 @@ -1781,7 +1782,7 @@ snapshots: hosted-git-info: 4.1.0 isbinaryfile: 5.0.7 jiti: 2.6.1 - js-yaml: 4.1.1 + js-yaml: 4.2.0 json5: 2.2.3 lazy-val: 1.0.5 minimatch: 10.2.5 @@ -1928,7 +1929,7 @@ snapshots: http-proxy-agent: 5.0.0 https-proxy-agent: 5.0.1 is-ci: 3.0.1 - js-yaml: 4.1.1 + js-yaml: 4.2.0 source-map-support: 0.5.21 stat-mode: 1.0.0 temp-file: 3.4.0 @@ -1945,7 +1946,7 @@ snapshots: fs-extra: 10.1.0 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.5 - js-yaml: 4.1.1 + js-yaml: 4.2.0 sanitize-filename: 1.6.4 source-map-support: 0.5.21 stat-mode: 1.0.0 @@ -2098,7 +2099,7 @@ snapshots: app-builder-lib: 26.15.2(dmg-builder@26.15.2)(electron-builder-squirrel-windows@24.13.3) builder-util: 26.15.0 fs-extra: 10.1.0 - js-yaml: 4.1.1 + js-yaml: 4.2.0 transitivePeerDependencies: - electron-builder-squirrel-windows - supports-color @@ -2543,7 +2544,7 @@ snapshots: jiti@2.6.1: {} - js-yaml@4.1.1: + js-yaml@4.2.0: dependencies: argparse: 2.0.1 @@ -2800,7 +2801,7 @@ snapshots: config-file-ts: 0.2.6 dotenv: 9.0.2 dotenv-expand: 5.1.0 - js-yaml: 4.1.1 + js-yaml: 4.2.0 json5: 2.2.3 lazy-val: 1.0.5 diff --git a/frontend/package.json b/frontend/package.json index 7a70177a5..ce84aa7fd 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -181,7 +181,8 @@ "form-data": ">=4.0.6", "markdown-it": ">=14.2.0", "launch-editor": ">=2.14.1", - "@babel/core": ">=7.29.6" + "@babel/core": ">=7.29.6", + "js-yaml@4": ">=4.2.0" } } } diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 1dc79da14..21158fb5e 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -18,6 +18,7 @@ overrides: markdown-it: '>=14.2.0' launch-editor: '>=2.14.1' '@babel/core': '>=7.29.6' + js-yaml@4: '>=4.2.0' importers: @@ -4558,8 +4559,8 @@ packages: resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} hasBin: true - js-yaml@4.1.1: - resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + js-yaml@4.2.0: + resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==} hasBin: true jsdom@27.4.0: @@ -8296,7 +8297,7 @@ snapshots: globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.0 - js-yaml: 4.1.1 + js-yaml: 4.2.0 minimatch: 10.2.4 strip-json-comments: 3.1.1 transitivePeerDependencies: @@ -10260,7 +10261,7 @@ snapshots: dependencies: env-paths: 2.2.1 import-fresh: 3.3.0 - js-yaml: 4.1.1 + js-yaml: 4.2.0 parse-json: 5.2.0 optionalDependencies: typescript: 5.9.3 @@ -11529,7 +11530,7 @@ snapshots: argparse: 1.0.10 esprima: 4.0.1 - js-yaml@4.1.1: + js-yaml@4.2.0: dependencies: argparse: 2.0.1 From f851e6f9590b56c8f3f95db742f51b71e2daf05a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 16 Jun 2026 06:37:55 +0000 Subject: [PATCH 12/53] chore(deps): update dev-dependencies --- desktop/package.json | 2 +- desktop/pnpm-lock.yaml | 68 ++++++------ frontend/package.json | 8 +- frontend/pnpm-lock.yaml | 222 ++++++++++++++++++---------------------- 4 files changed, 141 insertions(+), 159 deletions(-) diff --git a/desktop/package.json b/desktop/package.json index 20465374b..415238608 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -62,7 +62,7 @@ }, "devDependencies": { "electron": "40.10.3", - "electron-builder": "26.15.2", + "electron-builder": "26.15.3", "unzipper": "0.12.3" }, "dependencies": { diff --git a/desktop/pnpm-lock.yaml b/desktop/pnpm-lock.yaml index 4f2e92832..0eaa08028 100644 --- a/desktop/pnpm-lock.yaml +++ b/desktop/pnpm-lock.yaml @@ -26,8 +26,8 @@ importers: specifier: 40.10.3 version: 40.10.3 electron-builder: - specifier: 26.15.2 - version: 26.15.2(electron-builder-squirrel-windows@24.13.3) + specifier: 26.15.3 + version: 26.15.3(electron-builder-squirrel-windows@24.13.3) unzipper: specifier: 0.12.3 version: 0.12.3 @@ -236,12 +236,12 @@ packages: dmg-builder: 24.13.3 electron-builder-squirrel-windows: 24.13.3 - app-builder-lib@26.15.2: - resolution: {integrity: sha512-3mYfKOjr/ZY7gFESOcq8kylBMgGPpmlQYnpBVit4p6zIg0t/8bkWBILdMMtnjFyN2jllyBf225T8dLlz3D6oBQ==} + app-builder-lib@26.15.3: + resolution: {integrity: sha512-2VnyWkqsP5v5XbBhL3tD5Syx8iNPBYsoU7kY4S2fz7wg8Rj/nztWKCUzGKaFRTv0Xwf3/H058CR1Kvtd/3lRow==} engines: {node: '>=14.0.0'} peerDependencies: - dmg-builder: 26.15.2 - electron-builder-squirrel-windows: 26.15.2 + dmg-builder: 26.15.3 + electron-builder-squirrel-windows: 26.15.3 archiver-utils@2.1.0: resolution: {integrity: sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==} @@ -331,8 +331,8 @@ packages: builder-util@24.13.1: resolution: {integrity: sha512-NhbCSIntruNDTOVI9fdXz0dihaqX2YuE1D6zZMrwiErzH4ELZHE6mdiB40wEgZNprDia+FghRFgKoAqMZRRjSA==} - builder-util@26.15.0: - resolution: {integrity: sha512-dUx+HxVbiNsNQ4mGe1PyoC/tBmsHwBNDLdBuqWCj+rhHFE9lHgrXiGYKAM1uNlznhAaUSyMlms84VeSSr3gOBA==} + builder-util@26.15.3: + resolution: {integrity: sha512-q2hn7Mbo2nFNkVekPiHFx6Nfo3hURmES3tfBn+k5Pqxl2RkmP3QGqZUhH/q9Pch/4G05NRhPjDlVj1O8q4Txvw==} engines: {node: '>=14.0.0'} bytes@3.1.2: @@ -485,8 +485,8 @@ packages: dir-compare@4.2.0: resolution: {integrity: sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==} - dmg-builder@26.15.2: - resolution: {integrity: sha512-fMkjRqKyPtsz4Kzu/qGP0BGjqzMCIgp+/7kw/u6YH6lvn/8hvL3c0TXhoFayBoYdpPCnEinnCHztd4bW7/jetA==} + dmg-builder@26.15.3: + resolution: {integrity: sha512-O3zJUFUYHJKgzPqioHxfxzBzlSC1eXCSr79gMSBKBP5AgjjpmrydMsMLotEg9fAJF36vdUncb+4ndRNxoPdlSQ==} dotenv-expand@11.0.6: resolution: {integrity: sha512-8NHi73otpWsZGBSZwwknTXS5pqMOrk9+Ssrna8xCaxkzEpU9OTf9R5ArQGVw03//Zmk9MOwLPng9WwndvpAJ5g==} @@ -524,16 +524,16 @@ packages: electron-builder-squirrel-windows@24.13.3: resolution: {integrity: sha512-oHkV0iogWfyK+ah9ZIvMDpei1m9ZRpdXcvde1wTpra2U8AFDNNpqJdnin5z+PM1GbQ5BoaKCWas2HSjtR0HwMg==} - electron-builder@26.15.2: - resolution: {integrity: sha512-veKM9+dCljaC5A74Pwc0ZWQ9arOHREXWh9hUIf8NGg49ch7x+IB4QhbMzIrV5ONZIXM2OEkaxW11cAPjPtoi4A==} + electron-builder@26.15.3: + resolution: {integrity: sha512-a1KM5heqS3gQCZzizXEI8RjJy3QVogULPdeSknt76uLDpBIW/HDGsMg/XgP0riP6PI9COsRvFITKKGDqA8fJxA==} engines: {node: '>=14.0.0'} hasBin: true electron-publish@24.13.1: resolution: {integrity: sha512-2ZgdEqJ8e9D17Hwp5LEq5mLQPjqU3lv/IALvgp+4W8VeNhryfGhYEQC/PgDPMrnWUp+l60Ou5SJLsu+k4mhQ8A==} - electron-publish@26.15.1: - resolution: {integrity: sha512-BMgMHOyexWn0UnOC+Afffw0DMrr0yfLp4U8YsLXwoJ3Da7LS7WUnz21teYZqO0gaApE1KgsjREWmbPqvF5JcPg==} + electron-publish@26.15.3: + resolution: {integrity: sha512-g/2bn8YTavY4cuS5F+jOS7zmZbXXBV8KZ8yHKfJjFPoKtzBqrpCdNPxBd3tqdBwP7BVd0lGzf7Bk2s0KesWZ4Q==} electron@40.10.3: resolution: {integrity: sha512-DdWRsHm4j5wH9TMcfnB2Dqx44G/6BgLKSG/oeRe9kS60pfqCUwzUkHk0ClwvZzBVXtJ1kcdkHVRrJsl1ooKp+g==} @@ -1717,7 +1717,7 @@ snapshots: app-builder-bin@4.0.0: {} - app-builder-lib@24.13.3(dmg-builder@26.15.2)(electron-builder-squirrel-windows@24.13.3): + app-builder-lib@24.13.3(dmg-builder@26.15.3)(electron-builder-squirrel-windows@24.13.3): dependencies: '@develar/schema-utils': 2.6.5 '@electron/notarize': 2.2.1 @@ -1731,9 +1731,9 @@ snapshots: builder-util-runtime: 9.2.4 chromium-pickle-js: 0.2.0 debug: 4.4.3 - dmg-builder: 26.15.2(electron-builder-squirrel-windows@24.13.3) + dmg-builder: 26.15.3(electron-builder-squirrel-windows@24.13.3) ejs: 3.1.10 - electron-builder-squirrel-windows: 24.13.3(dmg-builder@26.15.2) + electron-builder-squirrel-windows: 24.13.3(dmg-builder@26.15.3) electron-publish: 24.13.1 form-data: 4.0.6 fs-extra: 10.1.0 @@ -1751,7 +1751,7 @@ snapshots: transitivePeerDependencies: - supports-color - app-builder-lib@26.15.2(dmg-builder@26.15.2)(electron-builder-squirrel-windows@24.13.3): + app-builder-lib@26.15.3(dmg-builder@26.15.3)(electron-builder-squirrel-windows@24.13.3): dependencies: '@electron/asar': 3.4.1 '@electron/fuses': 1.8.0 @@ -1767,17 +1767,17 @@ snapshots: ajv: 8.20.0 asn1js: 3.0.10 async-exit-hook: 2.0.1 - builder-util: 26.15.0 + builder-util: 26.15.3 builder-util-runtime: 9.7.0 chromium-pickle-js: 0.2.0 ci-info: 4.3.1 debug: 4.4.3 - dmg-builder: 26.15.2(electron-builder-squirrel-windows@24.13.3) + dmg-builder: 26.15.3(electron-builder-squirrel-windows@24.13.3) dotenv: 16.4.5 dotenv-expand: 11.0.6 ejs: 3.1.10 - electron-builder-squirrel-windows: 24.13.3(dmg-builder@26.15.2) - electron-publish: 26.15.1 + electron-builder-squirrel-windows: 24.13.3(dmg-builder@26.15.3) + electron-publish: 26.15.3 fs-extra: 10.1.0 hosted-git-info: 4.1.0 isbinaryfile: 5.0.7 @@ -1936,7 +1936,7 @@ snapshots: transitivePeerDependencies: - supports-color - builder-util@26.15.0: + builder-util@26.15.3: dependencies: '@types/debug': 4.1.13 builder-util-runtime: 9.7.0 @@ -2094,10 +2094,10 @@ snapshots: minimatch: 10.2.5 p-limit: 3.1.0 - dmg-builder@26.15.2(electron-builder-squirrel-windows@24.13.3): + dmg-builder@26.15.3(electron-builder-squirrel-windows@24.13.3): dependencies: - app-builder-lib: 26.15.2(dmg-builder@26.15.2)(electron-builder-squirrel-windows@24.13.3) - builder-util: 26.15.0 + app-builder-lib: 26.15.3(dmg-builder@26.15.3)(electron-builder-squirrel-windows@24.13.3) + builder-util: 26.15.3 fs-extra: 10.1.0 js-yaml: 4.2.0 transitivePeerDependencies: @@ -2132,9 +2132,9 @@ snapshots: dependencies: jake: 10.8.7 - electron-builder-squirrel-windows@24.13.3(dmg-builder@26.15.2): + electron-builder-squirrel-windows@24.13.3(dmg-builder@26.15.3): dependencies: - app-builder-lib: 24.13.3(dmg-builder@26.15.2)(electron-builder-squirrel-windows@24.13.3) + app-builder-lib: 24.13.3(dmg-builder@26.15.3)(electron-builder-squirrel-windows@24.13.3) archiver: 5.3.2 builder-util: 24.13.1 fs-extra: 10.1.0 @@ -2142,14 +2142,14 @@ snapshots: - dmg-builder - supports-color - electron-builder@26.15.2(electron-builder-squirrel-windows@24.13.3): + electron-builder@26.15.3(electron-builder-squirrel-windows@24.13.3): dependencies: - app-builder-lib: 26.15.2(dmg-builder@26.15.2)(electron-builder-squirrel-windows@24.13.3) - builder-util: 26.15.0 + app-builder-lib: 26.15.3(dmg-builder@26.15.3)(electron-builder-squirrel-windows@24.13.3) + builder-util: 26.15.3 builder-util-runtime: 9.7.0 chalk: 4.1.2 ci-info: 4.3.1 - dmg-builder: 26.15.2(electron-builder-squirrel-windows@24.13.3) + dmg-builder: 26.15.3(electron-builder-squirrel-windows@24.13.3) fs-extra: 10.1.0 lazy-val: 1.0.5 simple-update-notifier: 2.0.0 @@ -2170,11 +2170,11 @@ snapshots: transitivePeerDependencies: - supports-color - electron-publish@26.15.1: + electron-publish@26.15.3: dependencies: '@types/fs-extra': 9.0.13 aws4: 1.13.2 - builder-util: 26.15.0 + builder-util: 26.15.3 builder-util-runtime: 9.7.0 chalk: 4.1.2 form-data: 4.0.6 diff --git a/frontend/package.json b/frontend/package.json index ce84aa7fd..9425d7be2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -110,7 +110,7 @@ "@histoire/plugin-vue": "1.0.0-beta.1", "@playwright/test": "1.58.2", "@sentry/vite-plugin": "3.6.1", - "@tailwindcss/vite": "4.3.0", + "@tailwindcss/vite": "4.3.1", "@tsconfig/node24": "24.0.4", "@types/codemirror": "5.60.17", "@types/is-touch-device": "1.0.3", @@ -132,7 +132,7 @@ "eslint": "9.39.4", "eslint-plugin-depend": "1.5.0", "eslint-plugin-vue": "10.9.2", - "happy-dom": "20.10.2", + "happy-dom": "20.10.3", "histoire": "1.0.0-beta.1", "otplib": "12.0.1", "postcss": "8.5.15", @@ -147,7 +147,7 @@ "stylelint-config-recommended-vue": "1.6.1", "stylelint-config-standard-scss": "17.0.0", "stylelint-use-logical": "2.1.3", - "tailwindcss": "4.3.0", + "tailwindcss": "4.3.1", "typescript": "5.9.3", "unplugin-inject-preload": "3.0.0", "vite": "7.3.5", @@ -155,7 +155,7 @@ "vite-plugin-vue-devtools": "8.1.2", "vite-svg-loader": "5.1.1", "vitest": "4.1.8", - "vue-tsc": "3.3.4", + "vue-tsc": "3.3.5", "wait-on": "9.0.10", "workbox-cli": "7.4.1", "ws": "8.21.0" diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 21158fb5e..afd41923f 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -197,8 +197,8 @@ importers: specifier: 3.6.1 version: 3.6.1 '@tailwindcss/vite': - specifier: 4.3.0 - version: 4.3.0(vite@7.3.5(@types/node@24.13.2)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.31.6)(yaml@2.8.3)) + specifier: 4.3.1 + version: 4.3.1(vite@7.3.5(@types/node@24.13.2)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.31.6)(yaml@2.8.3)) '@tsconfig/node24': specifier: 24.0.4 version: 24.0.4 @@ -263,8 +263,8 @@ importers: specifier: 10.9.2 version: 10.9.2(@typescript-eslint/parser@8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@9.39.4(jiti@2.6.1))) happy-dom: - specifier: 20.10.2 - version: 20.10.2 + specifier: 20.10.3 + version: 20.10.3 histoire: specifier: 1.0.0-beta.1 version: 1.0.0-beta.1(@types/node@24.13.2)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.31.6)(vite@7.3.5(@types/node@24.13.2)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.31.6)(yaml@2.8.3))(yaml@2.8.3) @@ -308,8 +308,8 @@ importers: specifier: 2.1.3 version: 2.1.3(stylelint@17.13.0(typescript@5.9.3)) tailwindcss: - specifier: 4.3.0 - version: 4.3.0 + specifier: 4.3.1 + version: 4.3.1 typescript: specifier: 5.9.3 version: 5.9.3 @@ -330,10 +330,10 @@ importers: version: 5.1.1(vue@3.5.27(typescript@5.9.3)) vitest: specifier: 4.1.8 - version: 4.1.8(@types/node@24.13.2)(happy-dom@20.10.2)(jsdom@27.4.0)(vite@7.3.5(@types/node@24.13.2)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.31.6)(yaml@2.8.3)) + version: 4.1.8(@types/node@24.13.2)(happy-dom@20.10.3)(jsdom@27.4.0)(vite@7.3.5(@types/node@24.13.2)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.31.6)(yaml@2.8.3)) vue-tsc: - specifier: 3.3.4 - version: 3.3.4(typescript@5.9.3) + specifier: 3.3.5 + version: 3.3.5(typescript@5.9.3) wait-on: specifier: 9.0.10 version: 9.0.10 @@ -1774,10 +1774,6 @@ packages: '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} - '@jridgewell/gen-mapping@0.3.5': - resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} - engines: {node: '>=6.0.0'} - '@jridgewell/remapping@2.3.5': resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} @@ -1785,19 +1781,12 @@ packages: resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} - '@jridgewell/set-array@1.2.1': - resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} - engines: {node: '>=6.0.0'} - '@jridgewell/source-map@0.3.6': resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - '@jridgewell/trace-mapping@0.3.25': - resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} - '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} @@ -2305,65 +2294,65 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@tailwindcss/node@4.3.0': - resolution: {integrity: sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==} + '@tailwindcss/node@4.3.1': + resolution: {integrity: sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A==} - '@tailwindcss/oxide-android-arm64@4.3.0': - resolution: {integrity: sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==} + '@tailwindcss/oxide-android-arm64@4.3.1': + resolution: {integrity: sha512-SVlyf61g374l5cHyg8x9kf5xmLcOaxvOTsbsqDnSsDJaKOEFZ7GCvi84VAVGpxojYOs1+3K6M0UjXfqPU8vmOQ==} engines: {node: '>= 20'} cpu: [arm64] os: [android] - '@tailwindcss/oxide-darwin-arm64@4.3.0': - resolution: {integrity: sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==} + '@tailwindcss/oxide-darwin-arm64@4.3.1': + resolution: {integrity: sha512-hVnWLwv+e/l7c4WKyVtHVrIPvYdqWHjRB3MDIqARynzFtnQg85kmQEFCbV9Ja0VVx4xXTIiDWY60Y7iz/iNoDA==} engines: {node: '>= 20'} cpu: [arm64] os: [darwin] - '@tailwindcss/oxide-darwin-x64@4.3.0': - resolution: {integrity: sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==} + '@tailwindcss/oxide-darwin-x64@4.3.1': + resolution: {integrity: sha512-Cf7abu0WVgbhU7ANgPUnSAvm7nCvMweusHb8FnaHlLfv/Caq4GYaEZg7ZImzzmjx4lIAfuS8q+eLIS7A7IzxIg==} engines: {node: '>= 20'} cpu: [x64] os: [darwin] - '@tailwindcss/oxide-freebsd-x64@4.3.0': - resolution: {integrity: sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==} + '@tailwindcss/oxide-freebsd-x64@4.3.1': + resolution: {integrity: sha512-ZZqzX2Y+GXtXXfqSfpJhDm60OoZfvLHLCgm+J7NVqgHHJjG/m9ugZI77RwTsVd4fnBJuCFP6Ae6kTJb71UdS8g==} engines: {node: '>= 20'} cpu: [x64] os: [freebsd] - '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0': - resolution: {integrity: sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==} + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.1': + resolution: {integrity: sha512-/Ah/xik0LaMYfv9DZ0S/t4pBlBNYOcqtRwusjgovHkvT8ixueWCLyJjsaF5kQIckjb4IT8Q6K6p/iPmZMixYgg==} engines: {node: '>= 20'} cpu: [arm] os: [linux] - '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': - resolution: {integrity: sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==} + '@tailwindcss/oxide-linux-arm64-gnu@4.3.1': + resolution: {integrity: sha512-gqdFoVJlw444GvpnheZLHmvTzSxI/cOUUh2KSNejQjTcYkW062SVD+En0rUgD+QV91bz1XGIGtt1HJd48xUGbQ==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] - '@tailwindcss/oxide-linux-arm64-musl@4.3.0': - resolution: {integrity: sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==} + '@tailwindcss/oxide-linux-arm64-musl@4.3.1': + resolution: {integrity: sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] - '@tailwindcss/oxide-linux-x64-gnu@4.3.0': - resolution: {integrity: sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==} + '@tailwindcss/oxide-linux-x64-gnu@4.3.1': + resolution: {integrity: sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg==} engines: {node: '>= 20'} cpu: [x64] os: [linux] - '@tailwindcss/oxide-linux-x64-musl@4.3.0': - resolution: {integrity: sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==} + '@tailwindcss/oxide-linux-x64-musl@4.3.1': + resolution: {integrity: sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ==} engines: {node: '>= 20'} cpu: [x64] os: [linux] - '@tailwindcss/oxide-wasm32-wasi@4.3.0': - resolution: {integrity: sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==} + '@tailwindcss/oxide-wasm32-wasi@4.3.1': + resolution: {integrity: sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA==} engines: {node: '>=14.0.0'} cpu: [wasm32] bundledDependencies: @@ -2374,24 +2363,24 @@ packages: - '@emnapi/wasi-threads' - tslib - '@tailwindcss/oxide-win32-arm64-msvc@4.3.0': - resolution: {integrity: sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==} + '@tailwindcss/oxide-win32-arm64-msvc@4.3.1': + resolution: {integrity: sha512-aiNvSq9BsVk8V513lDKlrCFAgf8qBMPZTpgEhInL+NwQqs97mYmupVMrPrgBBSL8Pv/0zXu9MrMF9rMun1ZeNg==} engines: {node: '>= 20'} cpu: [arm64] os: [win32] - '@tailwindcss/oxide-win32-x64-msvc@4.3.0': - resolution: {integrity: sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==} + '@tailwindcss/oxide-win32-x64-msvc@4.3.1': + resolution: {integrity: sha512-xDEyu1rg290472FEGaKHnzyDyh5QH+AlWvsU5hMoMtPpzmKlRI0jaYKCgSHDYtaQWZOYbMaduSyCwFwY4n1HmA==} engines: {node: '>= 20'} cpu: [x64] os: [win32] - '@tailwindcss/oxide@4.3.0': - resolution: {integrity: sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==} + '@tailwindcss/oxide@4.3.1': + resolution: {integrity: sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA==} engines: {node: '>= 20'} - '@tailwindcss/vite@4.3.0': - resolution: {integrity: sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==} + '@tailwindcss/vite@4.3.1': + resolution: {integrity: sha512-hItDHuIIlEV61R+faXu66s1K36aTurO/Qw0e45Vskz57gXl9pWOT6eg3zmcEui6CZXddbN7zd41bwmvag4JGwQ==} peerDependencies: vite: ^5.2.0 || ^6 || ^7 || ^8 @@ -2927,8 +2916,8 @@ packages: typescript: optional: true - '@vue/language-core@3.3.4': - resolution: {integrity: sha512-IuHqQ5zGGOE7CXP72VX6A42IVeIzYv4WAhO6arej11TRNqtdZfGyH8Yr2FOCaDX0dSQG+JwULLoFHGY1igYVjQ==} + '@vue/language-core@3.3.5': + resolution: {integrity: sha512-UkKu5nhX89fg4VhlG/FOeI10G3cj/7radKT/cy9BT4Q9qJmJlSTAc/dP63Xqs29aypN4f39xUV6PsLNk/dcD6g==} '@vue/reactivity@3.5.27': resolution: {integrity: sha512-vvorxn2KXfJ0nBEnj4GYshSgsyMNFnIQah/wczXlsNXt+ijhugmW+PpJ2cNPe4V6jpnBcs0MhCODKllWG+nvoQ==} @@ -3694,8 +3683,8 @@ packages: end-of-stream@1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} - enhanced-resolve@5.21.3: - resolution: {integrity: sha512-QyL119InA+XXEkNLNTPCXPugSvOfhwv0JOlGNzvxs0hZaiHLNvXSpudUWsOlsXGWJh8G6ckCScEkVHfX3kw/2Q==} + enhanced-resolve@5.21.6: + resolution: {integrity: sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==} engines: {node: '>=10.13.0'} entities@4.5.0: @@ -4152,8 +4141,8 @@ packages: resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} engines: {node: '>=6.0'} - happy-dom@20.10.2: - resolution: {integrity: sha512-5p9Sxis3eowDJKqx90QCsgbNA02XXqJ59NOHvD4V6cxp+rP4d/xOyVx7uY3hS8hiUbY1VeiFH8lbJ81AyuDVLQ==} + happy-dom@20.10.3: + resolution: {integrity: sha512-Hjdiy8RziuCcn5z04QI/rlsNuQoG8P0xxjgvsSMpi89cvIXIOcucQtiHS1yHSShxoBcSCeYqAskINmTiy/mlfw==} engines: {node: '>=20.0.0'} hard-rejection@2.1.0: @@ -4536,6 +4525,10 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + joi@18.2.1: resolution: {integrity: sha512-2/OKlogiESf2Nh3TFCrRjrr9z1DRHeW0I+KReF67+4J0Ns+8hBtHRmoWAZ2OFU6I5+TWLEe6sVlSdXPjHm5UbQ==} engines: {node: '>= 20'} @@ -6155,8 +6148,8 @@ packages: resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} engines: {node: '>=10.0.0'} - tailwindcss@4.3.0: - resolution: {integrity: sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==} + tailwindcss@4.3.1: + resolution: {integrity: sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q==} tapable@2.3.3: resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} @@ -6643,8 +6636,8 @@ packages: peerDependencies: vue: ^3.5.0 - vue-tsc@3.3.4: - resolution: {integrity: sha512-XA/JqmQwS2GZmfgpjOEGdrKwaTSEuPwxpHa7/t6f4yiGrJb3gVHTPb9wBfByMNZwQ+xDXs41b8gaS2DKsOozUw==} + vue-tsc@3.3.5: + resolution: {integrity: sha512-Rzh/G2MmNlMSAMTiQEjDrsb4dgB/jbtEM47rVN2NtidF1dfb/q4w4QvpQBtW5+y3y5H27Hjh7deVwk+YB02fNg==} hasBin: true peerDependencies: typescript: '>=5.0.0' @@ -6981,8 +6974,8 @@ snapshots: dependencies: '@babel/parser': 7.28.5 '@babel/types': 7.28.5 - '@jridgewell/gen-mapping': 0.3.5 - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.0.2 '@babel/generator@7.29.1': @@ -8538,33 +8531,20 @@ snapshots: '@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/trace-mapping': 0.3.31 - '@jridgewell/gen-mapping@0.3.5': - dependencies: - '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.25 - '@jridgewell/remapping@2.3.5': dependencies: - '@jridgewell/gen-mapping': 0.3.5 - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/resolve-uri@3.1.2': {} - '@jridgewell/set-array@1.2.1': {} - '@jridgewell/source-map@0.3.6': dependencies: - '@jridgewell/gen-mapping': 0.3.5 - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/sourcemap-codec@1.5.5': {} - '@jridgewell/trace-mapping@0.3.25': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping@0.3.31': dependencies: '@jridgewell/resolve-uri': 3.1.2 @@ -9015,72 +8995,72 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@tailwindcss/node@4.3.0': + '@tailwindcss/node@4.3.1': dependencies: '@jridgewell/remapping': 2.3.5 - enhanced-resolve: 5.21.3 - jiti: 2.6.1 + enhanced-resolve: 5.21.6 + jiti: 2.7.0 lightningcss: 1.32.0 magic-string: 0.30.21 source-map-js: 1.2.1 - tailwindcss: 4.3.0 + tailwindcss: 4.3.1 - '@tailwindcss/oxide-android-arm64@4.3.0': + '@tailwindcss/oxide-android-arm64@4.3.1': optional: true - '@tailwindcss/oxide-darwin-arm64@4.3.0': + '@tailwindcss/oxide-darwin-arm64@4.3.1': optional: true - '@tailwindcss/oxide-darwin-x64@4.3.0': + '@tailwindcss/oxide-darwin-x64@4.3.1': optional: true - '@tailwindcss/oxide-freebsd-x64@4.3.0': + '@tailwindcss/oxide-freebsd-x64@4.3.1': optional: true - '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0': + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.1': optional: true - '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': + '@tailwindcss/oxide-linux-arm64-gnu@4.3.1': optional: true - '@tailwindcss/oxide-linux-arm64-musl@4.3.0': + '@tailwindcss/oxide-linux-arm64-musl@4.3.1': optional: true - '@tailwindcss/oxide-linux-x64-gnu@4.3.0': + '@tailwindcss/oxide-linux-x64-gnu@4.3.1': optional: true - '@tailwindcss/oxide-linux-x64-musl@4.3.0': + '@tailwindcss/oxide-linux-x64-musl@4.3.1': optional: true - '@tailwindcss/oxide-wasm32-wasi@4.3.0': + '@tailwindcss/oxide-wasm32-wasi@4.3.1': optional: true - '@tailwindcss/oxide-win32-arm64-msvc@4.3.0': + '@tailwindcss/oxide-win32-arm64-msvc@4.3.1': optional: true - '@tailwindcss/oxide-win32-x64-msvc@4.3.0': + '@tailwindcss/oxide-win32-x64-msvc@4.3.1': optional: true - '@tailwindcss/oxide@4.3.0': + '@tailwindcss/oxide@4.3.1': optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.3.0 - '@tailwindcss/oxide-darwin-arm64': 4.3.0 - '@tailwindcss/oxide-darwin-x64': 4.3.0 - '@tailwindcss/oxide-freebsd-x64': 4.3.0 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.0 - '@tailwindcss/oxide-linux-arm64-gnu': 4.3.0 - '@tailwindcss/oxide-linux-arm64-musl': 4.3.0 - '@tailwindcss/oxide-linux-x64-gnu': 4.3.0 - '@tailwindcss/oxide-linux-x64-musl': 4.3.0 - '@tailwindcss/oxide-wasm32-wasi': 4.3.0 - '@tailwindcss/oxide-win32-arm64-msvc': 4.3.0 - '@tailwindcss/oxide-win32-x64-msvc': 4.3.0 + '@tailwindcss/oxide-android-arm64': 4.3.1 + '@tailwindcss/oxide-darwin-arm64': 4.3.1 + '@tailwindcss/oxide-darwin-x64': 4.3.1 + '@tailwindcss/oxide-freebsd-x64': 4.3.1 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.1 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.1 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.1 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.1 + '@tailwindcss/oxide-linux-x64-musl': 4.3.1 + '@tailwindcss/oxide-wasm32-wasi': 4.3.1 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.1 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.1 - '@tailwindcss/vite@4.3.0(vite@7.3.5(@types/node@24.13.2)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.31.6)(yaml@2.8.3))': + '@tailwindcss/vite@4.3.1(vite@7.3.5(@types/node@24.13.2)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.31.6)(yaml@2.8.3))': dependencies: - '@tailwindcss/node': 4.3.0 - '@tailwindcss/oxide': 4.3.0 - tailwindcss: 4.3.0 + '@tailwindcss/node': 4.3.1 + '@tailwindcss/oxide': 4.3.1 + tailwindcss: 4.3.1 vite: 7.3.5(@types/node@24.13.2)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.31.6)(yaml@2.8.3) '@tiptap/core@3.17.0(@tiptap/pm@3.17.0)': @@ -9764,7 +9744,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vue/language-core@3.3.4': + '@vue/language-core@3.3.5': dependencies: '@volar/language-core': 2.4.28 '@vue/compiler-dom': 3.5.27 @@ -10515,7 +10495,7 @@ snapshots: dependencies: once: 1.4.0 - enhanced-resolve@5.21.3: + enhanced-resolve@5.21.6: dependencies: graceful-fs: 4.2.11 tapable: 2.3.3 @@ -11088,7 +11068,7 @@ snapshots: section-matter: 1.0.0 strip-bom-string: 1.0.0 - happy-dom@20.10.2: + happy-dom@20.10.3: dependencies: '@types/node': 24.13.2 '@types/whatwg-mimetype': 3.0.2 @@ -11501,6 +11481,8 @@ snapshots: jiti@2.6.1: {} + jiti@2.7.0: {} + joi@18.2.1: dependencies: '@hapi/address': 5.1.1 @@ -13308,7 +13290,7 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 - tailwindcss@4.3.0: {} + tailwindcss@4.3.1: {} tapable@2.3.3: {} @@ -13735,7 +13717,7 @@ snapshots: terser: 5.31.6 yaml: 2.8.3 - vitest@4.1.8(@types/node@24.13.2)(happy-dom@20.10.2)(jsdom@27.4.0)(vite@7.3.5(@types/node@24.13.2)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.31.6)(yaml@2.8.3)): + vitest@4.1.8(@types/node@24.13.2)(happy-dom@20.10.3)(jsdom@27.4.0)(vite@7.3.5(@types/node@24.13.2)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.31.6)(yaml@2.8.3)): dependencies: '@vitest/expect': 4.1.8 '@vitest/mocker': 4.1.8(vite@7.3.5(@types/node@24.13.2)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.31.6)(yaml@2.8.3)) @@ -13759,7 +13741,7 @@ snapshots: why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.13.2 - happy-dom: 20.10.2 + happy-dom: 20.10.3 jsdom: 27.4.0 transitivePeerDependencies: - msw @@ -13812,10 +13794,10 @@ snapshots: '@vue/devtools-api': 6.6.4 vue: 3.5.27(typescript@5.9.3) - vue-tsc@3.3.4(typescript@5.9.3): + vue-tsc@3.3.5(typescript@5.9.3): dependencies: '@volar/typescript': 2.4.28 - '@vue/language-core': 3.3.4 + '@vue/language-core': 3.3.5 typescript: 5.9.3 vue@3.5.27(typescript@5.9.3): From a8bce2ef0b99ff98d4f65b4f1c384fd5950e4fd6 Mon Sep 17 00:00:00 2001 From: "Frederick [Bot]" Date: Wed, 17 Jun 2026 00:35:30 +0000 Subject: [PATCH 13/53] chore(i18n): update translations via Crowdin --- frontend/src/i18n/lang/uk-UA.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/frontend/src/i18n/lang/uk-UA.json b/frontend/src/i18n/lang/uk-UA.json index d92642a0d..9c95bc7f1 100644 --- a/frontend/src/i18n/lang/uk-UA.json +++ b/frontend/src/i18n/lang/uk-UA.json @@ -1480,7 +1480,15 @@ "smartFill": "Заповнити з останнього запису" }, "list": { - "emptyTask": "Для цього завдання ще немає записів обліку часу." + "emptyTask": "Для цього завдання ще немає записів обліку часу.", + "emptyFiltered": "Немає записів обліку часу для вибраних фільтрів.", + "total": "Загалом", + "time": "Час", + "duration": "Тривалість" + }, + "browse": { + "selectRange": "Обрати діапазон", + "userSearch": "Знайти користувача…" } }, "time": { From ea4bb09679f7da771b35c1a27be5d04e74699f13 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 17 Jun 2026 06:50:20 +0000 Subject: [PATCH 14/53] chore(deps): update dev-dependencies --- frontend/package.json | 4 +- frontend/pnpm-lock.yaml | 681 ++++++++++++++++------------------------ 2 files changed, 272 insertions(+), 413 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 9425d7be2..5c520099d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -139,7 +139,7 @@ "postcss-easing-gradients": "3.0.1", "postcss-html": "1.8.1", "postcss-preset-env": "11.3.0", - "rollup": "4.61.1", + "rollup": "4.62.0", "rollup-plugin-visualizer": "6.0.11", "sass-embedded": "1.100.0", "stylelint": "17.13.0", @@ -152,7 +152,7 @@ "unplugin-inject-preload": "3.0.0", "vite": "7.3.5", "vite-plugin-pwa": "1.3.0", - "vite-plugin-vue-devtools": "8.1.2", + "vite-plugin-vue-devtools": "8.1.3", "vite-svg-loader": "5.1.1", "vitest": "4.1.8", "vue-tsc": "3.3.5", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index afd41923f..d818b1c71 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -6,7 +6,7 @@ settings: overrides: minimatch: ^10.2.3 - rollup: 4.61.1 + rollup: 4.62.0 basic-ftp: '>=5.2.2' serialize-javascript: ^7.0.5 flatted: ^3.4.1 @@ -41,7 +41,7 @@ importers: version: 3.1.3(@fortawesome/fontawesome-svg-core@7.1.0)(vue@3.5.27(typescript@5.9.3)) '@intlify/unplugin-vue-i18n': specifier: 11.0.3 - version: 11.0.3(@vue/compiler-dom@3.5.27)(eslint@9.39.4(jiti@2.6.1))(rollup@4.61.1)(typescript@5.9.3)(vue-i18n@11.2.8(vue@3.5.27(typescript@5.9.3)))(vue@3.5.27(typescript@5.9.3)) + version: 11.0.3(@vue/compiler-dom@3.5.27)(eslint@9.39.4(jiti@2.6.1))(rollup@4.62.0)(typescript@5.9.3)(vue-i18n@11.2.8(vue@3.5.27(typescript@5.9.3)))(vue@3.5.27(typescript@5.9.3)) '@kyvg/vue3-notification': specifier: 3.4.2 version: 3.4.2(vue@3.5.27(typescript@5.9.3)) @@ -284,11 +284,11 @@ importers: specifier: 11.3.0 version: 11.3.0(postcss@8.5.14) rollup: - specifier: 4.61.1 - version: 4.61.1 + specifier: 4.62.0 + version: 4.62.0 rollup-plugin-visualizer: specifier: 6.0.11 - version: 6.0.11(rollup@4.61.1) + version: 6.0.11(rollup@4.62.0) sass-embedded: specifier: 1.100.0 version: 1.100.0 @@ -323,8 +323,8 @@ importers: specifier: 1.3.0 version: 1.3.0(vite@7.3.5(@types/node@24.13.2)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.31.6)(yaml@2.8.3))(workbox-build@7.4.1)(workbox-window@7.4.1) vite-plugin-vue-devtools: - specifier: 8.1.2 - version: 8.1.2(vite@7.3.5(@types/node@24.13.2)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.31.6)(yaml@2.8.3))(vue@3.5.27(typescript@5.9.3)) + specifier: 8.1.3 + version: 8.1.3(vite@7.3.5(@types/node@24.13.2)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.31.6)(yaml@2.8.3))(vue@3.5.27(typescript@5.9.3)) vite-svg-loader: specifier: 5.1.1 version: 5.1.1(vue@3.5.27(typescript@5.9.3)) @@ -388,14 +388,6 @@ packages: resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} engines: {node: '>=6.9.0'} - '@babel/generator@7.26.0': - resolution: {integrity: sha512-/AIkAmInnWwgEAJGQr9vY0c66Mj6kjkE2ZPB1PurTRaRAh3U+J45sAQMjQDJdh4WbR3l0x5xkimXBKyBXXAu2w==} - engines: {node: '>=6.9.0'} - - '@babel/generator@7.29.1': - resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} - engines: {node: '>=6.9.0'} - '@babel/generator@7.29.7': resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} engines: {node: '>=6.9.0'} @@ -433,10 +425,6 @@ packages: peerDependencies: '@babel/core': '>=7.29.6' - '@babel/helper-globals@7.28.0': - resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} - engines: {node: '>=6.9.0'} - '@babel/helper-globals@7.29.7': resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} engines: {node: '>=6.9.0'} @@ -449,26 +437,10 @@ packages: resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==} engines: {node: '>=6.9.0'} - '@babel/helper-module-imports@7.28.6': - resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} - engines: {node: '>=6.9.0'} - '@babel/helper-module-imports@7.29.7': resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} engines: {node: '>=6.9.0'} - '@babel/helper-module-transforms@7.26.0': - resolution: {integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': '>=7.29.6' - - '@babel/helper-module-transforms@7.28.6': - resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': '>=7.29.6' - '@babel/helper-module-transforms@7.29.7': resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} engines: {node: '>=6.9.0'} @@ -544,11 +516,6 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - '@babel/parser@7.29.3': - resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} - engines: {node: '>=6.0.0'} - hasBin: true - '@babel/parser@7.29.7': resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} engines: {node: '>=6.0.0'} @@ -958,26 +925,10 @@ packages: resolution: {integrity: sha512-DSgLeL/FNcpXuzav5wfYvHCGvynXkJbn3Zvc3823AEe9nPwW9IK4UoCSS5yGymmQzN0pCPvivtgS6/8U2kkm1w==} engines: {node: '>=6.9.0'} - '@babel/template@7.26.9': - resolution: {integrity: sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==} - engines: {node: '>=6.9.0'} - - '@babel/template@7.28.6': - resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} - engines: {node: '>=6.9.0'} - '@babel/template@7.29.7': resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.25.9': - resolution: {integrity: sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==} - engines: {node: '>=6.9.0'} - - '@babel/traverse@7.29.0': - resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} - engines: {node: '>=6.9.0'} - '@babel/traverse@7.29.7': resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} engines: {node: '>=6.9.0'} @@ -986,10 +937,6 @@ packages: resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} engines: {node: '>=6.9.0'} - '@babel/types@7.29.0': - resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} - engines: {node: '>=6.9.0'} - '@babel/types@7.29.7': resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} @@ -1993,7 +1940,7 @@ packages: peerDependencies: '@babel/core': '>=7.29.6' '@types/babel__core': ^7.1.9 - rollup: 4.61.1 + rollup: 4.62.0 peerDependenciesMeta: '@types/babel__core': optional: true @@ -2004,7 +1951,7 @@ packages: resolution: {integrity: sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==} engines: {node: '>=14.0.0'} peerDependencies: - rollup: 4.61.1 + rollup: 4.62.0 peerDependenciesMeta: rollup: optional: true @@ -2013,7 +1960,7 @@ packages: resolution: {integrity: sha512-J4RZarRvQAm5IF0/LwUUg+obsm+xZhYnbMXmXROyoSE1ATJe3oXSb9L5MMppdxP2ylNSjv6zFBwKYjcKMucVfA==} engines: {node: '>=14.0.0'} peerDependencies: - rollup: 4.61.1 + rollup: 4.62.0 peerDependenciesMeta: rollup: optional: true @@ -2022,7 +1969,7 @@ packages: resolution: {integrity: sha512-FnCxhTBx6bMOYQrar6C8h3scPt8/JwIzw3+AJ2K++6guogH5fYaIFia+zZuhqv0eo1RN7W1Pz630SyvLbDjhtQ==} engines: {node: '>=20.0.0'} peerDependencies: - rollup: 4.61.1 + rollup: 4.62.0 peerDependenciesMeta: rollup: optional: true @@ -2031,133 +1978,133 @@ packages: resolution: {integrity: sha512-Pnsb6f32CD2W3uCaLZIzDmeFyQ2b8UWMFI7xtwUezpcGBDVDW6y9XgAWIlARiGAo6eNF5FK5aQTr0LFyNyqq5A==} engines: {node: '>=14.0.0'} peerDependencies: - rollup: 4.61.1 + rollup: 4.62.0 peerDependenciesMeta: rollup: optional: true - '@rollup/rollup-android-arm-eabi@4.61.1': - resolution: {integrity: sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==} + '@rollup/rollup-android-arm-eabi@4.62.0': + resolution: {integrity: sha512-IPIQ55ythEHkfEd9jMEi32OQ7SxURsGA43JI22lj01OLZNt2NUbJX8YUHxkVWyQ6daHPNn0truF5nSj3DQp6YQ==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.61.1': - resolution: {integrity: sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==} + '@rollup/rollup-android-arm64@4.62.0': + resolution: {integrity: sha512-M6s9cr10MibETyo8JsOkq+Lo1+lU6hcvb1MApnUql5qte/5hMEgzlN8/ReIKNfRV8rrqX50W1BX9zoUhC192RA==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.61.1': - resolution: {integrity: sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==} + '@rollup/rollup-darwin-arm64@4.62.0': + resolution: {integrity: sha512-BqCoMoIbn0keKys+dEAdBa70EtOwV1bEsQCUgU9FdiZmmMge/Zk7LlkYGqbrdHR+Frnt0E1FOanly+rlwvvQzw==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.61.1': - resolution: {integrity: sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==} + '@rollup/rollup-darwin-x64@4.62.0': + resolution: {integrity: sha512-SIMzST3VFNXDAbeIWDWiFCNM5qncUBDWaEV7NfE7oZbDt2mgfW4MvbKdbYiGOLoM32gbTv608UMd0XktEYSD7w==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.61.1': - resolution: {integrity: sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==} + '@rollup/rollup-freebsd-arm64@4.62.0': + resolution: {integrity: sha512-ezjfSQMP7ArdUsbBwbQIfwAlhE84I2iVnzQNCFSveqV42q+BmKlzVpf7mxv5EchLcoWU4y6/heFzVg1F+hodUQ==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.61.1': - resolution: {integrity: sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==} + '@rollup/rollup-freebsd-x64@4.62.0': + resolution: {integrity: sha512-9+qTWGW9AZRhnUgwtTwzNwcPlL87ngkeN0LA+q1bADvmY9aNvWaF2TFW8BZgnQPYxpDI7+rMVLivcd4V737TAQ==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.61.1': - resolution: {integrity: sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==} + '@rollup/rollup-linux-arm-gnueabihf@4.62.0': + resolution: {integrity: sha512-T1dMEQhXA/jkJ/jyMIw9IovK8bSUq7A8kLIlvZTb/6YIVsp2zLavr4F3oyllHWo7eIVJRyE5n3tUjQJEbE1IuQ==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.61.1': - resolution: {integrity: sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==} + '@rollup/rollup-linux-arm-musleabihf@4.62.0': + resolution: {integrity: sha512-2as0LgT7qQpyceQq6VUJYnumUMUrgGQCWIiDIN9DE0/tglsk6o66uCB4f3djRawAltvfCNLyZZrsqbPA6inCsA==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.61.1': - resolution: {integrity: sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==} + '@rollup/rollup-linux-arm64-gnu@4.62.0': + resolution: {integrity: sha512-bVURMg+6eNN9C/yc0aVjooZcwTTtYF4YW3xta5pP0//r3o1V8gXEHXWCndj47w/HhwsFroZrFhR+6uQP5T0n0g==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.61.1': - resolution: {integrity: sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==} + '@rollup/rollup-linux-arm64-musl@4.62.0': + resolution: {integrity: sha512-Ful8pM/2yYI83PViWdFdpZhdI8HJ5qsXANe5atypbHDf+KIBBDsZsbyy8hbXnULVvW9NsTh5DHwbcBftyLTfiw==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loong64-gnu@4.61.1': - resolution: {integrity: sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==} + '@rollup/rollup-linux-loong64-gnu@4.62.0': + resolution: {integrity: sha512-9Gp/DgrkzfUBmNPVTyPTvay+4xEP7M/clXpj3efXBcm6uTIVIgDg4rqUpqKXvLEuFRVuEpSAOkhgNeecvaZ4Cg==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-loong64-musl@4.61.1': - resolution: {integrity: sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==} + '@rollup/rollup-linux-loong64-musl@4.62.0': + resolution: {integrity: sha512-m9tsJz54LUXkSYM8+8PG81B9IKK5r+2T0clMq4QrS16xFosufU7firBDAZEsDheDs7wTlP7h3++S7lMsU955HA==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-ppc64-gnu@4.61.1': - resolution: {integrity: sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==} + '@rollup/rollup-linux-ppc64-gnu@4.62.0': + resolution: {integrity: sha512-3UvJ5PNVU16aJf6M3tFI24pWzAl2/ynfbyRN3ICyQajK1lSkrnVYNnLz3v04J32qKa0FczJc22zeToc0lr2A3w==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-ppc64-musl@4.61.1': - resolution: {integrity: sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==} + '@rollup/rollup-linux-ppc64-musl@4.62.0': + resolution: {integrity: sha512-vRWUAbYLGHBZS6Q8Msb2sfnf1fvJf+47t8l/TwOerM2qArzy+IeNMTHrYLHXh95h8MoatPHI5hhSZNs+mGXKPg==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.61.1': - resolution: {integrity: sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==} + '@rollup/rollup-linux-riscv64-gnu@4.62.0': + resolution: {integrity: sha512-c00T5SYENHAt86cfW47URaP3Us5vLC/4QO7GYud1G5VNRffCwwCuBspwqYrriuJB+5m0WFzClCn9wed0FBjKvg==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-riscv64-musl@4.61.1': - resolution: {integrity: sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==} + '@rollup/rollup-linux-riscv64-musl@4.62.0': + resolution: {integrity: sha512-krrCDilhXOwFkSkO3Wm9I/f9H0L92XHHwy2fwxjukxIbh0dem8gZqOW5Y8BsHrpJv5qwlRBV+Wl4ZFyRWhUpwg==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.61.1': - resolution: {integrity: sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==} + '@rollup/rollup-linux-s390x-gnu@4.62.0': + resolution: {integrity: sha512-7pfYFSTc4/rUC/FtAI0Qp6QthDBCIi6/AuP1xYqFk5vanI6KnL5dWKP60OM/05LOsbwTmIcvr6eXC4CJuJ75IA==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.61.1': - resolution: {integrity: sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==} + '@rollup/rollup-linux-x64-gnu@4.62.0': + resolution: {integrity: sha512-7SDIalKeIpG0Ifogbbdn58HmSotYMlf23K3dCJEmiVd9Fg36Vmni82iPQec27N3wY4Bvbxftkxz6vSx9OcouTg==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.61.1': - resolution: {integrity: sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==} + '@rollup/rollup-linux-x64-musl@4.62.0': + resolution: {integrity: sha512-eRZevouTH2i1HeAVLqJuLnt256krQkGY0TN6WsTmsIhuzbh457HuWDMakKwmi0Cjadux983CoSr8Lim2QhUIFw==} cpu: [x64] os: [linux] - '@rollup/rollup-openbsd-x64@4.61.1': - resolution: {integrity: sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==} + '@rollup/rollup-openbsd-x64@4.62.0': + resolution: {integrity: sha512-3oVS7FLGa4U1qcvao9ylGxrjXZyUQqR8UwxEcnUEyPX53O/C/mKDZegNXTdHCP+h3e6ta/f1EN38Yif1mmZHYg==} cpu: [x64] os: [openbsd] - '@rollup/rollup-openharmony-arm64@4.61.1': - resolution: {integrity: sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==} + '@rollup/rollup-openharmony-arm64@4.62.0': + resolution: {integrity: sha512-yTB9TgfWj5wHe5QgktAgXTLLot1gvEjl1NiPPAUiCs4oPrIWFl5V4nC3GrkNdj9LaAU4s94nVrGbGOCqUpyWsg==} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.61.1': - resolution: {integrity: sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==} + '@rollup/rollup-win32-arm64-msvc@4.62.0': + resolution: {integrity: sha512-5LOhoaesY3doG1c+ac/2JtgREpKoJr5bUHH8tKY0V8di7+uSV6BwLs2PlR0/yzefGOkR+wE7ZolZphHCsyG5Rw==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.61.1': - resolution: {integrity: sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==} + '@rollup/rollup-win32-ia32-msvc@4.62.0': + resolution: {integrity: sha512-yYkWHhmbhRTWTnWos5HC4GcPQfjlzzCNbM9e/+GXrLuaBXYA3qSDR9f0Vgufd5S8yX81U8jPKp7ZnAjZFMtRnw==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.61.1': - resolution: {integrity: sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==} + '@rollup/rollup-win32-x64-gnu@4.62.0': + resolution: {integrity: sha512-SoTb6lPg25xZlA2ibwQ++ahCCnH+FP0qmEuafMJ4gznZKOlXioKEAeJLgCrqjM98ACziXM9V1amFjICVL4IFoA==} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.61.1': - resolution: {integrity: sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==} + '@rollup/rollup-win32-x64-msvc@4.62.0': + resolution: {integrity: sha512-5L+T1fMX4RIEBoZzT0+sQ0PhTS36NULFmMXtl1TZo44TMAROIMHbZufSOjVWt/Y622BtxgxtaNOokbTDvfsrZA==} cpu: [x64] os: [win32] @@ -2888,22 +2835,22 @@ packages: '@vue/devtools-api@7.7.7': resolution: {integrity: sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg==} - '@vue/devtools-core@8.1.2': - resolution: {integrity: sha512-ZGGyaSBP4/+bN2Nd9ZHNYAVDRIzMw1rv2RyXWtyZlo6mQal+IDmTvKY4V+DjAEBhaXt30mHmsgYp1yXJ/2tIWg==} + '@vue/devtools-core@8.1.3': + resolution: {integrity: sha512-xezkv5/CPH/o5C8PE2Len9MnTJMsctYYQbKbbUiNOJpKd+fRHj27nKDb/sbtYI8NSQduegeQhCJGKRgAiOV6Uw==} peerDependencies: vue: ^3.0.0 '@vue/devtools-kit@7.7.7': resolution: {integrity: sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA==} - '@vue/devtools-kit@8.1.2': - resolution: {integrity: sha512-f75/upc+GCyjXErpgPGz4582ujS0L/adAltGy+tqXMGUJpgAcfGr6CxnnhpZY8BHuMYt6KpbF8uaFrrQG66rGQ==} + '@vue/devtools-kit@8.1.3': + resolution: {integrity: sha512-cRn7GXiCQkMYU2Z3h3pM4YO/ndbx9FY1yLDAqIqPLcmIq4H6zAOJHein6tvZU3AfPwgrodqLiPBEF+YQaS8AxA==} '@vue/devtools-shared@7.7.7': resolution: {integrity: sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==} - '@vue/devtools-shared@8.1.2': - resolution: {integrity: sha512-X9RyVFYAdkBe4IUf5v48TxBF/6QPmF8CmWrDAjXzfUHrgQ/HGfTC1A6TqgXqZ03ye66l3AD51BAGD69IvKM9sw==} + '@vue/devtools-shared@8.1.3': + resolution: {integrity: sha512-CM3uIPL+v+lrJUk33+pxspYo0MhuMWlCvf7zC9fybifvCPyM2jUbYRPwoYEJgYbwRqPikm5HozbUhp60MF2QuA==} '@vue/eslint-config-typescript@14.8.0': resolution: {integrity: sha512-yIquzhXH7ZsrwSSm+rYvoGCRY6wcuF4qBi76e0l7hHLq7YU0f9aC+RcR5fL+XJNfmBZxgX5cVl4sppt4x7ZCBg==} @@ -5605,15 +5552,15 @@ packages: hasBin: true peerDependencies: rolldown: 1.x || ^1.0.0-beta - rollup: 4.61.1 + rollup: 4.62.0 peerDependenciesMeta: rolldown: optional: true rollup: optional: true - rollup@4.61.1: - resolution: {integrity: sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==} + rollup@4.62.0: + resolution: {integrity: sha512-nc72Wgq62I7rtDV4izT5/aaS0zxy3kttkinf9586ApknY3jZO9NYsmtc24fUckA0X7Q2v+ML4a15pdUlV5V/jA==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -6488,8 +6435,8 @@ packages: '@vite-pwa/assets-generator': optional: true - vite-plugin-vue-devtools@8.1.2: - resolution: {integrity: sha512-gt5h1CNryR9Hy0tvhSbqY3j0F7aj0pGxBxWLa1lXSiZVkhdWDf0vbCOZyjh8ivFGE6FDHTGy3zkcZGlMZdVHig==} + vite-plugin-vue-devtools@8.1.3: + resolution: {integrity: sha512-KBTUhbTXvY+GsCdShnCHG4WdijEV74KIDxhF8erfSs5g5mS13g/cPRUf4mLpD10qr5FqHYosNt0j6rP5kpiS1Q==} engines: {node: '>=v14.21.3'} peerDependencies: vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -6970,22 +6917,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/generator@7.26.0': - dependencies: - '@babel/parser': 7.28.5 - '@babel/types': 7.28.5 - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - jsesc: 3.0.2 - - '@babel/generator@7.29.1': - dependencies: - '@babel/parser': 7.29.3 - '@babel/types': 7.29.0 - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - jsesc: 3.1.0 - '@babel/generator@7.29.7': dependencies: '@babel/parser': 7.29.7 @@ -6996,19 +6927,19 @@ snapshots: '@babel/helper-annotate-as-pure@7.25.9': dependencies: - '@babel/types': 7.28.5 + '@babel/types': 7.29.7 '@babel/helper-builder-binary-assignment-operator-visitor@7.25.9': dependencies: - '@babel/traverse': 7.25.9 - '@babel/types': 7.28.5 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color '@babel/helper-compilation-targets@7.25.9': dependencies: - '@babel/compat-data': 7.26.0 - '@babel/helper-validator-option': 7.25.9 + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 browserslist: 4.28.2 lru-cache: 5.1.1 semver: 6.3.1 @@ -7029,7 +6960,7 @@ snapshots: '@babel/helper-optimise-call-expression': 7.25.9 '@babel/helper-replace-supers': 7.25.9(@babel/core@7.29.7) '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 - '@babel/traverse': 7.25.9 + '@babel/traverse': 7.29.7 semver: 6.3.1 transitivePeerDependencies: - supports-color @@ -7044,36 +6975,27 @@ snapshots: '@babel/helper-define-polyfill-provider@0.6.2(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-compilation-targets': 7.25.9 - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-plugin-utils': 7.28.6 debug: 4.4.3 lodash.debounce: 4.0.8 resolve: 1.22.8 transitivePeerDependencies: - supports-color - '@babel/helper-globals@7.28.0': {} - '@babel/helper-globals@7.29.7': {} '@babel/helper-member-expression-to-functions@7.25.9': dependencies: - '@babel/traverse': 7.25.9 - '@babel/types': 7.28.5 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color '@babel/helper-module-imports@7.25.9': dependencies: - '@babel/traverse': 7.25.9 - '@babel/types': 7.28.5 - transitivePeerDependencies: - - supports-color - - '@babel/helper-module-imports@7.28.6': - dependencies: - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color @@ -7084,24 +7006,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.26.0(@babel/core@7.29.7)': - dependencies: - '@babel/core': 7.29.7 - '@babel/helper-module-imports': 7.25.9 - '@babel/helper-validator-identifier': 7.28.5 - '@babel/traverse': 7.25.9 - transitivePeerDependencies: - - supports-color - - '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.7)': - dependencies: - '@babel/core': 7.29.7 - '@babel/helper-module-imports': 7.28.6 - '@babel/helper-validator-identifier': 7.28.5 - '@babel/traverse': 7.29.0 - transitivePeerDependencies: - - supports-color - '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 @@ -7113,7 +7017,7 @@ snapshots: '@babel/helper-optimise-call-expression@7.25.9': dependencies: - '@babel/types': 7.28.5 + '@babel/types': 7.29.7 '@babel/helper-plugin-utils@7.25.9': {} @@ -7124,7 +7028,7 @@ snapshots: '@babel/core': 7.29.7 '@babel/helper-annotate-as-pure': 7.25.9 '@babel/helper-wrap-function': 7.25.9 - '@babel/traverse': 7.25.9 + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color @@ -7133,21 +7037,21 @@ snapshots: '@babel/core': 7.29.7 '@babel/helper-member-expression-to-functions': 7.25.9 '@babel/helper-optimise-call-expression': 7.25.9 - '@babel/traverse': 7.25.9 + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color '@babel/helper-simple-access@7.25.9': dependencies: - '@babel/traverse': 7.25.9 - '@babel/types': 7.28.5 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color '@babel/helper-skip-transparent-expression-wrappers@7.25.9': dependencies: - '@babel/traverse': 7.25.9 - '@babel/types': 7.28.5 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color @@ -7165,9 +7069,9 @@ snapshots: '@babel/helper-wrap-function@7.25.9': dependencies: - '@babel/template': 7.26.9 - '@babel/traverse': 7.25.9 - '@babel/types': 7.28.5 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color @@ -7180,10 +7084,6 @@ snapshots: dependencies: '@babel/types': 7.28.5 - '@babel/parser@7.29.3': - dependencies: - '@babel/types': 7.29.0 - '@babel/parser@7.29.7': dependencies: '@babel/types': 7.29.7 @@ -7191,25 +7091,25 @@ snapshots: '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.25.9 - '@babel/traverse': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 '@babel/plugin-transform-optional-chaining': 7.25.9(@babel/core@7.29.7) transitivePeerDependencies: @@ -7218,8 +7118,8 @@ snapshots: '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.25.9 - '@babel/traverse': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color @@ -7227,7 +7127,7 @@ snapshots: dependencies: '@babel/core': 7.29.7 '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.29.7) - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-decorators': 7.25.9(@babel/core@7.29.7) transitivePeerDependencies: - supports-color @@ -7239,58 +7139,58 @@ snapshots: '@babel/plugin-syntax-decorators@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-import-assertions@7.26.0(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-import-attributes@7.26.0(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-jsx@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-typescript@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.29.7) - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-transform-arrow-functions@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-transform-async-generator-functions@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 '@babel/helper-remap-async-to-generator': 7.25.9(@babel/core@7.29.7) - '@babel/traverse': 7.25.9 + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color '@babel/plugin-transform-async-to-generator@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-module-imports': 7.25.9 - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-plugin-utils': 7.28.6 '@babel/helper-remap-async-to-generator': 7.25.9(@babel/core@7.29.7) transitivePeerDependencies: - supports-color @@ -7298,18 +7198,18 @@ snapshots: '@babel/plugin-transform-block-scoped-functions@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-transform-block-scoping@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-transform-class-properties@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.29.7) - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 transitivePeerDependencies: - supports-color @@ -7317,7 +7217,7 @@ snapshots: dependencies: '@babel/core': 7.29.7 '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.29.7) - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 transitivePeerDependencies: - supports-color @@ -7325,10 +7225,10 @@ snapshots: dependencies: '@babel/core': 7.29.7 '@babel/helper-annotate-as-pure': 7.25.9 - '@babel/helper-compilation-targets': 7.25.9 - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-plugin-utils': 7.28.6 '@babel/helper-replace-supers': 7.25.9(@babel/core@7.29.7) - '@babel/traverse': 7.25.9 + '@babel/traverse': 7.29.7 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -7336,53 +7236,53 @@ snapshots: '@babel/plugin-transform-computed-properties@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.25.9 - '@babel/template': 7.26.9 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/template': 7.29.7 '@babel/plugin-transform-destructuring@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-transform-dotall-regex@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.29.7) - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-transform-duplicate-keys@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.29.7) - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-transform-dynamic-import@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-transform-exponentiation-operator@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 '@babel/helper-builder-binary-assignment-operator-visitor': 7.25.9 - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 transitivePeerDependencies: - supports-color '@babel/plugin-transform-export-namespace-from@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-transform-for-of@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 transitivePeerDependencies: - supports-color @@ -7390,45 +7290,45 @@ snapshots: '@babel/plugin-transform-function-name@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-compilation-targets': 7.25.9 - '@babel/helper-plugin-utils': 7.25.9 - '@babel/traverse': 7.25.9 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color '@babel/plugin-transform-json-strings@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-transform-literals@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-transform-logical-assignment-operators@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-transform-member-expression-literals@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-transform-modules-amd@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-module-transforms': 7.26.0(@babel/core@7.29.7) - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.28.6 transitivePeerDependencies: - supports-color '@babel/plugin-transform-modules-commonjs@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-module-transforms': 7.26.0(@babel/core@7.29.7) - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.28.6 '@babel/helper-simple-access': 7.25.9 transitivePeerDependencies: - supports-color @@ -7436,18 +7336,18 @@ snapshots: '@babel/plugin-transform-modules-systemjs@7.29.4(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.7) + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-validator-identifier': 7.28.5 - '@babel/traverse': 7.29.0 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color '@babel/plugin-transform-modules-umd@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-module-transforms': 7.26.0(@babel/core@7.29.7) - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.28.6 transitivePeerDependencies: - supports-color @@ -7455,34 +7355,34 @@ snapshots: dependencies: '@babel/core': 7.29.7 '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.29.7) - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-transform-new-target@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-transform-nullish-coalescing-operator@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-transform-numeric-separator@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-transform-object-rest-spread@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-compilation-targets': 7.25.9 - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-transform-parameters': 7.25.9(@babel/core@7.29.7) '@babel/plugin-transform-object-super@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 '@babel/helper-replace-supers': 7.25.9(@babel/core@7.29.7) transitivePeerDependencies: - supports-color @@ -7490,12 +7390,12 @@ snapshots: '@babel/plugin-transform-optional-catch-binding@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-transform-optional-chaining@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 transitivePeerDependencies: - supports-color @@ -7503,13 +7403,13 @@ snapshots: '@babel/plugin-transform-parameters@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-transform-private-methods@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.29.7) - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 transitivePeerDependencies: - supports-color @@ -7518,41 +7418,41 @@ snapshots: '@babel/core': 7.29.7 '@babel/helper-annotate-as-pure': 7.25.9 '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.29.7) - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 transitivePeerDependencies: - supports-color '@babel/plugin-transform-property-literals@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-transform-regenerator@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 regenerator-transform: 0.15.2 '@babel/plugin-transform-regexp-modifiers@7.26.0(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.29.7) - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-transform-reserved-words@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-transform-shorthand-properties@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-transform-spread@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 transitivePeerDependencies: - supports-color @@ -7560,24 +7460,24 @@ snapshots: '@babel/plugin-transform-sticky-regex@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-transform-template-literals@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-transform-typeof-symbol@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-transform-typescript@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 '@babel/helper-annotate-as-pure': 7.25.9 '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.29.7) - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.29.7) transitivePeerDependencies: @@ -7586,25 +7486,25 @@ snapshots: '@babel/plugin-transform-unicode-escapes@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-transform-unicode-property-regex@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.29.7) - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-transform-unicode-regex@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.29.7) - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-transform-unicode-sets-regex@7.25.9(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.29.7) - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 '@babel/preset-env@7.26.0(@babel/core@7.29.7)': dependencies: @@ -7684,56 +7584,20 @@ snapshots: '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.25.9 - '@babel/types': 7.28.5 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/types': 7.29.7 esutils: 2.0.3 '@babel/runtime@7.25.4': dependencies: regenerator-runtime: 0.14.1 - '@babel/template@7.26.9': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/parser': 7.28.5 - '@babel/types': 7.28.5 - - '@babel/template@7.28.6': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/parser': 7.29.3 - '@babel/types': 7.29.0 - '@babel/template@7.29.7': dependencies: '@babel/code-frame': 7.29.7 '@babel/parser': 7.29.7 '@babel/types': 7.29.7 - '@babel/traverse@7.25.9': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.26.0 - '@babel/parser': 7.28.5 - '@babel/template': 7.26.9 - '@babel/types': 7.28.5 - debug: 4.4.3 - globals: 11.12.0 - transitivePeerDependencies: - - supports-color - - '@babel/traverse@7.29.0': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.29.3 - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - '@babel/traverse@7.29.7': dependencies: '@babel/code-frame': 7.29.7 @@ -7751,11 +7615,6 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 - '@babel/types@7.29.0': - dependencies: - '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.28.5 - '@babel/types@7.29.7': dependencies: '@babel/helper-string-parser': 7.29.7 @@ -8484,13 +8343,13 @@ snapshots: '@intlify/shared@11.2.8': {} - '@intlify/unplugin-vue-i18n@11.0.3(@vue/compiler-dom@3.5.27)(eslint@9.39.4(jiti@2.6.1))(rollup@4.61.1)(typescript@5.9.3)(vue-i18n@11.2.8(vue@3.5.27(typescript@5.9.3)))(vue@3.5.27(typescript@5.9.3))': + '@intlify/unplugin-vue-i18n@11.0.3(@vue/compiler-dom@3.5.27)(eslint@9.39.4(jiti@2.6.1))(rollup@4.62.0)(typescript@5.9.3)(vue-i18n@11.2.8(vue@3.5.27(typescript@5.9.3)))(vue@3.5.27(typescript@5.9.3))': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) '@intlify/bundle-utils': 11.0.3(vue-i18n@11.2.8(vue@3.5.27(typescript@5.9.3))) '@intlify/shared': 11.2.8 '@intlify/vue-i18n-extensions': 8.0.0(@intlify/shared@11.2.8)(@vue/compiler-dom@3.5.27)(vue-i18n@11.2.8(vue@3.5.27(typescript@5.9.3)))(vue@3.5.27(typescript@5.9.3)) - '@rollup/pluginutils': 5.1.3(rollup@4.61.1) + '@rollup/pluginutils': 5.1.3(rollup@4.62.0) '@typescript-eslint/scope-manager': 8.58.0 '@typescript-eslint/typescript-estree': 8.58.0(typescript@5.9.3) debug: 4.4.3 @@ -8734,122 +8593,122 @@ snapshots: '@rolldown/pluginutils@1.0.1': {} - '@rollup/plugin-babel@6.1.0(@babel/core@7.29.7)(rollup@4.61.1)': + '@rollup/plugin-babel@6.1.0(@babel/core@7.29.7)(rollup@4.62.0)': dependencies: '@babel/core': 7.29.7 '@babel/helper-module-imports': 7.25.9 - '@rollup/pluginutils': 5.1.3(rollup@4.61.1) + '@rollup/pluginutils': 5.1.3(rollup@4.62.0) optionalDependencies: - rollup: 4.61.1 + rollup: 4.62.0 transitivePeerDependencies: - supports-color - '@rollup/plugin-node-resolve@16.0.3(rollup@4.61.1)': + '@rollup/plugin-node-resolve@16.0.3(rollup@4.62.0)': dependencies: - '@rollup/pluginutils': 5.1.3(rollup@4.61.1) + '@rollup/pluginutils': 5.1.3(rollup@4.62.0) '@types/resolve': 1.20.2 deepmerge: 4.3.1 is-module: 1.0.0 resolve: 1.22.8 optionalDependencies: - rollup: 4.61.1 + rollup: 4.62.0 - '@rollup/plugin-replace@6.0.3(rollup@4.61.1)': + '@rollup/plugin-replace@6.0.3(rollup@4.62.0)': dependencies: - '@rollup/pluginutils': 5.1.3(rollup@4.61.1) + '@rollup/pluginutils': 5.1.3(rollup@4.62.0) magic-string: 0.30.21 optionalDependencies: - rollup: 4.61.1 + rollup: 4.62.0 - '@rollup/plugin-terser@1.0.0(rollup@4.61.1)': + '@rollup/plugin-terser@1.0.0(rollup@4.62.0)': dependencies: serialize-javascript: 7.0.5 smob: 1.5.0 terser: 5.31.6 optionalDependencies: - rollup: 4.61.1 + rollup: 4.62.0 - '@rollup/pluginutils@5.1.3(rollup@4.61.1)': + '@rollup/pluginutils@5.1.3(rollup@4.62.0)': dependencies: '@types/estree': 1.0.9 estree-walker: 2.0.2 picomatch: 4.0.4 optionalDependencies: - rollup: 4.61.1 + rollup: 4.62.0 - '@rollup/rollup-android-arm-eabi@4.61.1': + '@rollup/rollup-android-arm-eabi@4.62.0': optional: true - '@rollup/rollup-android-arm64@4.61.1': + '@rollup/rollup-android-arm64@4.62.0': optional: true - '@rollup/rollup-darwin-arm64@4.61.1': + '@rollup/rollup-darwin-arm64@4.62.0': optional: true - '@rollup/rollup-darwin-x64@4.61.1': + '@rollup/rollup-darwin-x64@4.62.0': optional: true - '@rollup/rollup-freebsd-arm64@4.61.1': + '@rollup/rollup-freebsd-arm64@4.62.0': optional: true - '@rollup/rollup-freebsd-x64@4.61.1': + '@rollup/rollup-freebsd-x64@4.62.0': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.61.1': + '@rollup/rollup-linux-arm-gnueabihf@4.62.0': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.61.1': + '@rollup/rollup-linux-arm-musleabihf@4.62.0': optional: true - '@rollup/rollup-linux-arm64-gnu@4.61.1': + '@rollup/rollup-linux-arm64-gnu@4.62.0': optional: true - '@rollup/rollup-linux-arm64-musl@4.61.1': + '@rollup/rollup-linux-arm64-musl@4.62.0': optional: true - '@rollup/rollup-linux-loong64-gnu@4.61.1': + '@rollup/rollup-linux-loong64-gnu@4.62.0': optional: true - '@rollup/rollup-linux-loong64-musl@4.61.1': + '@rollup/rollup-linux-loong64-musl@4.62.0': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.61.1': + '@rollup/rollup-linux-ppc64-gnu@4.62.0': optional: true - '@rollup/rollup-linux-ppc64-musl@4.61.1': + '@rollup/rollup-linux-ppc64-musl@4.62.0': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.61.1': + '@rollup/rollup-linux-riscv64-gnu@4.62.0': optional: true - '@rollup/rollup-linux-riscv64-musl@4.61.1': + '@rollup/rollup-linux-riscv64-musl@4.62.0': optional: true - '@rollup/rollup-linux-s390x-gnu@4.61.1': + '@rollup/rollup-linux-s390x-gnu@4.62.0': optional: true - '@rollup/rollup-linux-x64-gnu@4.61.1': + '@rollup/rollup-linux-x64-gnu@4.62.0': optional: true - '@rollup/rollup-linux-x64-musl@4.61.1': + '@rollup/rollup-linux-x64-musl@4.62.0': optional: true - '@rollup/rollup-openbsd-x64@4.61.1': + '@rollup/rollup-openbsd-x64@4.62.0': optional: true - '@rollup/rollup-openharmony-arm64@4.61.1': + '@rollup/rollup-openharmony-arm64@4.62.0': optional: true - '@rollup/rollup-win32-arm64-msvc@4.61.1': + '@rollup/rollup-win32-arm64-msvc@4.62.0': optional: true - '@rollup/rollup-win32-ia32-msvc@4.61.1': + '@rollup/rollup-win32-ia32-msvc@4.62.0': optional: true - '@rollup/rollup-win32-x64-gnu@4.61.1': + '@rollup/rollup-win32-x64-gnu@4.62.0': optional: true - '@rollup/rollup-win32-x64-msvc@4.61.1': + '@rollup/rollup-win32-x64-msvc@4.62.0': optional: true '@sentry-internal/browser-utils@10.36.0': @@ -9640,12 +9499,12 @@ snapshots: '@vue/babel-plugin-jsx@1.2.5(@babel/core@7.29.7)': dependencies: - '@babel/helper-module-imports': 7.25.9 - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.29.7) - '@babel/template': 7.26.9 - '@babel/traverse': 7.25.9 - '@babel/types': 7.28.5 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 '@vue/babel-helper-vue-transform-on': 1.2.5 '@vue/babel-plugin-resolve-type': 1.2.5(@babel/core@7.29.7) html-tags: 3.3.1 @@ -9657,11 +9516,11 @@ snapshots: '@vue/babel-plugin-resolve-type@1.2.5(@babel/core@7.29.7)': dependencies: - '@babel/code-frame': 7.29.0 + '@babel/code-frame': 7.29.7 '@babel/core': 7.29.7 - '@babel/helper-module-imports': 7.25.9 - '@babel/helper-plugin-utils': 7.25.9 - '@babel/parser': 7.28.5 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/parser': 7.29.7 '@vue/compiler-sfc': 3.5.27 transitivePeerDependencies: - supports-color @@ -9702,10 +9561,10 @@ snapshots: dependencies: '@vue/devtools-kit': 7.7.7 - '@vue/devtools-core@8.1.2(vue@3.5.27(typescript@5.9.3))': + '@vue/devtools-core@8.1.3(vue@3.5.27(typescript@5.9.3))': dependencies: - '@vue/devtools-kit': 8.1.2 - '@vue/devtools-shared': 8.1.2 + '@vue/devtools-kit': 8.1.3 + '@vue/devtools-shared': 8.1.3 vue: 3.5.27(typescript@5.9.3) '@vue/devtools-kit@7.7.7': @@ -9718,9 +9577,9 @@ snapshots: speakingurl: 14.0.1 superjson: 2.2.2 - '@vue/devtools-kit@8.1.2': + '@vue/devtools-kit@8.1.3': dependencies: - '@vue/devtools-shared': 8.1.2 + '@vue/devtools-shared': 8.1.3 birpc: 2.6.1 hookable: 5.5.3 perfect-debounce: 2.0.0 @@ -9729,7 +9588,7 @@ snapshots: dependencies: rfdc: 1.4.1 - '@vue/devtools-shared@8.1.2': {} + '@vue/devtools-shared@8.1.3': {} '@vue/eslint-config-typescript@14.8.0(eslint-plugin-vue@10.9.2(@typescript-eslint/parser@8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@9.39.4(jiti@2.6.1))))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': dependencies: @@ -9939,7 +9798,7 @@ snapshots: babel-plugin-polyfill-corejs2@0.4.11(@babel/core@7.29.7): dependencies: - '@babel/compat-data': 7.26.0 + '@babel/compat-data': 7.29.7 '@babel/core': 7.29.7 '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.29.7) semver: 6.3.1 @@ -12665,44 +12524,44 @@ snapshots: rfdc@1.4.1: {} - rollup-plugin-visualizer@6.0.11(rollup@4.61.1): + rollup-plugin-visualizer@6.0.11(rollup@4.62.0): dependencies: open: 8.4.2 picomatch: 4.0.4 source-map: 0.7.4 yargs: 17.7.2 optionalDependencies: - rollup: 4.61.1 + rollup: 4.62.0 - rollup@4.61.1: + rollup@4.62.0: dependencies: '@types/estree': 1.0.9 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.61.1 - '@rollup/rollup-android-arm64': 4.61.1 - '@rollup/rollup-darwin-arm64': 4.61.1 - '@rollup/rollup-darwin-x64': 4.61.1 - '@rollup/rollup-freebsd-arm64': 4.61.1 - '@rollup/rollup-freebsd-x64': 4.61.1 - '@rollup/rollup-linux-arm-gnueabihf': 4.61.1 - '@rollup/rollup-linux-arm-musleabihf': 4.61.1 - '@rollup/rollup-linux-arm64-gnu': 4.61.1 - '@rollup/rollup-linux-arm64-musl': 4.61.1 - '@rollup/rollup-linux-loong64-gnu': 4.61.1 - '@rollup/rollup-linux-loong64-musl': 4.61.1 - '@rollup/rollup-linux-ppc64-gnu': 4.61.1 - '@rollup/rollup-linux-ppc64-musl': 4.61.1 - '@rollup/rollup-linux-riscv64-gnu': 4.61.1 - '@rollup/rollup-linux-riscv64-musl': 4.61.1 - '@rollup/rollup-linux-s390x-gnu': 4.61.1 - '@rollup/rollup-linux-x64-gnu': 4.61.1 - '@rollup/rollup-linux-x64-musl': 4.61.1 - '@rollup/rollup-openbsd-x64': 4.61.1 - '@rollup/rollup-openharmony-arm64': 4.61.1 - '@rollup/rollup-win32-arm64-msvc': 4.61.1 - '@rollup/rollup-win32-ia32-msvc': 4.61.1 - '@rollup/rollup-win32-x64-gnu': 4.61.1 - '@rollup/rollup-win32-x64-msvc': 4.61.1 + '@rollup/rollup-android-arm-eabi': 4.62.0 + '@rollup/rollup-android-arm64': 4.62.0 + '@rollup/rollup-darwin-arm64': 4.62.0 + '@rollup/rollup-darwin-x64': 4.62.0 + '@rollup/rollup-freebsd-arm64': 4.62.0 + '@rollup/rollup-freebsd-x64': 4.62.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.62.0 + '@rollup/rollup-linux-arm-musleabihf': 4.62.0 + '@rollup/rollup-linux-arm64-gnu': 4.62.0 + '@rollup/rollup-linux-arm64-musl': 4.62.0 + '@rollup/rollup-linux-loong64-gnu': 4.62.0 + '@rollup/rollup-linux-loong64-musl': 4.62.0 + '@rollup/rollup-linux-ppc64-gnu': 4.62.0 + '@rollup/rollup-linux-ppc64-musl': 4.62.0 + '@rollup/rollup-linux-riscv64-gnu': 4.62.0 + '@rollup/rollup-linux-riscv64-musl': 4.62.0 + '@rollup/rollup-linux-s390x-gnu': 4.62.0 + '@rollup/rollup-linux-x64-gnu': 4.62.0 + '@rollup/rollup-linux-x64-musl': 4.62.0 + '@rollup/rollup-openbsd-x64': 4.62.0 + '@rollup/rollup-openharmony-arm64': 4.62.0 + '@rollup/rollup-win32-arm64-msvc': 4.62.0 + '@rollup/rollup-win32-ia32-msvc': 4.62.0 + '@rollup/rollup-win32-x64-gnu': 4.62.0 + '@rollup/rollup-win32-x64-msvc': 4.62.0 fsevents: 2.3.3 rope-sequence@1.3.4: {} @@ -13662,11 +13521,11 @@ snapshots: transitivePeerDependencies: - supports-color - vite-plugin-vue-devtools@8.1.2(vite@7.3.5(@types/node@24.13.2)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.31.6)(yaml@2.8.3))(vue@3.5.27(typescript@5.9.3)): + vite-plugin-vue-devtools@8.1.3(vite@7.3.5(@types/node@24.13.2)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.31.6)(yaml@2.8.3))(vue@3.5.27(typescript@5.9.3)): dependencies: - '@vue/devtools-core': 8.1.2(vue@3.5.27(typescript@5.9.3)) - '@vue/devtools-kit': 8.1.2 - '@vue/devtools-shared': 8.1.2 + '@vue/devtools-core': 8.1.3(vue@3.5.27(typescript@5.9.3)) + '@vue/devtools-kit': 8.1.3 + '@vue/devtools-shared': 8.1.3 sirv: 3.0.2 vite: 7.3.5(@types/node@24.13.2)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.31.6)(yaml@2.8.3) vite-plugin-inspect: 11.3.3(vite@7.3.5(@types/node@24.13.2)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.31.6)(yaml@2.8.3)) @@ -13705,7 +13564,7 @@ snapshots: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 postcss: 8.5.14 - rollup: 4.61.1 + rollup: 4.62.0 tinyglobby: 0.2.15 optionalDependencies: '@types/node': 24.13.2 @@ -13945,10 +13804,10 @@ snapshots: '@babel/core': 7.29.7 '@babel/preset-env': 7.26.0(@babel/core@7.29.7) '@babel/runtime': 7.25.4 - '@rollup/plugin-babel': 6.1.0(@babel/core@7.29.7)(rollup@4.61.1) - '@rollup/plugin-node-resolve': 16.0.3(rollup@4.61.1) - '@rollup/plugin-replace': 6.0.3(rollup@4.61.1) - '@rollup/plugin-terser': 1.0.0(rollup@4.61.1) + '@rollup/plugin-babel': 6.1.0(@babel/core@7.29.7)(rollup@4.62.0) + '@rollup/plugin-node-resolve': 16.0.3(rollup@4.62.0) + '@rollup/plugin-replace': 6.0.3(rollup@4.62.0) + '@rollup/plugin-terser': 1.0.0(rollup@4.62.0) '@trickfilm400/rollup-plugin-off-main-thread': 3.0.0-pre1 ajv: 8.18.0 common-tags: 1.8.2 @@ -13957,7 +13816,7 @@ snapshots: fs-extra: 9.1.0 glob: 11.1.0 pretty-bytes: 5.6.0 - rollup: 4.61.1 + rollup: 4.62.0 source-map: 0.8.0-beta.0 stringify-object: 3.3.0 strip-comments: 2.0.1 From 8bec65459542fb9c0e412e7901b1279a3803a181 Mon Sep 17 00:00:00 2001 From: kolaente Date: Fri, 12 Jun 2026 11:48:41 +0200 Subject: [PATCH 15/53] refactor(background): extract download + unsplash-proxy logic for reuse Split the HTTP plumbing from the business logic in the v1 project-background download and Unsplash image proxy handlers so /api/v2 can reuse it without duplicating it: - LoadProjectBackgroundForDownload (background/handler) loads the bg file + modtime and fires the Unsplash pingback; GetProjectBackground now calls it. - WriteProjectBackground (web/files) writes v1's exact background wire shape (image/jpg, no-cache, stat-modtime Last-Modified, If-Modified-Since 304). - FetchUnsplashImageByID / FetchUnsplashThumbByID (background/unsplash) return the open upstream body for the caller to stream; the v1 proxy handlers now call them. A typed ErrUnsplashImageDoesNotExist maps to 404 on both APIs. - ErrProjectHasNoBackground (models) gives the no-background case a domain error; v1 keeps its verbatim 404 message. v1 responses are unchanged on the wire. --- pkg/models/error.go | 28 ++++++ pkg/modules/background/handler/background.go | 65 +++++++------- pkg/modules/background/unsplash/proxy.go | 93 ++++++++++++++++---- pkg/web/files/project_background.go | 55 ++++++++++++ 4 files changed, 187 insertions(+), 54 deletions(-) create mode 100644 pkg/web/files/project_background.go diff --git a/pkg/models/error.go b/pkg/models/error.go index 2f9d652c9..0b793aecc 100644 --- a/pkg/models/error.go +++ b/pkg/models/error.go @@ -535,6 +535,34 @@ func (err *ErrProjectViewDoesNotExist) HTTPError() web.HTTPError { } } +// ErrProjectHasNoBackground represents an error where a project has no background set. +type ErrProjectHasNoBackground struct { + ProjectID int64 +} + +// IsErrProjectHasNoBackground checks if an error is ErrProjectHasNoBackground. +func IsErrProjectHasNoBackground(err error) bool { + _, ok := err.(*ErrProjectHasNoBackground) + return ok +} + +func (err *ErrProjectHasNoBackground) Error() string { + return fmt.Sprintf("Project has no background [ProjectID: %d]", err.ProjectID) +} + +// ErrCodeProjectHasNoBackground holds the unique world-error code of this error +const ErrCodeProjectHasNoBackground = 3015 + +// HTTPError holds the http error description +func (err *ErrProjectHasNoBackground) HTTPError() web.HTTPError { + return web.HTTPError{ + HTTPCode: http.StatusNotFound, + Code: ErrCodeProjectHasNoBackground, + // Message kept verbatim from v1's inline handler error so the wire body is unchanged. + Message: "Project background not found", + } +} + // ============== // Task errors // ============== diff --git a/pkg/modules/background/handler/background.go b/pkg/modules/background/handler/background.go index afe7901e5..da9d0e522 100644 --- a/pkg/modules/background/handler/background.go +++ b/pkg/modules/background/handler/background.go @@ -31,6 +31,7 @@ import ( "image" "io" "net/http" + "os" "strconv" "strings" @@ -43,6 +44,7 @@ import ( "code.vikunja.io/api/pkg/modules/background/unsplash" "code.vikunja.io/api/pkg/modules/background/upload" "code.vikunja.io/api/pkg/web" + webfiles "code.vikunja.io/api/pkg/web/files" "github.com/bbrks/go-blurhash" "github.com/gabriel-vasile/mimetype" @@ -385,54 +387,47 @@ func GetProjectBackground(c *echo.Context) error { return err } - if project.BackgroundFileID == 0 { - _ = s.Rollback() - return echo.NewHTTPError(http.StatusNotFound, "Project background not found") - } - - // Get the file - bgFile := &files.File{ - ID: project.BackgroundFileID, - } - if err := bgFile.LoadFileByID(); err != nil { - _ = s.Rollback() - return err - } - stat, err := files.FileStat(bgFile) + bgFile, stat, err := LoadProjectBackgroundForDownload(s, project) if err != nil { _ = s.Rollback() + if models.IsErrProjectHasNoBackground(err) { + return echo.NewHTTPError(http.StatusNotFound, "Project background not found") + } return err } - // Unsplash requires pingbacks as per their api usage guidelines. - // To do this in a privacy-preserving manner, we do the ping from inside of Vikunja to not expose any user details. - // FIXME: This should use an event once we have events - unsplash.Pingback(s, bgFile) - if err := s.Commit(); err != nil { _ = s.Rollback() return err } - // Override the global no-store directive so browsers can cache background images. - // no-cache allows caching but requires revalidation via If-Modified-Since. - c.Response().Header().Set("Cache-Control", "no-cache") + webfiles.WriteProjectBackground(c.Response(), c.Request(), bgFile, stat) + return nil +} - // Set Last-Modified header if we have the file stat, so clients can decide whether to use cached files - if stat != nil { - modTime := stat.ModTime().UTC() - c.Response().Header().Set(echo.HeaderLastModified, modTime.Format(http.TimeFormat)) - - // Check If-Modified-Since and return 304 if the file hasn't changed - if ifModSince := c.Request().Header.Get("If-Modified-Since"); ifModSince != "" { - if t, err := http.ParseTime(ifModSince); err == nil && !modTime.After(t) { - return c.NoContent(http.StatusNotModified) - } - } +// LoadProjectBackgroundForDownload opens the project's background file (bytes ready to +// read) and stats it for the modtime the download uses for caching. It also fires the +// Unsplash pingback side effect, required by Unsplash's API guidelines and done +// server-side so no user details are exposed. Returns ErrProjectHasNoBackground when the +// project has none; the caller owns committing the session and closing bgFile.File. +func LoadProjectBackgroundForDownload(s *xorm.Session, project *models.Project) (bgFile *files.File, stat os.FileInfo, err error) { + if project.BackgroundFileID == 0 { + return nil, nil, &models.ErrProjectHasNoBackground{ProjectID: project.ID} } - // Serve the file - return c.Stream(http.StatusOK, "image/jpg", bgFile.File) + bgFile = &files.File{ID: project.BackgroundFileID} + if err := bgFile.LoadFileByID(); err != nil { + return nil, nil, err + } + stat, err = files.FileStat(bgFile) + if err != nil { + return nil, nil, err + } + + // FIXME: This should use an event once we have events + unsplash.Pingback(s, bgFile) + + return bgFile, stat, nil } // RemoveProjectBackground removes a project background, no matter the background provider diff --git a/pkg/modules/background/unsplash/proxy.go b/pkg/modules/background/unsplash/proxy.go index 69fb23716..1d0b11607 100644 --- a/pkg/modules/background/unsplash/proxy.go +++ b/pkg/modules/background/unsplash/proxy.go @@ -18,32 +18,95 @@ package unsplash import ( "context" + "errors" + "io" "net/http" "strings" "code.vikunja.io/api/pkg/utils" + "code.vikunja.io/api/pkg/web" "github.com/labstack/echo/v5" ) -func unsplashImage(url string, c *echo.Context) error { +// ErrUnsplashImageDoesNotExist is returned when Unsplash answers an image proxy fetch +// with a non-success status, mirroring v1's echo.ErrNotFound. It satisfies +// web.HTTPErrorProcessor so the v2 error bridge maps it to a 404. +type ErrUnsplashImageDoesNotExist struct{} + +// IsErrUnsplashImageDoesNotExist checks if an error is ErrUnsplashImageDoesNotExist. +func IsErrUnsplashImageDoesNotExist(err error) bool { + var target *ErrUnsplashImageDoesNotExist + return errors.As(err, &target) +} + +func (err *ErrUnsplashImageDoesNotExist) Error() string { + return "Unsplash image does not exist" +} + +// HTTPError holds the http error description. +func (err *ErrUnsplashImageDoesNotExist) HTTPError() web.HTTPError { + return web.HTTPError{HTTPCode: http.StatusNotFound, Message: "Not Found"} +} + +// fetchUnsplashImage fetches an image from Unsplash through the SSRF-safe client and +// returns its still-open response body for the caller to stream and close. The url is +// rebased onto the hardcoded images.unsplash.com host (stripping any client-supplied +// host) so the proxy can only ever reach Unsplash. It returns +// ErrUnsplashImageDoesNotExist on a non-success upstream status. +func fetchUnsplashImage(url string) (io.ReadCloser, error) { // Replacing and appending the url for security reasons req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "https://images.unsplash.com/"+strings.Replace(url, "https://images.unsplash.com/", "", 1), nil) if err != nil { - return err + return nil, err } resp, err := utils.NewSSRFSafeHTTPClient().Do(req) //nolint:gosec // SSRF protection is handled by the SSRF-safe client if err != nil { - return err + return nil, err } - defer resp.Body.Close() if resp.StatusCode > 399 { - return echo.ErrNotFound + _ = resp.Body.Close() + return nil, &ErrUnsplashImageDoesNotExist{} } - return c.Stream(http.StatusOK, "image/jpg", resp.Body) + return resp.Body, nil } -// ProxyUnsplashImage proxies a thumbnail from unsplash for privacy reasons. +// FetchUnsplashImageByID resolves an Unsplash image by id, fires the required pingback, +// and returns the full-resolution image body for the caller to stream and close. +func FetchUnsplashImageByID(imageID string) (io.ReadCloser, error) { + photo, err := getUnsplashPhotoInfoByID(imageID) + if err != nil { + return nil, err + } + pingbackByPhotoID(photo.ID) + return fetchUnsplashImage(photo.Urls.Raw) +} + +// FetchUnsplashThumbByID resolves an Unsplash image by id, fires the required pingback, +// and returns a thumbnail (max width 200px) body for the caller to stream and close. +func FetchUnsplashThumbByID(imageID string) (io.ReadCloser, error) { + photo, err := getUnsplashPhotoInfoByID(imageID) + if err != nil { + return nil, err + } + pingbackByPhotoID(photo.ID) + return fetchUnsplashImage("https://images.unsplash.com/" + getImageID(photo.Urls.Raw) + "?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=200&fit=max&ixid=eyJhcHBfaWQiOjcyODAwfQ") +} + +// streamUnsplashImage streams a fetched image body to the v1 echo response, mapping the +// not-found sentinel back to echo.ErrNotFound so v1's wire response is unchanged. +func streamUnsplashImage(body io.ReadCloser, err error, c *echo.Context) error { + if err != nil { + if IsErrUnsplashImageDoesNotExist(err) { + return echo.ErrNotFound + } + return err + } + defer body.Close() + return c.Stream(http.StatusOK, "image/jpg", body) +} + +// ProxyUnsplashImage proxies an image from unsplash for privacy reasons. // @Summary Get an unsplash image // @Description Get an unsplash image. **Returns json on error.** // @tags project @@ -55,12 +118,8 @@ func unsplashImage(url string, c *echo.Context) error { // @Failure 500 {object} models.Message "Internal error" // @Router /backgrounds/unsplash/image/{image} [get] func ProxyUnsplashImage(c *echo.Context) error { - photo, err := getUnsplashPhotoInfoByID(c.Param("image")) - if err != nil { - return err - } - pingbackByPhotoID(photo.ID) - return unsplashImage(photo.Urls.Raw, c) + body, err := FetchUnsplashImageByID(c.Param("image")) + return streamUnsplashImage(body, err, c) } // ProxyUnsplashThumb proxies a thumbnail from unsplash for privacy reasons. @@ -75,10 +134,6 @@ func ProxyUnsplashImage(c *echo.Context) error { // @Failure 500 {object} models.Message "Internal error" // @Router /backgrounds/unsplash/image/{image}/thumb [get] func ProxyUnsplashThumb(c *echo.Context) error { - photo, err := getUnsplashPhotoInfoByID(c.Param("image")) - if err != nil { - return err - } - pingbackByPhotoID(photo.ID) - return unsplashImage("https://images.unsplash.com/"+getImageID(photo.Urls.Raw)+"?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=200&fit=max&ixid=eyJhcHBfaWQiOjcyODAwfQ", c) + body, err := FetchUnsplashThumbByID(c.Param("image")) + return streamUnsplashImage(body, err, c) } diff --git a/pkg/web/files/project_background.go b/pkg/web/files/project_background.go new file mode 100644 index 000000000..aeda725a7 --- /dev/null +++ b/pkg/web/files/project_background.go @@ -0,0 +1,55 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-present Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package files + +import ( + "io" + "net/http" + "os" + + "code.vikunja.io/api/pkg/files" +) + +// WriteProjectBackground streams a project's background file (its .File reader must be +// open) to the response, shared by the v1 and v2 background handlers. It does not close +// the reader; the caller owns it. +// +// The wire shape differs from WriteFileDownload on purpose and must stay byte-identical +// to v1: backgrounds are always served as image/jpg (no Content-Disposition, no +// Content-Length), with a cache-revalidation Last-Modified from the storage modtime +// rather than the file's DB Created timestamp. +func WriteProjectBackground(w http.ResponseWriter, r *http.Request, bgFile *files.File, stat os.FileInfo) { + // Override the global no-store directive so browsers can cache background images. + // no-cache allows caching but requires revalidation via If-Modified-Since. + w.Header().Set("Cache-Control", "no-cache") + + if stat != nil { + modTime := stat.ModTime().UTC() + w.Header().Set("Last-Modified", modTime.Format(http.TimeFormat)) + + if ifModSince := r.Header.Get("If-Modified-Since"); ifModSince != "" { + if t, err := http.ParseTime(ifModSince); err == nil && !modTime.After(t) { + w.WriteHeader(http.StatusNotModified) + return + } + } + } + + w.Header().Set("Content-Type", "image/jpg") + w.WriteHeader(http.StatusOK) + _, _ = io.Copy(w, bgFile.File) +} From 5ccbd0d74e15410a0e3f83a25b316f367ed634b1 Mon Sep 17 00:00:00 2001 From: kolaente Date: Fri, 12 Jun 2026 11:48:47 +0200 Subject: [PATCH 16/53] feat(api/v2): add project background download and unsplash proxies Port the remaining read-only background blob endpoints to /api/v2: - GET /projects/{project}/background streams the stored background (project CanRead, in-handler), modeled as an image/jpeg binary response. Honors If-Modified-Since (304) and serves through the shared WriteProjectBackground. - GET /backgrounds/unsplash/images/{image} and .../thumb proxy the upstream Unsplash image through the SSRF-safe client, gated on the unsplash provider like the sibling unsplash routes, modeled as image/jpeg binary responses. All three reuse the v1 business logic extracted in the previous commit. --- pkg/routes/api/v2/backgrounds.go | 142 +++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) diff --git a/pkg/routes/api/v2/backgrounds.go b/pkg/routes/api/v2/backgrounds.go index f01fcb4e3..4d7b5befe 100644 --- a/pkg/routes/api/v2/backgrounds.go +++ b/pkg/routes/api/v2/backgrounds.go @@ -18,6 +18,7 @@ package apiv2 import ( "context" + "io" "net/http" "code.vikunja.io/api/pkg/config" @@ -26,6 +27,8 @@ import ( "code.vikunja.io/api/pkg/modules/background" backgroundHandler "code.vikunja.io/api/pkg/modules/background/handler" "code.vikunja.io/api/pkg/modules/background/unsplash" + "code.vikunja.io/api/pkg/modules/humaecho5" + webfiles "code.vikunja.io/api/pkg/web/files" "github.com/danielgtaylor/huma/v2" ) @@ -55,6 +58,26 @@ func RegisterBackgroundRoutes(api huma.API) { Tags: tags, }, backgroundRemove) + Register(api, huma.Operation{ + OperationID: "projects-background-get", + Summary: "Get a project background", + Description: "Streams a project's background image, whichever provider set it. Requires read access to the project. Always served as image/jpeg with a revalidation Last-Modified header, so a conditional If-Modified-Since request gets a 304. Returns 404 when the project has no background.", + Method: http.MethodGet, + Path: "/projects/{project}/background", + Tags: tags, + // Spell out the binary response; the default would be modeled as JSON. + Responses: map[string]*huma.Response{ + "200": { + Description: "The project background as a jpeg image.", + Content: map[string]*huma.MediaType{ + "image/jpeg": { + Schema: &huma.Schema{Type: huma.TypeString, Format: "binary"}, + }, + }, + }, + }, + }, backgroundGet) + if config.BackgroundsUploadEnabled.GetBool() { Register(api, huma.Operation{ OperationID: "projects-background-upload", @@ -89,6 +112,39 @@ func RegisterBackgroundRoutes(api huma.API) { Path: "/projects/{project}/backgrounds/unsplash", Tags: tags, }, backgroundUnsplashSet) + + unsplashProxyResponses := map[string]*huma.Response{ + "200": { + Description: "The proxied Unsplash image as a jpeg image.", + Content: map[string]*huma.MediaType{ + "image/jpeg": { + Schema: &huma.Schema{Type: huma.TypeString, Format: "binary"}, + }, + }, + }, + } + + Register(api, huma.Operation{ + OperationID: "backgrounds-unsplash-image", + Summary: "Proxy a full-resolution Unsplash image", + Description: "Proxies the full-resolution Unsplash image for the given image id through Vikunja, so the client never contacts Unsplash directly (privacy). Vikunja fires the required Unsplash pingback as a side effect. Returns 404 if the image does not exist.", + Method: http.MethodGet, + Path: "/backgrounds/unsplash/images/{image}", + Tags: tags, + // Spell out the binary response; the default would be modeled as JSON. + Responses: unsplashProxyResponses, + }, backgroundUnsplashImage) + + Register(api, huma.Operation{ + OperationID: "backgrounds-unsplash-thumb", + Summary: "Proxy an Unsplash image thumbnail", + Description: "Proxies a thumbnail (max width 200px) of the Unsplash image for the given image id through Vikunja, so the client never contacts Unsplash directly (privacy). Vikunja fires the required Unsplash pingback as a side effect. Returns 404 if the image does not exist.", + Method: http.MethodGet, + Path: "/backgrounds/unsplash/images/{image}/thumb", + Tags: tags, + // Spell out the binary response; the default would be modeled as JSON. + Responses: unsplashProxyResponses, + }, backgroundUnsplashThumb) } } @@ -227,6 +283,92 @@ func backgroundUpload(ctx context.Context, in *backgroundUploadInput) (*singleBo return &singleBody[models.Project]{Body: project}, nil } +// backgroundGet owns auth, the session and the permission check because there is no +// handler.Do* for a file body. CanRead hydrates the project (including its +// BackgroundFileID), which the shared loader then needs. +func backgroundGet(ctx context.Context, in *struct { + ProjectID int64 `path:"project" doc:"The id of the project whose background to fetch."` +}) (*huma.StreamResponse, error) { + a, err := authFromCtx(ctx) + if err != nil { + return nil, err + } + + s := db.NewSession() + defer s.Close() + + project := &models.Project{ID: in.ProjectID} + can, _, err := project.CanRead(s, a) + if err != nil { + _ = s.Rollback() + return nil, translateDomainError(err) + } + if !can { + _ = s.Rollback() + return nil, huma.Error403Forbidden("forbidden") + } + + bgFile, stat, err := backgroundHandler.LoadProjectBackgroundForDownload(s, project) + if err != nil { + _ = s.Rollback() + return nil, translateDomainError(err) + } + + // The file reader comes from object storage, not the DB session, so it stays + // valid after the commit; the StreamResponse callback runs after this returns. + if err := s.Commit(); err != nil { + _ = s.Rollback() + // The stream callback (which closes the reader) won't run on this error path. + _ = bgFile.File.Close() + return nil, translateDomainError(err) + } + + return &huma.StreamResponse{Body: func(hctx huma.Context) { + defer func() { _ = bgFile.File.Close() }() + c := humaecho5.Unwrap(hctx) + webfiles.WriteProjectBackground((*c).Response(), (*c).Request(), bgFile, stat) + }}, nil +} + +func backgroundUnsplashImage(ctx context.Context, in *struct { + ImageID string `path:"image" doc:"The Unsplash image id, from a prior background search."` +}) (*huma.StreamResponse, error) { + if _, err := authFromCtx(ctx); err != nil { + return nil, err + } + body, err := unsplash.FetchUnsplashImageByID(in.ImageID) + if err != nil { + return nil, translateDomainError(err) + } + return streamUnsplashProxy(body), nil +} + +func backgroundUnsplashThumb(ctx context.Context, in *struct { + ImageID string `path:"image" doc:"The Unsplash image id, from a prior background search."` +}) (*huma.StreamResponse, error) { + if _, err := authFromCtx(ctx); err != nil { + return nil, err + } + body, err := unsplash.FetchUnsplashThumbByID(in.ImageID) + if err != nil { + return nil, translateDomainError(err) + } + return streamUnsplashProxy(body), nil +} + +// streamUnsplashProxy copies the open upstream Unsplash body to the response as +// image/jpeg and closes it, mirroring v1's c.Stream. +func streamUnsplashProxy(body io.ReadCloser) *huma.StreamResponse { + return &huma.StreamResponse{Body: func(hctx huma.Context) { + defer func() { _ = body.Close() }() + c := humaecho5.Unwrap(hctx) + resp := (*c).Response() + resp.Header().Set("Content-Type", "image/jpg") + resp.WriteHeader(http.StatusOK) + _, _ = io.Copy(resp, body) + }} +} + func backgroundRemove(ctx context.Context, in *struct { ProjectID int64 `path:"project"` }) (*singleBody[models.Project], error) { From c5d615843d4cf1d83aa38dd83edbcf9d607e57f5 Mon Sep 17 00:00:00 2001 From: kolaente Date: Fri, 12 Jun 2026 11:48:55 +0200 Subject: [PATCH 17/53] test(api/v2): cover background download and unsplash proxy routes - Download: upload-then-download (real bytes), content-type, If-Modified-Since 304, read-only access allowed, no-access 403, unauthenticated 401, no background 404, and the config-disabled route being absent. - Unsplash proxies: routes absent when the provider is disabled, and 401 when unauthenticated. The live Unsplash fetch is not exercised, matching v1. --- pkg/webtests/huma_background_download_test.go | 162 ++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 pkg/webtests/huma_background_download_test.go diff --git a/pkg/webtests/huma_background_download_test.go b/pkg/webtests/huma_background_download_test.go new file mode 100644 index 000000000..e4c542a26 --- /dev/null +++ b/pkg/webtests/huma_background_download_test.go @@ -0,0 +1,162 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-present Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package webtests + +import ( + "net/http" + "net/http/httptest" + "testing" + + "code.vikunja.io/api/pkg/config" + "code.vikunja.io/api/pkg/routes" + + "github.com/labstack/echo/v5" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// getBackgroundRequest issues a GET against the background download route with an +// optional If-Modified-Since header (humaRequest can't set arbitrary headers). +func getBackgroundRequest(t *testing.T, e *echo.Echo, project, token, ifModifiedSince string) *httptest.ResponseRecorder { + t.Helper() + req := httptest.NewRequest(http.MethodGet, "/api/v2/projects/"+project+"/background", nil) + if token != "" { + req.Header.Set("Authorization", "Bearer "+token) + } + if ifModifiedSince != "" { + req.Header.Set("If-Modified-Since", ifModifiedSince) + } + rec := httptest.NewRecorder() + e.ServeHTTP(rec, req) + return rec +} + +// TestHumaProjectBackgroundDownload covers GET /projects/{project}/background. The +// fixture file row (project 35, background_file_id 1) carries no bytes, so the happy +// path uploads a real background first (the "upload-then-download" pattern) before +// fetching it back. +func TestHumaProjectBackgroundDownload(t *testing.T) { + e, err := setupTestEnv() + require.NoError(t, err) + + t.Run("Owner uploads then downloads the background", func(t *testing.T) { + // testuser1 owns project 1, which starts without a background. + body, contentType := multipartFileBody(t, "background", "bg.png", pngBytes(t)) + up := uploadBackgroundRequest(t, e, "1", humaTokenFor(t, &testuser1), body, contentType) + require.Equal(t, http.StatusOK, up.Code, "upload body: %s", up.Body.String()) + + rec := getBackgroundRequest(t, e, "1", humaTokenFor(t, &testuser1), "") + require.Equal(t, http.StatusOK, rec.Code, "body: %s", rec.Body.String()) + assert.Equal(t, "image/jpg", rec.Header().Get("Content-Type")) + assert.Equal(t, "no-cache", rec.Header().Get("Cache-Control")) + assert.NotEmpty(t, rec.Body.Bytes(), "the download must return the stored bytes") + }) + + t.Run("If-Modified-Since returns 304", func(t *testing.T) { + // The in-memory test storage reports a zero modtime, so any valid + // If-Modified-Since is not-before it and yields a 304. + body, contentType := multipartFileBody(t, "background", "bg.png", pngBytes(t)) + up := uploadBackgroundRequest(t, e, "1", humaTokenFor(t, &testuser1), body, contentType) + require.Equal(t, http.StatusOK, up.Code, "upload body: %s", up.Body.String()) + + rec := getBackgroundRequest(t, e, "1", humaTokenFor(t, &testuser1), "Wed, 21 Oct 2015 07:28:00 GMT") + assert.Equal(t, http.StatusNotModified, rec.Code, "body: %s", rec.Body.String()) + assert.Empty(t, rec.Body.Bytes(), "a 304 must not carry a body") + }) + + t.Run("Project without a background returns 404", func(t *testing.T) { + // testuser1 owns project 21, which has no background and isn't uploaded to + // by any other subtest (project 1 is, and subtests share this env). + rec := getBackgroundRequest(t, e, "21", humaTokenFor(t, &testuser1), "") + assert.Equal(t, http.StatusNotFound, rec.Code, "body: %s", rec.Body.String()) + }) + + t.Run("Read-only user may download", func(t *testing.T) { + // testuser6 owns project 35 and uploads a real background; testuser15 has + // read-only access, which CanRead allows for the download. Uploading first + // gives the file real bytes (the fixture row has none). + body, contentType := multipartFileBody(t, "background", "bg.png", pngBytes(t)) + up := uploadBackgroundRequest(t, e, "35", humaTokenFor(t, &testuser6), body, contentType) + require.Equal(t, http.StatusOK, up.Code, "upload body: %s", up.Body.String()) + + rec := getBackgroundRequest(t, e, "35", humaTokenFor(t, &testuser15), "") + require.Equal(t, http.StatusOK, rec.Code, "body: %s", rec.Body.String()) + assert.NotEmpty(t, rec.Body.Bytes(), "the read-only user must receive the bytes") + }) + + t.Run("No access at all is forbidden", func(t *testing.T) { + // testuser1 has no access to project 35. + rec := getBackgroundRequest(t, e, "35", humaTokenFor(t, &testuser1), "") + assert.Equal(t, http.StatusForbidden, rec.Code, "body: %s", rec.Body.String()) + }) + + t.Run("Unauthenticated", func(t *testing.T) { + rec := getBackgroundRequest(t, e, "35", "", "") + assert.Equal(t, http.StatusUnauthorized, rec.Code, "body: %s", rec.Body.String()) + }) +} + +// TestHumaProjectBackgroundDownloadDisabledByConfig verifies the download route is +// absent (404) when project backgrounds are disabled. +func TestHumaProjectBackgroundDownloadDisabledByConfig(t *testing.T) { + _, err := setupTestEnv() + require.NoError(t, err) + + config.BackgroundsEnabled.Set(false) + defer config.BackgroundsEnabled.Set(true) + + e := routes.NewEcho() + routes.RegisterRoutes(e) + + rec := getBackgroundRequest(t, e, "35", humaTokenFor(t, &testuser6), "") + assert.Equal(t, http.StatusNotFound, rec.Code, "route must be absent when backgrounds are disabled; body: %s", rec.Body.String()) +} + +// TestHumaUnsplashProxy covers the Unsplash image/thumb proxy routes' gating and auth. +// They only register when the unsplash provider is enabled (off by default), so the +// router is rebuilt with the flag on. The proxy's happy path needs the live Unsplash +// API and is therefore not covered here, matching v1 (which has no proxy tests). +func TestHumaUnsplashProxy(t *testing.T) { + _, err := setupTestEnv() + require.NoError(t, err) + + t.Run("Routes absent when unsplash is disabled", func(t *testing.T) { + // Unsplash is disabled by default; the proxy routes must not exist. + e := routes.NewEcho() + routes.RegisterRoutes(e) + + rec := humaRequest(t, e, http.MethodGet, "/api/v2/backgrounds/unsplash/images/abc", "", humaTokenFor(t, &testuser1), "") + assert.Equal(t, http.StatusNotFound, rec.Code, "image proxy must be absent when unsplash is disabled; body: %s", rec.Body.String()) + + rec = humaRequest(t, e, http.MethodGet, "/api/v2/backgrounds/unsplash/images/abc/thumb", "", humaTokenFor(t, &testuser1), "") + assert.Equal(t, http.StatusNotFound, rec.Code, "thumb proxy must be absent when unsplash is disabled; body: %s", rec.Body.String()) + }) + + t.Run("Proxies require auth when unsplash is enabled", func(t *testing.T) { + config.BackgroundsUnsplashEnabled.Set(true) + defer config.BackgroundsUnsplashEnabled.Set(false) + + e := routes.NewEcho() + routes.RegisterRoutes(e) + + rec := humaRequest(t, e, http.MethodGet, "/api/v2/backgrounds/unsplash/images/abc", "", "", "") + assert.Equal(t, http.StatusUnauthorized, rec.Code, "image proxy body: %s", rec.Body.String()) + + rec = humaRequest(t, e, http.MethodGet, "/api/v2/backgrounds/unsplash/images/abc/thumb", "", "", "") + assert.Equal(t, http.StatusUnauthorized, rec.Code, "thumb proxy body: %s", rec.Body.String()) + }) +} From ffcf92936a86a945462fb20c77f293458ba83c26 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 17 Jun 2026 11:38:59 +0000 Subject: [PATCH 18/53] chore(deps): update dev-dependencies --- desktop/package.json | 2 +- desktop/pnpm-lock.yaml | 10 +- frontend/package.json | 10 +- frontend/pnpm-lock.yaml | 392 +++++++++++++++++++++------------------- 4 files changed, 215 insertions(+), 199 deletions(-) diff --git a/desktop/package.json b/desktop/package.json index 415238608..1aaabf2c2 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -61,7 +61,7 @@ } }, "devDependencies": { - "electron": "40.10.3", + "electron": "40.10.4", "electron-builder": "26.15.3", "unzipper": "0.12.3" }, diff --git a/desktop/pnpm-lock.yaml b/desktop/pnpm-lock.yaml index 0eaa08028..248ad6337 100644 --- a/desktop/pnpm-lock.yaml +++ b/desktop/pnpm-lock.yaml @@ -23,8 +23,8 @@ importers: version: 5.2.1 devDependencies: electron: - specifier: 40.10.3 - version: 40.10.3 + specifier: 40.10.4 + version: 40.10.4 electron-builder: specifier: 26.15.3 version: 26.15.3(electron-builder-squirrel-windows@24.13.3) @@ -535,8 +535,8 @@ packages: electron-publish@26.15.3: resolution: {integrity: sha512-g/2bn8YTavY4cuS5F+jOS7zmZbXXBV8KZ8yHKfJjFPoKtzBqrpCdNPxBd3tqdBwP7BVd0lGzf7Bk2s0KesWZ4Q==} - electron@40.10.3: - resolution: {integrity: sha512-DdWRsHm4j5wH9TMcfnB2Dqx44G/6BgLKSG/oeRe9kS60pfqCUwzUkHk0ClwvZzBVXtJ1kcdkHVRrJsl1ooKp+g==} + electron@40.10.4: + resolution: {integrity: sha512-ouNZrXXmdPL/wiTQ+xzXpb7B/BHg+j7XARig0SE7azFO3bjbYUd6lFjIAAiDQ02Pl/Oj7MUk+4C0hdf9yFtA1A==} engines: {node: '>= 22.12.0'} hasBin: true @@ -2184,7 +2184,7 @@ snapshots: transitivePeerDependencies: - supports-color - electron@40.10.3: + electron@40.10.4: dependencies: '@electron-internal/extract-zip': 1.0.2 '@electron/get': 5.0.0 diff --git a/frontend/package.json b/frontend/package.json index 5c520099d..d2ce35838 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -117,8 +117,8 @@ "@types/node": "24.13.2", "@types/sortablejs": "1.15.9", "@types/ws": "8.18.1", - "@typescript-eslint/eslint-plugin": "8.61.0", - "@typescript-eslint/parser": "8.61.0", + "@typescript-eslint/eslint-plugin": "8.61.1", + "@typescript-eslint/parser": "8.61.1", "@vitejs/plugin-vue": "6.0.7", "@vue/eslint-config-typescript": "14.8.0", "@vue/test-utils": "2.4.11", @@ -132,13 +132,13 @@ "eslint": "9.39.4", "eslint-plugin-depend": "1.5.0", "eslint-plugin-vue": "10.9.2", - "happy-dom": "20.10.3", + "happy-dom": "20.10.5", "histoire": "1.0.0-beta.1", "otplib": "12.0.1", "postcss": "8.5.15", "postcss-easing-gradients": "3.0.1", "postcss-html": "1.8.1", - "postcss-preset-env": "11.3.0", + "postcss-preset-env": "11.3.1", "rollup": "4.62.0", "rollup-plugin-visualizer": "6.0.11", "sass-embedded": "1.100.0", @@ -154,7 +154,7 @@ "vite-plugin-pwa": "1.3.0", "vite-plugin-vue-devtools": "8.1.3", "vite-svg-loader": "5.1.1", - "vitest": "4.1.8", + "vitest": "4.1.9", "vue-tsc": "3.3.5", "wait-on": "9.0.10", "workbox-cli": "7.4.1", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index d818b1c71..5d8174584 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -218,17 +218,17 @@ importers: specifier: 8.18.1 version: 8.18.1 '@typescript-eslint/eslint-plugin': - specifier: 8.61.0 - version: 8.61.0(@typescript-eslint/parser@8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + specifier: 8.61.1 + version: 8.61.1(@typescript-eslint/parser@8.61.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/parser': - specifier: 8.61.0 - version: 8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + specifier: 8.61.1 + version: 8.61.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) '@vitejs/plugin-vue': specifier: 6.0.7 version: 6.0.7(vite@7.3.5(@types/node@24.13.2)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.31.6)(yaml@2.8.3))(vue@3.5.27(typescript@5.9.3)) '@vue/eslint-config-typescript': specifier: 14.8.0 - version: 14.8.0(eslint-plugin-vue@10.9.2(@typescript-eslint/parser@8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@9.39.4(jiti@2.6.1))))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + version: 14.8.0(eslint-plugin-vue@10.9.2(@typescript-eslint/parser@8.61.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@9.39.4(jiti@2.6.1))))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) '@vue/test-utils': specifier: 2.4.11 version: 2.4.11(@vue/compiler-dom@3.5.27)(@vue/server-renderer@3.5.27(vue@3.5.27(typescript@5.9.3)))(vue@3.5.27(typescript@5.9.3)) @@ -261,10 +261,10 @@ importers: version: 1.5.0(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-vue: specifier: 10.9.2 - version: 10.9.2(@typescript-eslint/parser@8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@9.39.4(jiti@2.6.1))) + version: 10.9.2(@typescript-eslint/parser@8.61.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@9.39.4(jiti@2.6.1))) happy-dom: - specifier: 20.10.3 - version: 20.10.3 + specifier: 20.10.5 + version: 20.10.5 histoire: specifier: 1.0.0-beta.1 version: 1.0.0-beta.1(@types/node@24.13.2)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.31.6)(vite@7.3.5(@types/node@24.13.2)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.31.6)(yaml@2.8.3))(yaml@2.8.3) @@ -281,8 +281,8 @@ importers: specifier: 1.8.1 version: 1.8.1 postcss-preset-env: - specifier: 11.3.0 - version: 11.3.0(postcss@8.5.14) + specifier: 11.3.1 + version: 11.3.1(postcss@8.5.14) rollup: specifier: 4.62.0 version: 4.62.0 @@ -329,8 +329,8 @@ importers: specifier: 5.1.1 version: 5.1.1(vue@3.5.27(typescript@5.9.3)) vitest: - specifier: 4.1.8 - version: 4.1.8(@types/node@24.13.2)(happy-dom@20.10.3)(jsdom@27.4.0)(vite@7.3.5(@types/node@24.13.2)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.31.6)(yaml@2.8.3)) + specifier: 4.1.9 + version: 4.1.9(@types/node@24.13.2)(happy-dom@20.10.5)(jsdom@27.4.0)(vite@7.3.5(@types/node@24.13.2)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.31.6)(yaml@2.8.3)) vue-tsc: specifier: 3.3.5 version: 3.3.5(typescript@5.9.3) @@ -1007,8 +1007,8 @@ packages: '@csstools/css-parser-algorithms': ^3.0.5 '@csstools/css-tokenizer': ^3.0.4 - '@csstools/css-color-parser@4.1.1': - resolution: {integrity: sha512-eZ5XOtyhK+mggRafYUWzA0tvaYOFgdY8AkgQiCJF9qNAePnUo/zmsqqYubBBb3sQ8uNUaSKTY9s9klfRaAXL0g==} + '@csstools/css-color-parser@4.1.7': + resolution: {integrity: sha512-CmjJFQTFQx/U/xNJhSjCQ0ilpesPmNQ8+eOUeM/+kDOVW33qsIjeOXc27vrQDdWVkf83ZSWwtg7kXSUvKDJ8cQ==} engines: {node: '>=20.19.0'} peerDependencies: '@csstools/css-parser-algorithms': ^4.0.0 @@ -1057,8 +1057,8 @@ packages: '@csstools/css-parser-algorithms': ^4.0.0 '@csstools/css-tokenizer': ^4.0.0 - '@csstools/postcss-alpha-function@2.0.5': - resolution: {integrity: sha512-i2lNJ6b4GdMoybHlpUM07TIk8KQRXTTe7Qf8LfctQhjDRTIgaodWTQqzWU4fpWO/nxBWNkSloDM22Lw/30NBcg==} + '@csstools/postcss-alpha-function@2.0.6': + resolution: {integrity: sha512-XaMnJJqqZv4veulLELvM+5caEMcLTsFyqTrkwGKPMF+UbiM7dlQoe4K46EnwfSJIvnm91K1ZXsZSd3OuJ04p9w==} engines: {node: '>=20.19.0'} peerDependencies: postcss: '>=8.5.10' @@ -1069,26 +1069,26 @@ packages: peerDependencies: postcss: '>=8.5.10' - '@csstools/postcss-color-function-display-p3-linear@2.0.4': - resolution: {integrity: sha512-xrGqSFj9pu6XbJYD4NNCxYK9WFbf0KMfXFaisnJezkIRDZCwefUB2azkU4Zr0dFmLtIb9LlshrSZ0be1/QVthQ==} + '@csstools/postcss-color-function-display-p3-linear@2.0.5': + resolution: {integrity: sha512-YzY5qI0S/CsvqvMSiDn85ZyTCRLdnywxQn+6Fv8AU17aCE/fjcor54OSdVb/HlABBTcBq+d8NlWcLz11Bmo2mQ==} engines: {node: '>=20.19.0'} peerDependencies: postcss: '>=8.5.10' - '@csstools/postcss-color-function@5.0.4': - resolution: {integrity: sha512-PhUu86ppxKcNHHqrJ43ZL1mYa2uHKGRoY0KPbZA9k8iOaanL3I+1zYqbgVumxj1UgNTDw5BE3BUQ1Dono6bD6g==} + '@csstools/postcss-color-function@5.0.5': + resolution: {integrity: sha512-s+9fU1+sZazUNk0WyKShlfmTLC0fosxNY5x7DiD637xXbZLX2lyce23QrdRhytP3Ja1G77qUk6cRD37N1gemdQ==} engines: {node: '>=20.19.0'} peerDependencies: postcss: '>=8.5.10' - '@csstools/postcss-color-mix-function@4.0.4': - resolution: {integrity: sha512-zYS78MHBuih9f9qtPFcSvVXMKg9q/lNPeFJUjyw7+/W1VHRjubvs5MlzuC363UUeahAhrOvYdo2ZZhmlxZbj6w==} + '@csstools/postcss-color-mix-function@4.0.5': + resolution: {integrity: sha512-eBrrzTKudOlDl2XOJzW/pzHPIkC8tGkcGpNiFO/vmevb08U1huYEINhlxr8iz4OzSqs1GtiJx4d2v5iHFOZjNw==} engines: {node: '>=20.19.0'} peerDependencies: postcss: '>=8.5.10' - '@csstools/postcss-color-mix-variadic-function-arguments@2.0.4': - resolution: {integrity: sha512-qlrABMEFPUqbCxX0aOsHcxQZo/8XgMqnEtqqtVUbdizcuTUtJyLdHike7hkoemwDspMSEotdIfRlUY4jhZaD+A==} + '@csstools/postcss-color-mix-variadic-function-arguments@2.0.5': + resolution: {integrity: sha512-O4tE1hZXfEAbTP1IC2R857KjPCLNtpsFUqY2dqgycF/3M6GuFyJI20EWwkxVZzlSFvWdIcNppwRf9pxPFn0qnA==} engines: {node: '>=20.19.0'} peerDependencies: postcss: '>=8.5.10' @@ -1105,8 +1105,8 @@ packages: peerDependencies: postcss: '>=8.5.10' - '@csstools/postcss-contrast-color-function@3.0.4': - resolution: {integrity: sha512-EiTZzUICztGqEuYg8AVCUWH9vH2jDzO6RryxMja+PWluZHP6n3/iG6i1leTt5LiDQjDUQlCRbQtMNj7V7S+b4Q==} + '@csstools/postcss-contrast-color-function@3.0.5': + resolution: {integrity: sha512-gfdTZ4a5ioL2zM/yN2FqExy6rql+6egkI5sDuK9MvrbfrVJMzB0OjiCkboT5UprU/P0JwfTiIutW1ZSyqK4Icw==} engines: {node: '>=20.19.0'} peerDependencies: postcss: '>=8.5.10' @@ -1129,20 +1129,20 @@ packages: peerDependencies: postcss: '>=8.5.10' - '@csstools/postcss-gamut-mapping@3.0.4': - resolution: {integrity: sha512-2dWGsxtxypKU9Ra862F2335W8xegRwl9ohQ6hk808PiQlEahSaFtt5fqsGmKDaSiaFUx+2X8GZxVo970Ajr2vQ==} + '@csstools/postcss-gamut-mapping@3.0.5': + resolution: {integrity: sha512-X6XkKkR9R8KyJey9n1ryEzzfX6WpihPz/JBsyIVvxAlztQcMjMA7I9mMybWVv3ZyRMC+0+H7RlIUe85vZkasNQ==} engines: {node: '>=20.19.0'} peerDependencies: postcss: '>=8.5.10' - '@csstools/postcss-gradients-interpolation-method@6.0.4': - resolution: {integrity: sha512-sC/7dqVTtQTniLjPp/NagzeUn4sGinnMTicNBLDzirKq/GNXuJaApBOnvBmgNXjV6XPizfMhNRYCk5stn3q2nQ==} + '@csstools/postcss-gradients-interpolation-method@6.0.5': + resolution: {integrity: sha512-wXiZI6bLRAGcw7XuzsqqPnTVNrHFkHTkcymK2su+ynJjemfCdpCD9HdG+ICikPqtQ782r6LSZdyC3cDhSQqF3Q==} engines: {node: '>=20.19.0'} peerDependencies: postcss: '>=8.5.10' - '@csstools/postcss-hwb-function@5.0.4': - resolution: {integrity: sha512-cl0KPaaeYyAXNHO3pqK8adbpbAGmIU1cT1thyaEkmP8yvbJvmyztkpdGADGqziUUoh4dZQ0IhHxOxnKQ296T+A==} + '@csstools/postcss-hwb-function@5.0.5': + resolution: {integrity: sha512-HeJOXAMr1nYHZ7gJT1+6d899X9Y+5qJcpbLJ8WzhujQOIB4oqbzeP3769sd1xl3eH4qbasxtewxr4crs08SEQw==} engines: {node: '>=20.19.0'} peerDependencies: postcss: '>=8.5.10' @@ -1237,8 +1237,8 @@ packages: peerDependencies: postcss: '>=8.5.10' - '@csstools/postcss-oklab-function@5.0.4': - resolution: {integrity: sha512-vIgrKe5ffW99it5SUIXOBczGLSiTaHBhU6afVr9KPwoZ4uq9H0E3Ehvi+xsUjmvnAyMTxOUSszNo04kEhbvYjQ==} + '@csstools/postcss-oklab-function@5.0.5': + resolution: {integrity: sha512-A+Nkzj2ODvQboM5FlqEcp0iqilyVo78f9FMx/3cHrRrEBqCymSXvf8sa1cTY54lJoUVI3Sn9XysgvYaVIAuIYg==} engines: {node: '>=20.19.0'} peerDependencies: postcss: '>=8.5.10' @@ -1267,8 +1267,8 @@ packages: peerDependencies: postcss: '>=8.5.10' - '@csstools/postcss-relative-color-syntax@4.0.4': - resolution: {integrity: sha512-reFFKD9eS602We8621e5cAroKD7hH4104duLNBBhzwawGN7dhbnL1+c/DRHqwyq6eGK35HaKMMiifEZhAztlOA==} + '@csstools/postcss-relative-color-syntax@4.0.5': + resolution: {integrity: sha512-kBzf+LIm824cpjsZPhNtl/2N1KK+TXnxy8Kce4y+pEAQSrxhpX6WDUg54wjdHBGx2UZUXKBnlaUOsc71sSRDvg==} engines: {node: '>=20.19.0'} peerDependencies: postcss: '>=8.5.10' @@ -2612,11 +2612,11 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/eslint-plugin@8.61.0': - resolution: {integrity: sha512-bFNvl9ZczlVb+wR2Akszf3gHfKVj/8WanXaGJ3UstTA7brNKg0cNdk6X1Psu5V7MZ2oQtzZKOEzIUehaoxbDGw==} + '@typescript-eslint/eslint-plugin@8.61.1': + resolution: {integrity: sha512-ZPlVl3PB3et/59Ne0fv/sci6ZXz4T4Hp4nTJ56i/Y0gR89ARb+KphojTq6j+56E5PIezmOIOOWyY+aWQFd+IkQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.61.0 + '@typescript-eslint/parser': ^8.61.1 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' @@ -2627,8 +2627,8 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.61.0': - resolution: {integrity: sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==} + '@typescript-eslint/parser@8.61.1': + resolution: {integrity: sha512-PJ5vePq5/ognBbrIcoC5+SHO5dfpeLPzP9FpLkzWrguoYQEeeSjlJpVwOpo1JRSTEi7dRcwNy4h4dzV70PqHcg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -2646,8 +2646,8 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.61.0': - resolution: {integrity: sha512-DV42F7MLJO6Rax7SK1yg43tcnEfGUrurSpSxKuVX+a3RCTzBlH3fuxprrOJXKCJGAaw82xXocikJ0uQaqwXgGA==} + '@typescript-eslint/project-service@8.61.1': + resolution: {integrity: sha512-PrC4JYGmR241lYnfhmKGTXkFqv8+ymbTFgSAY0fVXpY82/QkMw5TZPl+vGzuDDU2QYJk9fIDOBTntF+yDv9LEA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' @@ -2660,8 +2660,8 @@ packages: resolution: {integrity: sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/scope-manager@8.61.0': - resolution: {integrity: sha512-IWdXFHFSb6mlC3HPc7QsLDm5zYEbUla6trDEHf32D3/dnuUyXd87plScSNXSbm0/RxMvObpI17sv/EDTGrGZkA==} + '@typescript-eslint/scope-manager@8.61.1': + resolution: {integrity: sha512-L2bdIeoQS8FlKAvONAr20w6OcLXeB+qiDKbAooS9A0Ben+iSIkBef0FxqwKWYqt5sa0i4KJtxVyVmhMylKzF5w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@typescript-eslint/tsconfig-utils@8.58.0': @@ -2682,6 +2682,12 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/tsconfig-utils@8.61.1': + resolution: {integrity: sha512-UN/H4di+OO7EWx2ovME+8t31YO+KVnK0RRKEHR3kOt21/Ay8BOq3M1OMvWs5vNiqcFCYGYoxK3MXPZzmMUE+yg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/type-utils@8.60.1': resolution: {integrity: sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2689,8 +2695,8 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.61.0': - resolution: {integrity: sha512-TuBiQYIkd97yBfInHCTKVYMbX4kvEmpOEuixIuzCU9p8BGT1SfyyO0d0IfDMbPIHcjn/hWnusUX5e8v5Xg+X8A==} + '@typescript-eslint/type-utils@8.61.1': + resolution: {integrity: sha512-GYRicKmVK0C4fsKgaACaknOUAq9Oa2kwsjnpFhFcS/5p4Ht5IP9OVLbgIgcK4SRk92nVHFluurg1lumD9dBcLw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -2708,6 +2714,10 @@ packages: resolution: {integrity: sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/types@8.61.1': + resolution: {integrity: sha512-G+CRlPqLv7Bz1IZVs03x5K59F1veqL0EJUROAdGhKsEq8qOiRiZbI+HUojPq5l0fEGOKModD9br6lObhB8zkoA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/typescript-estree@8.58.0': resolution: {integrity: sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2720,8 +2730,8 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/typescript-estree@8.61.0': - resolution: {integrity: sha512-42zatd5qSvvcV1JdDBCLxYRznvP4eIHpPoZXdkPFnAmanA4FuZ5dibSnCBggY8hQnqajPpoGjXFdZ7fIJKQnlA==} + '@typescript-eslint/typescript-estree@8.61.1': + resolution: {integrity: sha512-u+oQD3BqYWPc8YV9Zab4vaJElJuwOLPRc10Jm1o/qS+6Qwen14HCWwx0Seo4LnSn2wxea2Ik8DxPt2/FHmuhrg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' @@ -2733,8 +2743,8 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.61.0': - resolution: {integrity: sha512-3bzFt7ImFMW/jVYwJamDoe/dMOdFLSC6pom6rRjdh4SZJEYupyMzem8e7vKZLclLfpHjlwSAXOUxtKxGXUiLqA==} + '@typescript-eslint/utils@8.61.1': + resolution: {integrity: sha512-1+P/3Dj6jvtybE1q0HQ6yBt/gq+oKJyLdEv4HdnqasaEXRSYCAsD59mXEVQnM/ULNdQxbX77tdG4jPRjIS6knA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -2748,8 +2758,8 @@ packages: resolution: {integrity: sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/visitor-keys@8.61.0': - resolution: {integrity: sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ==} + '@typescript-eslint/visitor-keys@8.61.1': + resolution: {integrity: sha512-6fJ9MHWtK14C1DSkiMlHUSOmrVebL7150xZJBlJiL62jjhIA4JmOq6flwBgDxIdBKKdoiZRel+dfPD5MLfny3w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@ungap/structured-clone@1.3.0': @@ -2763,11 +2773,11 @@ packages: vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 vue: ^3.2.25 - '@vitest/expect@4.1.8': - resolution: {integrity: sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==} + '@vitest/expect@4.1.9': + resolution: {integrity: sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==} - '@vitest/mocker@4.1.8': - resolution: {integrity: sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==} + '@vitest/mocker@4.1.9': + resolution: {integrity: sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==} peerDependencies: msw: ^2.4.9 vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -2777,20 +2787,20 @@ packages: vite: optional: true - '@vitest/pretty-format@4.1.8': - resolution: {integrity: sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==} + '@vitest/pretty-format@4.1.9': + resolution: {integrity: sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==} - '@vitest/runner@4.1.8': - resolution: {integrity: sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==} + '@vitest/runner@4.1.9': + resolution: {integrity: sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==} - '@vitest/snapshot@4.1.8': - resolution: {integrity: sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==} + '@vitest/snapshot@4.1.9': + resolution: {integrity: sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==} - '@vitest/spy@4.1.8': - resolution: {integrity: sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==} + '@vitest/spy@4.1.9': + resolution: {integrity: sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==} - '@vitest/utils@4.1.8': - resolution: {integrity: sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==} + '@vitest/utils@4.1.9': + resolution: {integrity: sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==} '@volar/language-core@2.4.28': resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==} @@ -4088,8 +4098,8 @@ packages: resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} engines: {node: '>=6.0'} - happy-dom@20.10.3: - resolution: {integrity: sha512-Hjdiy8RziuCcn5z04QI/rlsNuQoG8P0xxjgvsSMpi89cvIXIOcucQtiHS1yHSShxoBcSCeYqAskINmTiy/mlfw==} + happy-dom@20.10.5: + resolution: {integrity: sha512-0aA6BQoMnpcRE/c1E8ZyF2jXnET7MJskereWOXher4CJuYjrI5esN0Az/1NPMD4KeWUbampBGw2MGqabMPFIbg==} engines: {node: '>=20.0.0'} hard-rejection@2.1.0: @@ -5116,8 +5126,8 @@ packages: peerDependencies: postcss: '>=8.5.10' - postcss-color-functional-notation@8.0.4: - resolution: {integrity: sha512-Zn3yPgBFakVXthmA2n1NUMY7gdhuFUB/DrUJ0Eug/d0rl9wahMQZykp4NVTJLGzQrDUwZ2rzjiTeW5udxFNG8A==} + postcss-color-functional-notation@8.0.5: + resolution: {integrity: sha512-Cxr97Vtt2VeJCGaex0JNSU5MViqYtjKmJLHKM+jI7d+qIs0J5xgHEVG6Q2bTCaFJ1yjcFz9s9VmWCibuzk3+MA==} engines: {node: '>=20.19.0'} peerDependencies: postcss: '>=8.5.10' @@ -5201,8 +5211,8 @@ packages: peerDependencies: postcss: '>=8.5.10' - postcss-lab-function@8.0.4: - resolution: {integrity: sha512-dqcJSzVasdELD9xqJ1wfP95uzP57J6zFd80c7S3AWK127H9zwqR9Kbk5ZgyIfN2DiMStI7Vq8E7ablXNeTvpew==} + postcss-lab-function@8.0.5: + resolution: {integrity: sha512-ohQnYx1LloPkiLQhAjpt/Y9tAGCGOBOUaxgbcmO+1bDTFzUQCTfdpemOVh6oewI4V2K6q7+Vz8d3rP1glvK3uw==} engines: {node: '>=20.19.0'} peerDependencies: postcss: '>=8.5.10' @@ -5245,8 +5255,8 @@ packages: peerDependencies: postcss: '>=8.5.10' - postcss-preset-env@11.3.0: - resolution: {integrity: sha512-PpijTuY+NT35vvk7us0pw9lJVrsZZWukjONZsza2Kq1Gag8nrUXRkgdKdxyyhZPJ6R43L3/nLpspUK99TmU9xg==} + postcss-preset-env@11.3.1: + resolution: {integrity: sha512-ox2lu2L0fbuKXB0zRcUFCNii7koS9+fNLFqj+WOKaJ4DU/zZsYkFHOmz73lWNTKx8OHDqnV0R7Si98PIbJXLjQ==} engines: {node: '>=20.19.0'} peerDependencies: postcss: '>=8.5.10' @@ -6491,20 +6501,20 @@ packages: yaml: optional: true - vitest@4.1.8: - resolution: {integrity: sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==} + vitest@4.1.9: + resolution: {integrity: sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==} 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.8 - '@vitest/browser-preview': 4.1.8 - '@vitest/browser-webdriverio': 4.1.8 - '@vitest/coverage-istanbul': 4.1.8 - '@vitest/coverage-v8': 4.1.8 - '@vitest/ui': 4.1.8 + '@vitest/browser-playwright': 4.1.9 + '@vitest/browser-preview': 4.1.9 + '@vitest/browser-webdriverio': 4.1.9 + '@vitest/coverage-istanbul': 4.1.9 + '@vitest/coverage-v8': 4.1.9 + '@vitest/ui': 4.1.9 happy-dom: '*' jsdom: '*' vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -7704,7 +7714,7 @@ snapshots: '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/css-color-parser@4.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + '@csstools/css-color-parser@4.1.7(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': dependencies: '@csstools/color-helpers': 6.0.2 '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) @@ -7736,9 +7746,9 @@ snapshots: '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 - '@csstools/postcss-alpha-function@2.0.5(postcss@8.5.14)': + '@csstools/postcss-alpha-function@2.0.6(postcss@8.5.14)': dependencies: - '@csstools/css-color-parser': 4.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.1.7(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 '@csstools/postcss-progressive-custom-properties': 5.1.0(postcss@8.5.14) @@ -7751,36 +7761,36 @@ snapshots: postcss: 8.5.14 postcss-selector-parser: 7.1.1 - '@csstools/postcss-color-function-display-p3-linear@2.0.4(postcss@8.5.14)': + '@csstools/postcss-color-function-display-p3-linear@2.0.5(postcss@8.5.14)': dependencies: - '@csstools/css-color-parser': 4.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.1.7(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 '@csstools/postcss-progressive-custom-properties': 5.1.0(postcss@8.5.14) '@csstools/utilities': 3.0.0(postcss@8.5.14) postcss: 8.5.14 - '@csstools/postcss-color-function@5.0.4(postcss@8.5.14)': + '@csstools/postcss-color-function@5.0.5(postcss@8.5.14)': dependencies: - '@csstools/css-color-parser': 4.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.1.7(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 '@csstools/postcss-progressive-custom-properties': 5.1.0(postcss@8.5.14) '@csstools/utilities': 3.0.0(postcss@8.5.14) postcss: 8.5.14 - '@csstools/postcss-color-mix-function@4.0.4(postcss@8.5.14)': + '@csstools/postcss-color-mix-function@4.0.5(postcss@8.5.14)': dependencies: - '@csstools/css-color-parser': 4.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.1.7(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 '@csstools/postcss-progressive-custom-properties': 5.1.0(postcss@8.5.14) '@csstools/utilities': 3.0.0(postcss@8.5.14) postcss: 8.5.14 - '@csstools/postcss-color-mix-variadic-function-arguments@2.0.4(postcss@8.5.14)': + '@csstools/postcss-color-mix-variadic-function-arguments@2.0.5(postcss@8.5.14)': dependencies: - '@csstools/css-color-parser': 4.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.1.7(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 '@csstools/postcss-progressive-custom-properties': 5.1.0(postcss@8.5.14) @@ -7801,9 +7811,9 @@ snapshots: '@csstools/utilities': 3.0.0(postcss@8.5.14) postcss: 8.5.14 - '@csstools/postcss-contrast-color-function@3.0.4(postcss@8.5.14)': + '@csstools/postcss-contrast-color-function@3.0.5(postcss@8.5.14)': dependencies: - '@csstools/css-color-parser': 4.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.1.7(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 '@csstools/postcss-progressive-custom-properties': 5.1.0(postcss@8.5.14) @@ -7828,25 +7838,25 @@ snapshots: '@csstools/utilities': 3.0.0(postcss@8.5.14) postcss: 8.5.14 - '@csstools/postcss-gamut-mapping@3.0.4(postcss@8.5.14)': + '@csstools/postcss-gamut-mapping@3.0.5(postcss@8.5.14)': dependencies: - '@csstools/css-color-parser': 4.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.1.7(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 postcss: 8.5.14 - '@csstools/postcss-gradients-interpolation-method@6.0.4(postcss@8.5.14)': + '@csstools/postcss-gradients-interpolation-method@6.0.5(postcss@8.5.14)': dependencies: - '@csstools/css-color-parser': 4.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.1.7(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 '@csstools/postcss-progressive-custom-properties': 5.1.0(postcss@8.5.14) '@csstools/utilities': 3.0.0(postcss@8.5.14) postcss: 8.5.14 - '@csstools/postcss-hwb-function@5.0.4(postcss@8.5.14)': + '@csstools/postcss-hwb-function@5.0.5(postcss@8.5.14)': dependencies: - '@csstools/css-color-parser': 4.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.1.7(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 '@csstools/postcss-progressive-custom-properties': 5.1.0(postcss@8.5.14) @@ -7941,9 +7951,9 @@ snapshots: postcss: 8.5.14 postcss-value-parser: 4.2.0 - '@csstools/postcss-oklab-function@5.0.4(postcss@8.5.14)': + '@csstools/postcss-oklab-function@5.0.5(postcss@8.5.14)': dependencies: - '@csstools/css-color-parser': 4.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.1.7(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 '@csstools/postcss-progressive-custom-properties': 5.1.0(postcss@8.5.14) @@ -7972,9 +7982,9 @@ snapshots: '@csstools/css-tokenizer': 4.0.0 postcss: 8.5.14 - '@csstools/postcss-relative-color-syntax@4.0.4(postcss@8.5.14)': + '@csstools/postcss-relative-color-syntax@4.0.5(postcss@8.5.14)': dependencies: - '@csstools/css-color-parser': 4.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.1.7(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 '@csstools/postcss-progressive-custom-properties': 5.1.0(postcss@8.5.14) @@ -9228,14 +9238,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@8.61.0(@typescript-eslint/parser@8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.61.1(@typescript-eslint/parser@8.61.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.61.0 - '@typescript-eslint/type-utils': 8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.61.0 + '@typescript-eslint/parser': 8.61.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.61.1 + '@typescript-eslint/type-utils': 8.61.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.61.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.61.1 eslint: 9.39.4(jiti@2.6.1) ignore: 7.0.5 natural-compare: 1.4.0 @@ -9256,12 +9266,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/parser@8.61.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.61.0 - '@typescript-eslint/types': 8.61.0 - '@typescript-eslint/typescript-estree': 8.61.0(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.61.0 + '@typescript-eslint/scope-manager': 8.61.1 + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/typescript-estree': 8.61.1(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.61.1 debug: 4.4.3 eslint: 9.39.4(jiti@2.6.1) typescript: 5.9.3 @@ -9270,8 +9280,8 @@ snapshots: '@typescript-eslint/project-service@8.58.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.60.1(typescript@5.9.3) - '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/tsconfig-utils': 8.61.0(typescript@5.9.3) + '@typescript-eslint/types': 8.61.0 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: @@ -9279,17 +9289,17 @@ snapshots: '@typescript-eslint/project-service@8.60.1(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.60.1(typescript@5.9.3) - '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/tsconfig-utils': 8.61.0(typescript@5.9.3) + '@typescript-eslint/types': 8.61.0 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.61.0(typescript@5.9.3)': + '@typescript-eslint/project-service@8.61.1(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.61.0(typescript@5.9.3) - '@typescript-eslint/types': 8.61.0 + '@typescript-eslint/tsconfig-utils': 8.61.1(typescript@5.9.3) + '@typescript-eslint/types': 8.61.1 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: @@ -9305,10 +9315,10 @@ snapshots: '@typescript-eslint/types': 8.60.1 '@typescript-eslint/visitor-keys': 8.60.1 - '@typescript-eslint/scope-manager@8.61.0': + '@typescript-eslint/scope-manager@8.61.1': dependencies: - '@typescript-eslint/types': 8.61.0 - '@typescript-eslint/visitor-keys': 8.61.0 + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/visitor-keys': 8.61.1 '@typescript-eslint/tsconfig-utils@8.58.0(typescript@5.9.3)': dependencies: @@ -9322,6 +9332,10 @@ snapshots: dependencies: typescript: 5.9.3 + '@typescript-eslint/tsconfig-utils@8.61.1(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + '@typescript-eslint/type-utils@8.60.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.60.1 @@ -9334,11 +9348,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/type-utils@8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.61.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.61.0 - '@typescript-eslint/typescript-estree': 8.61.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/typescript-estree': 8.61.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.61.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) debug: 4.4.3 eslint: 9.39.4(jiti@2.6.1) ts-api-utils: 2.5.0(typescript@5.9.3) @@ -9352,6 +9366,8 @@ snapshots: '@typescript-eslint/types@8.61.0': {} + '@typescript-eslint/types@8.61.1': {} + '@typescript-eslint/typescript-estree@8.58.0(typescript@5.9.3)': dependencies: '@typescript-eslint/project-service': 8.58.0(typescript@5.9.3) @@ -9382,12 +9398,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.61.0(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.61.1(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.61.0(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.61.0(typescript@5.9.3) - '@typescript-eslint/types': 8.61.0 - '@typescript-eslint/visitor-keys': 8.61.0 + '@typescript-eslint/project-service': 8.61.1(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.61.1(typescript@5.9.3) + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/visitor-keys': 8.61.1 debug: 4.4.3 minimatch: 10.2.4 semver: 7.7.3 @@ -9408,12 +9424,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/utils@8.61.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.61.0 - '@typescript-eslint/types': 8.61.0 - '@typescript-eslint/typescript-estree': 8.61.0(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.61.1 + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/typescript-estree': 8.61.1(typescript@5.9.3) eslint: 9.39.4(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: @@ -9429,9 +9445,9 @@ snapshots: '@typescript-eslint/types': 8.60.1 eslint-visitor-keys: 5.0.0 - '@typescript-eslint/visitor-keys@8.61.0': + '@typescript-eslint/visitor-keys@8.61.1': dependencies: - '@typescript-eslint/types': 8.61.0 + '@typescript-eslint/types': 8.61.1 eslint-visitor-keys: 5.0.0 '@ungap/structured-clone@1.3.0': {} @@ -9442,44 +9458,44 @@ snapshots: vite: 7.3.5(@types/node@24.13.2)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.31.6)(yaml@2.8.3) vue: 3.5.27(typescript@5.9.3) - '@vitest/expect@4.1.8': + '@vitest/expect@4.1.9': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.2 - '@vitest/spy': 4.1.8 - '@vitest/utils': 4.1.8 + '@vitest/spy': 4.1.9 + '@vitest/utils': 4.1.9 chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.8(vite@7.3.5(@types/node@24.13.2)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.31.6)(yaml@2.8.3))': + '@vitest/mocker@4.1.9(vite@7.3.5(@types/node@24.13.2)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.31.6)(yaml@2.8.3))': dependencies: - '@vitest/spy': 4.1.8 + '@vitest/spy': 4.1.9 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: vite: 7.3.5(@types/node@24.13.2)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.31.6)(yaml@2.8.3) - '@vitest/pretty-format@4.1.8': + '@vitest/pretty-format@4.1.9': dependencies: tinyrainbow: 3.1.0 - '@vitest/runner@4.1.8': + '@vitest/runner@4.1.9': dependencies: - '@vitest/utils': 4.1.8 + '@vitest/utils': 4.1.9 pathe: 2.0.3 - '@vitest/snapshot@4.1.8': + '@vitest/snapshot@4.1.9': dependencies: - '@vitest/pretty-format': 4.1.8 - '@vitest/utils': 4.1.8 + '@vitest/pretty-format': 4.1.9 + '@vitest/utils': 4.1.9 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@4.1.8': {} + '@vitest/spy@4.1.9': {} - '@vitest/utils@4.1.8': + '@vitest/utils@4.1.9': dependencies: - '@vitest/pretty-format': 4.1.8 + '@vitest/pretty-format': 4.1.9 convert-source-map: 2.0.0 tinyrainbow: 3.1.0 @@ -9590,11 +9606,11 @@ snapshots: '@vue/devtools-shared@8.1.3': {} - '@vue/eslint-config-typescript@14.8.0(eslint-plugin-vue@10.9.2(@typescript-eslint/parser@8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@9.39.4(jiti@2.6.1))))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': + '@vue/eslint-config-typescript@14.8.0(eslint-plugin-vue@10.9.2(@typescript-eslint/parser@8.61.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@9.39.4(jiti@2.6.1))))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@typescript-eslint/utils': 8.60.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.4(jiti@2.6.1) - eslint-plugin-vue: 10.9.2(@typescript-eslint/parser@8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@9.39.4(jiti@2.6.1))) + eslint-plugin-vue: 10.9.2(@typescript-eslint/parser@8.61.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@9.39.4(jiti@2.6.1))) fast-glob: 3.3.3 typescript-eslint: 8.60.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) vue-eslint-parser: 10.4.0(eslint@9.39.4(jiti@2.6.1)) @@ -10513,7 +10529,7 @@ snapshots: module-replacements: 2.11.0 semver: 7.7.3 - eslint-plugin-vue@10.9.2(@typescript-eslint/parser@8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@9.39.4(jiti@2.6.1))): + eslint-plugin-vue@10.9.2(@typescript-eslint/parser@8.61.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@9.39.4(jiti@2.6.1))): dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) eslint: 9.39.4(jiti@2.6.1) @@ -10524,7 +10540,7 @@ snapshots: vue-eslint-parser: 10.4.0(eslint@9.39.4(jiti@2.6.1)) xml-name-validator: 4.0.0 optionalDependencies: - '@typescript-eslint/parser': 8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.61.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) eslint-scope@8.4.0: dependencies: @@ -10927,7 +10943,7 @@ snapshots: section-matter: 1.0.0 strip-bom-string: 1.0.0 - happy-dom@20.10.3: + happy-dom@20.10.5: dependencies: '@types/node': 24.13.2 '@types/whatwg-mimetype': 3.0.2 @@ -11951,9 +11967,9 @@ snapshots: postcss: 8.5.14 postcss-value-parser: 4.2.0 - postcss-color-functional-notation@8.0.4(postcss@8.5.14): + postcss-color-functional-notation@8.0.5(postcss@8.5.14): dependencies: - '@csstools/css-color-parser': 4.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.1.7(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 '@csstools/postcss-progressive-custom-properties': 5.1.0(postcss@8.5.14) @@ -12047,9 +12063,9 @@ snapshots: postcss: 8.5.14 postcss-value-parser: 4.2.0 - postcss-lab-function@8.0.4(postcss@8.5.14): + postcss-lab-function@8.0.5(postcss@8.5.14): dependencies: - '@csstools/css-color-parser': 4.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.1.7(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 '@csstools/postcss-progressive-custom-properties': 5.1.0(postcss@8.5.14) @@ -12088,23 +12104,23 @@ snapshots: postcss: 8.5.14 postcss-value-parser: 4.2.0 - postcss-preset-env@11.3.0(postcss@8.5.14): + postcss-preset-env@11.3.1(postcss@8.5.14): dependencies: - '@csstools/postcss-alpha-function': 2.0.5(postcss@8.5.14) + '@csstools/postcss-alpha-function': 2.0.6(postcss@8.5.14) '@csstools/postcss-cascade-layers': 6.0.0(postcss@8.5.14) - '@csstools/postcss-color-function': 5.0.4(postcss@8.5.14) - '@csstools/postcss-color-function-display-p3-linear': 2.0.4(postcss@8.5.14) - '@csstools/postcss-color-mix-function': 4.0.4(postcss@8.5.14) - '@csstools/postcss-color-mix-variadic-function-arguments': 2.0.4(postcss@8.5.14) + '@csstools/postcss-color-function': 5.0.5(postcss@8.5.14) + '@csstools/postcss-color-function-display-p3-linear': 2.0.5(postcss@8.5.14) + '@csstools/postcss-color-mix-function': 4.0.5(postcss@8.5.14) + '@csstools/postcss-color-mix-variadic-function-arguments': 2.0.5(postcss@8.5.14) '@csstools/postcss-container-rule-prelude-list': 1.0.1(postcss@8.5.14) '@csstools/postcss-content-alt-text': 3.0.1(postcss@8.5.14) - '@csstools/postcss-contrast-color-function': 3.0.4(postcss@8.5.14) + '@csstools/postcss-contrast-color-function': 3.0.5(postcss@8.5.14) '@csstools/postcss-exponential-functions': 3.0.3(postcss@8.5.14) '@csstools/postcss-font-format-keywords': 5.0.0(postcss@8.5.14) '@csstools/postcss-font-width-property': 1.0.0(postcss@8.5.14) - '@csstools/postcss-gamut-mapping': 3.0.4(postcss@8.5.14) - '@csstools/postcss-gradients-interpolation-method': 6.0.4(postcss@8.5.14) - '@csstools/postcss-hwb-function': 5.0.4(postcss@8.5.14) + '@csstools/postcss-gamut-mapping': 3.0.5(postcss@8.5.14) + '@csstools/postcss-gradients-interpolation-method': 6.0.5(postcss@8.5.14) + '@csstools/postcss-hwb-function': 5.0.5(postcss@8.5.14) '@csstools/postcss-ic-unit': 5.0.1(postcss@8.5.14) '@csstools/postcss-image-function': 1.0.0(postcss@8.5.14) '@csstools/postcss-initial': 3.0.0(postcss@8.5.14) @@ -12120,12 +12136,12 @@ snapshots: '@csstools/postcss-mixins': 1.0.0(postcss@8.5.14) '@csstools/postcss-nested-calc': 5.0.0(postcss@8.5.14) '@csstools/postcss-normalize-display-values': 5.0.1(postcss@8.5.14) - '@csstools/postcss-oklab-function': 5.0.4(postcss@8.5.14) + '@csstools/postcss-oklab-function': 5.0.5(postcss@8.5.14) '@csstools/postcss-position-area-property': 2.0.0(postcss@8.5.14) '@csstools/postcss-progressive-custom-properties': 5.1.0(postcss@8.5.14) '@csstools/postcss-property-rule-prelude-list': 2.0.0(postcss@8.5.14) '@csstools/postcss-random-function': 3.0.3(postcss@8.5.14) - '@csstools/postcss-relative-color-syntax': 4.0.4(postcss@8.5.14) + '@csstools/postcss-relative-color-syntax': 4.0.5(postcss@8.5.14) '@csstools/postcss-scope-pseudo-class': 5.0.0(postcss@8.5.14) '@csstools/postcss-sign-functions': 2.0.3(postcss@8.5.14) '@csstools/postcss-stepped-value-functions': 5.0.3(postcss@8.5.14) @@ -12143,7 +12159,7 @@ snapshots: postcss: 8.5.14 postcss-attribute-case-insensitive: 8.0.0(postcss@8.5.14) postcss-clamp: 4.1.0(postcss@8.5.14) - postcss-color-functional-notation: 8.0.4(postcss@8.5.14) + postcss-color-functional-notation: 8.0.5(postcss@8.5.14) postcss-color-hex-alpha: 11.0.0(postcss@8.5.14) postcss-color-rebeccapurple: 11.0.0(postcss@8.5.14) postcss-custom-media: 12.0.1(postcss@8.5.14) @@ -12156,7 +12172,7 @@ snapshots: postcss-font-variant: 5.0.0(postcss@8.5.14) postcss-gap-properties: 7.0.0(postcss@8.5.14) postcss-image-set-function: 8.0.0(postcss@8.5.14) - postcss-lab-function: 8.0.4(postcss@8.5.14) + postcss-lab-function: 8.0.5(postcss@8.5.14) postcss-logical: 9.0.0(postcss@8.5.14) postcss-nesting: 14.0.0(postcss@8.5.14) postcss-opacity-percentage: 3.0.0(postcss@8.5.14) @@ -13576,15 +13592,15 @@ snapshots: terser: 5.31.6 yaml: 2.8.3 - vitest@4.1.8(@types/node@24.13.2)(happy-dom@20.10.3)(jsdom@27.4.0)(vite@7.3.5(@types/node@24.13.2)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.31.6)(yaml@2.8.3)): + vitest@4.1.9(@types/node@24.13.2)(happy-dom@20.10.5)(jsdom@27.4.0)(vite@7.3.5(@types/node@24.13.2)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.31.6)(yaml@2.8.3)): dependencies: - '@vitest/expect': 4.1.8 - '@vitest/mocker': 4.1.8(vite@7.3.5(@types/node@24.13.2)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.31.6)(yaml@2.8.3)) - '@vitest/pretty-format': 4.1.8 - '@vitest/runner': 4.1.8 - '@vitest/snapshot': 4.1.8 - '@vitest/spy': 4.1.8 - '@vitest/utils': 4.1.8 + '@vitest/expect': 4.1.9 + '@vitest/mocker': 4.1.9(vite@7.3.5(@types/node@24.13.2)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.31.6)(yaml@2.8.3)) + '@vitest/pretty-format': 4.1.9 + '@vitest/runner': 4.1.9 + '@vitest/snapshot': 4.1.9 + '@vitest/spy': 4.1.9 + '@vitest/utils': 4.1.9 es-module-lexer: 2.0.0 expect-type: 1.3.0 magic-string: 0.30.21 @@ -13600,7 +13616,7 @@ snapshots: why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.13.2 - happy-dom: 20.10.3 + happy-dom: 20.10.5 jsdom: 27.4.0 transitivePeerDependencies: - msw From 5555950f0315d8f8177e066fd39fefafbd49fa83 Mon Sep 17 00:00:00 2001 From: kolaente Date: Fri, 12 Jun 2026 10:10:53 +0200 Subject: [PATCH 19/53] refactor(testing): extract e2e fixture reset/truncate into shared package Pull the HTTP-agnostic table reset and truncate-all logic out of the v1 testing handlers into pkg/routes/api/shared so /api/v2 can reuse it. v1's wire behavior is unchanged; it now delegates to the shared functions. --- pkg/routes/api/shared/testing.go | 92 ++++++++++++++++++++++++++++++++ pkg/routes/api/v1/testing.go | 66 ++--------------------- 2 files changed, 95 insertions(+), 63 deletions(-) create mode 100644 pkg/routes/api/shared/testing.go diff --git a/pkg/routes/api/shared/testing.go b/pkg/routes/api/shared/testing.go new file mode 100644 index 000000000..ba9118e5a --- /dev/null +++ b/pkg/routes/api/shared/testing.go @@ -0,0 +1,92 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-present Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package shared + +import ( + "code.vikunja.io/api/pkg/db" + "code.vikunja.io/api/pkg/events" + "code.vikunja.io/api/pkg/license" + "code.vikunja.io/api/pkg/log" +) + +// dependentTestingTables lists tables that reference a reset table by ID and +// must be truncated alongside it. Without foreign key cascades, stale rows +// would persist and pollute subsequent tests that reuse the same +// auto-increment IDs. +var dependentTestingTables = map[string][]string{ + "users": {"notifications"}, +} + +// ReplaceTableContents resets a single table to the provided rows for the e2e +// testing endpoint and returns the table's resulting contents. When truncate is +// true the table (and any dependent tables) is emptied first; otherwise the rows +// are restored on top of existing data. Callers must already have verified the +// testing token. +func ReplaceTableContents(table string, content []map[string]interface{}, truncate bool) ([]map[string]interface{}, error) { + // Wait for all async event handlers from the previous test to complete + // before modifying the database. Without this, handlers hold SQLite + // connections and starve this request's truncate/insert operations. + events.WaitForPendingHandlers() + + var err error + if truncate { + for _, dep := range dependentTestingTables[table] { + if err = db.RestoreAndTruncate(dep, nil); err != nil { + return nil, err + } + } + err = db.RestoreAndTruncate(table, content) + } else { + err = db.Restore(table, content) + } + if err != nil { + return nil, err + } + + // License state is cached at startup; re-apply so tests take effect without a restart. + if table == "license_status" { + if err := license.ReloadFromCache(); err != nil { + return nil, err + } + } + + s := db.NewSession() + defer s.Close() + data := []map[string]interface{}{} + if err := s.Table(table).Find(&data); err != nil { + return nil, err + } + return data, nil +} + +// TruncateAllTestingTables empties every Vikunja table for the e2e testing +// endpoint. Callers must already have verified the testing token. +func TruncateAllTestingTables() error { + events.WaitForPendingHandlers() + + if err := db.TruncateAllTables(); err != nil { + return err + } + + // Reload after truncate; otherwise features enabled by a prior test outlive + // the now-empty license_status table. A reload failure here is non-fatal — + // the truncate already succeeded — so it is logged and swallowed. + if err := license.ReloadFromCache(); err != nil { + log.Errorf("Error reloading license after truncate: %v", err) + } + return nil +} diff --git a/pkg/routes/api/v1/testing.go b/pkg/routes/api/v1/testing.go index 98f5aeca1..62d5f5206 100644 --- a/pkg/routes/api/v1/testing.go +++ b/pkg/routes/api/v1/testing.go @@ -22,10 +22,8 @@ import ( "net/http" "code.vikunja.io/api/pkg/config" - "code.vikunja.io/api/pkg/db" - "code.vikunja.io/api/pkg/events" - "code.vikunja.io/api/pkg/license" "code.vikunja.io/api/pkg/log" + "code.vikunja.io/api/pkg/routes/api/shared" "github.com/labstack/echo/v5" ) @@ -63,36 +61,8 @@ func HandleTesting(c *echo.Context) error { }) } - // Wait for all async event handlers from the previous test to complete - // before modifying the database. Without this, handlers hold SQLite - // connections and starve this request's truncate/insert operations. - events.WaitForPendingHandlers() - truncate := c.QueryParam("truncate") - if truncate == "true" || truncate == "" { - // When truncating certain tables, also truncate dependent tables - // whose rows reference the truncated table by user/entity ID. - // Without foreign key cascades, stale rows would persist and - // pollute subsequent tests that reuse the same auto-increment IDs. - dependentTables := map[string][]string{ - "users": {"notifications"}, - } - if deps, ok := dependentTables[table]; ok { - for _, dep := range deps { - if err = db.RestoreAndTruncate(dep, nil); err != nil { - log.Errorf("Error truncating dependent table %s: %v", dep, err) - return c.JSON(http.StatusInternalServerError, map[string]interface{}{ - "error": true, - "message": err.Error(), - }) - } - } - } - err = db.RestoreAndTruncate(table, content) - } else { - err = db.Restore(table, content) - } - + data, err := shared.ReplaceTableContents(table, content, truncate == "true" || truncate == "") if err != nil { log.Errorf("Error replacing table data: %v", err) return c.JSON(http.StatusInternalServerError, map[string]interface{}{ @@ -101,29 +71,6 @@ func HandleTesting(c *echo.Context) error { }) } - // License state is cached at startup; re-apply so tests take effect without a restart. - if table == "license_status" { - if err := license.ReloadFromCache(); err != nil { - log.Errorf("Error reloading license from seeded cache: %v", err) - return c.JSON(http.StatusInternalServerError, map[string]interface{}{ - "error": true, - "message": err.Error(), - }) - } - } - - s := db.NewSession() - defer s.Close() - data := []map[string]interface{}{} - err = s.Table(table).Find(&data) - if err != nil { - log.Errorf("Error fetching table data: %v", err) - return c.JSON(http.StatusInternalServerError, map[string]interface{}{ - "error": true, - "message": err.Error(), - }) - } - return c.JSON(http.StatusCreated, data) } @@ -142,9 +89,7 @@ func HandleTestingTruncateAll(c *echo.Context) error { return echo.ErrForbidden } - events.WaitForPendingHandlers() - - if err := db.TruncateAllTables(); err != nil { + if err := shared.TruncateAllTestingTables(); err != nil { log.Errorf("Error truncating all tables: %v", err) return c.JSON(http.StatusInternalServerError, map[string]interface{}{ "error": true, @@ -152,11 +97,6 @@ func HandleTestingTruncateAll(c *echo.Context) error { }) } - // Reload after truncate; otherwise features enabled by a prior test outlive the now-empty license_status table. - if err := license.ReloadFromCache(); err != nil { - log.Errorf("Error reloading license after truncate: %v", err) - } - return c.JSON(http.StatusOK, map[string]string{ "message": "ok", }) From 4737114b12354576a12d92dabff6b3fe39fc8e18 Mon Sep 17 00:00:00 2001 From: kolaente Date: Fri, 12 Jun 2026 10:11:00 +0200 Subject: [PATCH 20/53] feat(api/v2): add e2e testing-support endpoints on /api/v2 Port the testing fixture endpoints to /api/v2: PUT /test/{table} resets a table to a posted fixture set and DELETE /test/all truncates everything. Both authenticate with the configured testing token via a custom Authorization header (not JWT/API-token) and only mount when that token is set. Reuses the shared reset/truncate logic extracted from v1. --- pkg/routes/api/v2/testing.go | 129 +++++++++++++++++ pkg/routes/routes.go | 5 + pkg/webtests/huma_testing_test.go | 223 ++++++++++++++++++++++++++++++ 3 files changed, 357 insertions(+) create mode 100644 pkg/routes/api/v2/testing.go create mode 100644 pkg/webtests/huma_testing_test.go diff --git a/pkg/routes/api/v2/testing.go b/pkg/routes/api/v2/testing.go new file mode 100644 index 000000000..2f753f3fe --- /dev/null +++ b/pkg/routes/api/v2/testing.go @@ -0,0 +1,129 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-present Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package apiv2 + +import ( + "context" + "net/http" + + "code.vikunja.io/api/pkg/config" + "code.vikunja.io/api/pkg/log" + "code.vikunja.io/api/pkg/routes/api/shared" + + "github.com/danielgtaylor/huma/v2" +) + +// testingReplaceInput is the request for resetting a single table. The +// Authorization header carries the configured testing token (not a JWT or API +// token); the endpoint is public and checks it in-handler like v1. +type testingReplaceInput struct { + Table string `path:"table" doc:"The table to reset."` + // String (not bool) so absent is distinguishable from an explicit "false": + // like v1, an absent truncate parameter means truncate. Huma does not + // support *bool params, and a bool with default:"true" silently ignores an + // explicit ?truncate=false, so the parameter is read as a raw string and + // interpreted in the handler exactly like v1 does. + Truncate string `query:"truncate" enum:"true,false" doc:"Empty the table (and its dependents) before inserting the rows. Defaults to true; pass false to restore on top of existing data."` + Authorization string `header:"Authorization" doc:"The configured testing token."` + Body []map[string]any `doc:"The rows to write into the table. Free-form objects matching the table's columns."` +} + +type testingReplaceBody struct { + Body []map[string]any `doc:"The table's contents after the reset."` +} + +type testingTruncateAllInput struct { + Authorization string `header:"Authorization" doc:"The configured testing token."` +} + +type testingTruncateAllBody struct { + Body struct { + Message string `json:"message" doc:"Always \"ok\" on success."` + } +} + +// RegisterTestingRoutes wires the e2e testing-support endpoints onto the Huma +// API. They are only mounted when the testing token is configured, matching v1. +func RegisterTestingRoutes(api huma.API) { + if config.ServiceTestingtoken.GetString() == "" { + return + } + + tags := []string{"testing"} + // Public: opt out of the globally-applied JWT/API-token auth — these + // authenticate with the testing token via the Authorization header + // instead. Their paths are also listed in unauthenticatedAPIPaths so the + // token middleware lets them through. + noAuth := []map[string][]string{} + + Register(api, huma.Operation{ + OperationID: "testing-truncate-all", + Summary: "Truncate all tables", + Description: "Removes all data from every Vikunja table. Used by e2e tests to ensure a clean state before each test. Authenticates with the configured testing token via the Authorization header, not a JWT or API token.", + Method: http.MethodDelete, + Path: "/test/all", + Tags: tags, + Security: noAuth, + // v1 returns 200 with a body rather than the 204 a DELETE would default to. + DefaultStatus: http.StatusOK, + }, testingTruncateAll) + + Register(api, huma.Operation{ + OperationID: "testing-replace-table", + Summary: "Reset a table to a defined state", + Description: "Replaces the contents of the named table with the rows in the payload and returns the resulting contents. Used by e2e tests to seed fixtures. Authenticates with the configured testing token via the Authorization header, not a JWT or API token.", + Method: http.MethodPut, + Path: "/test/{table}", + Tags: tags, + Security: noAuth, + // Mirror v1's 201 for a successful reset. + DefaultStatus: http.StatusCreated, + }, testingReplaceTable) +} + +func init() { AddRouteRegistrar(RegisterTestingRoutes) } + +func testingReplaceTable(_ context.Context, in *testingReplaceInput) (*testingReplaceBody, error) { + if in.Authorization != config.ServiceTestingtoken.GetString() { + return nil, huma.Error403Forbidden("forbidden") + } + + // Mirror v1: absent or "true" truncates; only an explicit "false" appends. + truncate := in.Truncate == "true" || in.Truncate == "" + data, err := shared.ReplaceTableContents(in.Table, in.Body, truncate) + if err != nil { + log.Errorf("Error replacing table data: %v", err) + return nil, huma.Error500InternalServerError("could not replace table data") + } + + return &testingReplaceBody{Body: data}, nil +} + +func testingTruncateAll(_ context.Context, in *testingTruncateAllInput) (*testingTruncateAllBody, error) { + if in.Authorization != config.ServiceTestingtoken.GetString() { + return nil, huma.Error403Forbidden("forbidden") + } + + if err := shared.TruncateAllTestingTables(); err != nil { + log.Errorf("Error truncating all tables: %v", err) + return nil, huma.Error500InternalServerError("could not truncate tables") + } + + out := &testingTruncateAllBody{} + out.Body.Message = "ok" + return out, nil +} diff --git a/pkg/routes/routes.go b/pkg/routes/routes.go index 9f6af5af3..e232cbbfa 100644 --- a/pkg/routes/routes.go +++ b/pkg/routes/routes.go @@ -360,6 +360,11 @@ var unauthenticatedAPIPaths = map[string]bool{ "/api/v2/user/confirm": true, "/api/v2/shares/:share/auth": true, "/api/v2/oauth/token": true, + + // Testing endpoints authenticate with the testing token via a custom + // Authorization header, not a JWT; mounted only when that token is set. + "/api/v2/test/all": true, + "/api/v2/test/:table": true, } // collectRoutesForAPITokens collects all routes for API token permission checking. diff --git a/pkg/webtests/huma_testing_test.go b/pkg/webtests/huma_testing_test.go new file mode 100644 index 000000000..4f786c8f1 --- /dev/null +++ b/pkg/webtests/huma_testing_test.go @@ -0,0 +1,223 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-present Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package webtests + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "code.vikunja.io/api/pkg/config" + "code.vikunja.io/api/pkg/db" + "code.vikunja.io/api/pkg/events" + "code.vikunja.io/api/pkg/files" + "code.vikunja.io/api/pkg/license" + "code.vikunja.io/api/pkg/log" + "code.vikunja.io/api/pkg/models" + "code.vikunja.io/api/pkg/modules/keyvalue" + "code.vikunja.io/api/pkg/modules/migration" + "code.vikunja.io/api/pkg/routes" + "code.vikunja.io/api/pkg/user" + + "github.com/labstack/echo/v5" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "src.techknowlogick.com/xormigrate" +) + +const testingToken = "test-testing-token" + +// setupTestingEnv mirrors setupTestEnv but sets the testing token before +// registering routes, so the config-gated /api/v2/test/* endpoints mount. +// When token is empty the endpoints stay unmounted (the disabled case). +func setupTestingEnv(t *testing.T, token string) *echo.Echo { + t.Helper() + config.InitDefaultConfig() + config.ServicePublicURL.Set("https://localhost") + config.ServiceTestingtoken.Set(token) + t.Cleanup(func() { config.ServiceTestingtoken.Set("") }) + + log.InitLogger() + files.InitTests() + user.InitTests() + models.SetupTests() + events.Fake() + keyvalue.InitStorage() + + // models.SetupTests only syncs models + notifications tables, but + // TruncateAllTables walks *every* registered table — including ones created + // by migration in production (license_status, migration_status) plus + // xormigrate's "migration" tracking table. Create them here so truncate-all + // doesn't hit "no such table" (the same gap that kept v1 from testing it). + engine, err := db.CreateTestEngine() + require.NoError(t, err) + extraTables := append(append([]any{new(xormigrate.Migration)}, license.GetTables()...), migration.GetTables()...) + require.NoError(t, engine.Sync2(extraTables...)) + + require.NoError(t, db.LoadFixtures()) + + e := routes.NewEcho() + routes.RegisterRoutes(e) + return e +} + +// testingRequest dispatches a request to a /api/v2/test/* endpoint, sending the +// raw token in the Authorization header (not a Bearer JWT). +func testingRequest(e *echo.Echo, method, path, body, token string) *httptest.ResponseRecorder { + req := httptest.NewRequest(method, path, strings.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + if token != "" { + req.Header.Set("Authorization", token) + } + rec := httptest.NewRecorder() + e.ServeHTTP(rec, req) + return rec +} + +func countRows(t *testing.T, table string) int { + t.Helper() + s := db.NewSession() + defer s.Close() + rows := []map[string]interface{}{} + require.NoError(t, s.Table(table).Find(&rows)) + return len(rows) +} + +func TestTesting(t *testing.T) { + t.Run("replace table contents", func(t *testing.T) { + e := setupTestingEnv(t, testingToken) + t.Cleanup(func() { _ = db.LoadFixtures() }) + + body := `[{"id":1,"title":"only label","created_by_id":1,"created":"2020-01-01T00:00:00Z","updated":"2020-01-01T00:00:00Z"}]` + rec := testingRequest(e, http.MethodPut, "/api/v2/test/labels", body, testingToken) + require.Equal(t, http.StatusCreated, rec.Code, "body: %s", rec.Body.String()) + + var data []map[string]any + require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &data)) + require.Len(t, data, 1) + assert.EqualValues(t, "only label", data[0]["title"]) + assert.Equal(t, 1, countRows(t, "labels"), "table should hold exactly the seeded rows") + }) + + t.Run("replace without truncate keeps existing rows", func(t *testing.T) { + e := setupTestingEnv(t, testingToken) + t.Cleanup(func() { _ = db.LoadFixtures() }) + + before := countRows(t, "labels") + require.Positive(t, before, "fixtures should seed some labels") + + body := `[{"id":9999,"title":"added label","created_by_id":1,"created":"2020-01-01T00:00:00Z","updated":"2020-01-01T00:00:00Z"}]` + rec := testingRequest(e, http.MethodPut, "/api/v2/test/labels?truncate=false", body, testingToken) + require.Equal(t, http.StatusCreated, rec.Code, "body: %s", rec.Body.String()) + + assert.Equal(t, before+1, countRows(t, "labels"), "row should be added on top of existing data") + }) + + t.Run("truncate all tables", func(t *testing.T) { + e := setupTestingEnv(t, testingToken) + t.Cleanup(func() { _ = db.LoadFixtures() }) + + require.Positive(t, countRows(t, "labels")) + + rec := testingRequest(e, http.MethodDelete, "/api/v2/test/all", "", testingToken) + require.Equal(t, http.StatusOK, rec.Code, "body: %s", rec.Body.String()) + + var resp struct { + Message string `json:"message"` + } + require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp)) + assert.Equal(t, "ok", resp.Message) + assert.Equal(t, 0, countRows(t, "labels"), "every table should be empty after truncate") + }) + + t.Run("wrong token is forbidden", func(t *testing.T) { + e := setupTestingEnv(t, testingToken) + + rec := testingRequest(e, http.MethodPut, "/api/v2/test/labels", `[]`, "wrong-token") + assert.Equal(t, http.StatusForbidden, rec.Code, "body: %s", rec.Body.String()) + + rec = testingRequest(e, http.MethodDelete, "/api/v2/test/all", "", "wrong-token") + assert.Equal(t, http.StatusForbidden, rec.Code, "body: %s", rec.Body.String()) + }) + + t.Run("missing token is forbidden", func(t *testing.T) { + e := setupTestingEnv(t, testingToken) + + rec := testingRequest(e, http.MethodPut, "/api/v2/test/labels", `[]`, "") + assert.Equal(t, http.StatusForbidden, rec.Code, "body: %s", rec.Body.String()) + + rec = testingRequest(e, http.MethodDelete, "/api/v2/test/all", "", "") + assert.Equal(t, http.StatusForbidden, rec.Code, "body: %s", rec.Body.String()) + }) +} + +func TestTesting_DisabledConfig(t *testing.T) { + e := setupTestingEnv(t, "") + + rec := testingRequest(e, http.MethodPut, "/api/v2/test/labels", `[]`, "") + assert.Equal(t, http.StatusNotFound, rec.Code, "endpoint must be absent when no testing token is configured") + + rec = testingRequest(e, http.MethodDelete, "/api/v2/test/all", "", "") + assert.Equal(t, http.StatusNotFound, rec.Code, "endpoint must be absent when no testing token is configured") +} + +func TestTesting_BodySchemaIsArrayOfObjects(t *testing.T) { + e := setupTestingEnv(t, testingToken) + + req := httptest.NewRequest(http.MethodGet, "/api/v2/openapi.json", nil) + rec := httptest.NewRecorder() + e.ServeHTTP(rec, req) + require.Equal(t, http.StatusOK, rec.Code) + + var spec map[string]any + require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &spec)) + + paths, _ := spec["paths"].(map[string]any) + op, _ := paths["/test/{table}"].(map[string]any) + put, ok := op["put"].(map[string]any) + require.True(t, ok, "PUT /test/{table} must be in the spec") + + reqBody, _ := put["requestBody"].(map[string]any) + content, _ := reqBody["content"].(map[string]any) + appJSON, _ := content["application/json"].(map[string]any) + schema, _ := appJSON["schema"].(map[string]any) + // FieldsOptionalByDefault makes the array nullable, so `type` may be the + // string "array" or the list ["array","null"]. Either is honest; assert it + // describes an array (not, say, a base64 string as json.RawMessage would). + assert.Contains(t, schemaTypes(schema["type"]), "array", "request body must be modeled as an array") +} + +// schemaTypes normalises an OpenAPI `type` value (a string or a list of +// strings when nullable) into a slice for assertion. +func schemaTypes(v any) []string { + switch t := v.(type) { + case string: + return []string{t} + case []any: + out := make([]string, 0, len(t)) + for _, e := range t { + if s, ok := e.(string); ok { + out = append(out, s) + } + } + return out + default: + return nil + } +} From 13f1a13367a1645e3bf616ec70754635c50df769 Mon Sep 17 00:00:00 2001 From: kolaente Date: Wed, 17 Jun 2026 13:55:50 +0200 Subject: [PATCH 21/53] fix(db): interpolate table identifiers in truncate instead of binding them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MySQL/MariaDB/Postgres cannot bind a table name as a ? placeholder, so the non-SQLite branch failed with a syntax error. Interpolate the already-validated identifier with x.Quote (per-dialect quoting) instead. validateTableName restricts to registered table names, so this is injection-safe — the same trust model the SQLite branch already relies on. Latent bug surfaced by the new cross-engine testing webtest, which is the first to exercise this path on MySQL/MariaDB. --- pkg/db/dump.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/db/dump.go b/pkg/db/dump.go index 1d9f803b9..be345e173 100644 --- a/pkg/db/dump.go +++ b/pkg/db/dump.go @@ -127,7 +127,7 @@ func RestoreAndTruncate(table string, contents []map[string]interface{}) (err er return err } } else { - if _, err := x.Query("TRUNCATE TABLE ?", table); err != nil { + if _, err := x.Query("TRUNCATE TABLE " + x.Quote(table)); err != nil { return err } } @@ -148,7 +148,7 @@ func TruncateAllTables() error { return err } } else { - if _, err := x.Query("TRUNCATE TABLE ?", name); err != nil { + if _, err := x.Query("TRUNCATE TABLE " + x.Quote(name)); err != nil { return err } } From c4819631e2bff76b12a3bdbec04d837f51877025 Mon Sep 17 00:00:00 2001 From: kolaente Date: Wed, 17 Jun 2026 13:55:50 +0200 Subject: [PATCH 22/53] test(api/v2): use cross-engine datetime literals in testing webtest MariaDB strict mode rejects the RFC3339 T/Z form for DATETIME columns. The space-separated form is accepted by MariaDB, Postgres and SQLite alike; the test only asserts on title and row counts, never the datetime. --- pkg/webtests/huma_testing_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/webtests/huma_testing_test.go b/pkg/webtests/huma_testing_test.go index 4f786c8f1..480ef285f 100644 --- a/pkg/webtests/huma_testing_test.go +++ b/pkg/webtests/huma_testing_test.go @@ -104,7 +104,7 @@ func TestTesting(t *testing.T) { e := setupTestingEnv(t, testingToken) t.Cleanup(func() { _ = db.LoadFixtures() }) - body := `[{"id":1,"title":"only label","created_by_id":1,"created":"2020-01-01T00:00:00Z","updated":"2020-01-01T00:00:00Z"}]` + body := `[{"id":1,"title":"only label","created_by_id":1,"created":"2020-01-01 00:00:00","updated":"2020-01-01 00:00:00"}]` rec := testingRequest(e, http.MethodPut, "/api/v2/test/labels", body, testingToken) require.Equal(t, http.StatusCreated, rec.Code, "body: %s", rec.Body.String()) @@ -122,7 +122,7 @@ func TestTesting(t *testing.T) { before := countRows(t, "labels") require.Positive(t, before, "fixtures should seed some labels") - body := `[{"id":9999,"title":"added label","created_by_id":1,"created":"2020-01-01T00:00:00Z","updated":"2020-01-01T00:00:00Z"}]` + body := `[{"id":9999,"title":"added label","created_by_id":1,"created":"2020-01-01 00:00:00","updated":"2020-01-01 00:00:00"}]` rec := testingRequest(e, http.MethodPut, "/api/v2/test/labels?truncate=false", body, testingToken) require.Equal(t, http.StatusCreated, rec.Code, "body: %s", rec.Body.String()) From 8a255cbff656b10ab34cc9db235bf9f77ac88fc5 Mon Sep 17 00:00:00 2001 From: kolaente Date: Wed, 17 Jun 2026 19:32:39 +0200 Subject: [PATCH 23/53] fix(gantt): preserve horizontal scroll when focusing a task bar Focusing the task bar SVG `` inside the `overflow-x:auto` `.gantt-container` triggered Firefox's focus-induced scroll-into-view, which jumped the scroll container back toward `scrollLeft=0` (today). Pass `{ preventScroll: true }` to `focus()` so selecting a bar keeps the current scroll position. Chromium scrolls minimally on focus so it never manifested there. Fixes #2728 --- frontend/src/components/gantt/GanttChart.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/gantt/GanttChart.vue b/frontend/src/components/gantt/GanttChart.vue index 989bd400a..f75753428 100644 --- a/frontend/src/components/gantt/GanttChart.vue +++ b/frontend/src/components/gantt/GanttChart.vue @@ -730,7 +730,7 @@ function focusTaskBar(rowId: string) { setTimeout(() => { const taskBarElement = document.querySelector(`[data-row-id="${rowId}"] [role="slider"]`) as HTMLElement if (taskBarElement) { - taskBarElement.focus() + taskBarElement.focus({preventScroll: true}) } }, 0) } From cf456fb2232c7f7fbc0c48a412dd2888b6fb5031 Mon Sep 17 00:00:00 2001 From: kolaente Date: Wed, 17 Jun 2026 19:37:32 +0200 Subject: [PATCH 24/53] fix(kanban): count tasks in bucket, not filter total, for saved-filter bucket limits On a saved-filter (or view-filter) kanban view, checkBucketLimit counted the total number of tasks matching the filter instead of the number of tasks actually in the target bucket. Adding the first task to an empty limited bucket was therefore wrongly rejected with code 10004 "exceeded the limit", even though the bucket was at 0/limit. The same setup on a regular project bucket worked because that branch counts task_buckets rows scoped to the bucket. Scope the count to the bucket by adding `bucket_id = ` to the TaskCollection filter. ReadAll combines this with the saved-filter / view filter, so the count reflects exactly the tasks that are in this bucket and match the filter. This keeps the #355 behaviour (stale task_buckets rows whose tasks no longer match the filter are excluded) while fixing the unscoped over-count. Fixes #2672 --- pkg/models/kanban_task_bucket_test.go | 62 +++++++++++++++++++++++++++ pkg/models/tasks.go | 6 +++ 2 files changed, 68 insertions(+) diff --git a/pkg/models/kanban_task_bucket_test.go b/pkg/models/kanban_task_bucket_test.go index 6d1eb2f24..7bde2ade4 100644 --- a/pkg/models/kanban_task_bucket_test.go +++ b/pkg/models/kanban_task_bucket_test.go @@ -226,6 +226,68 @@ func TestTaskBucket_Update(t *testing.T) { }) }) + t.Run("saved filter: first task into empty limited bucket is allowed", func(t *testing.T) { + // Regression test for #2672: on a saved-filter kanban view the bucket + // limit was checked against the total number of tasks matching the + // filter instead of the number of tasks actually in the target bucket, + // so adding the first task to an empty limited bucket was wrongly + // rejected with ErrBucketLimitExceeded. + db.LoadAndAssertFixtures(t) + s := db.NewSession() + defer s.Close() + + // A saved filter matching many tasks; the filter total is well above + // the bucket limit we set below. + sf := &SavedFilter{ + Title: "limit-filter", + Filters: &TaskCollection{Filter: "done = false"}, + } + err := sf.Create(s, u) + require.NoError(t, err) + + filterProjectID := getProjectIDFromSavedFilterID(sf.ID) + + view := &ProjectView{} + exists, err := s.Where("project_id = ? AND view_kind = ?", filterProjectID, ProjectViewKindKanban).Get(view) + require.NoError(t, err) + require.True(t, exists) + + // All matching tasks are placed in the default bucket on creation; + // pick three of them to move into a fresh, empty bucket. + var defaultTasks []*TaskBucket + err = s.Where("project_view_id = ?", view.ID).Find(&defaultTasks) + require.NoError(t, err) + require.GreaterOrEqual(t, len(defaultTasks), 3, "filter must match enough tasks to exceed the bucket limit") + + limitedBucket := &Bucket{ + Title: "limited", + ProjectViewID: view.ID, + ProjectID: filterProjectID, + Limit: 2, + } + err = limitedBucket.Create(s, u) + require.NoError(t, err) + + moveTaskToBucket := func(taskID int64) error { + tb := &TaskBucket{ + TaskID: taskID, + BucketID: limitedBucket.ID, + ProjectViewID: view.ID, + ProjectID: filterProjectID, + } + return tb.Update(s, u) + } + + // Moving the FIRST task into the empty bucket must succeed (0/2 -> 1/2). + require.NoError(t, moveTaskToBucket(defaultTasks[0].TaskID)) + // The second one fills the bucket up to the limit (1/2 -> 2/2). + require.NoError(t, moveTaskToBucket(defaultTasks[1].TaskID)) + // The third one would exceed the limit and must be rejected. + err = moveTaskToBucket(defaultTasks[2].TaskID) + require.Error(t, err) + assert.True(t, IsErrBucketLimitExceeded(err)) + }) + t.Run("keep done timestamp when moving task between projects", func(t *testing.T) { db.LoadAndAssertFixtures(t) u := &user.User{ID: 1} diff --git a/pkg/models/tasks.go b/pkg/models/tasks.go index ee5eda824..eb2989694 100644 --- a/pkg/models/tasks.go +++ b/pkg/models/tasks.go @@ -830,9 +830,15 @@ func checkBucketLimit(s *xorm.Session, a web.Auth, t *Task, bucket *Bucket) (tas } if view.ProjectID < 0 || (view.Filter != nil && view.Filter.Filter != "") { + // For saved filters or views with a filter, the count must be scoped to + // this bucket *and* the filter: raw task_buckets rows can include tasks + // that no longer match the filter (#355), while the unscoped filter total + // counts tasks across all buckets, not just this one (#2672). ReadAll + // combines the bucket_id condition with the saved-filter / view filter. tc := &TaskCollection{ ProjectID: view.ProjectID, ProjectViewID: bucket.ProjectViewID, + Filter: "bucket_id = " + strconv.FormatInt(bucket.ID, 10), } _, _, taskCount, err = tc.ReadAll(s, a, "", 1, 1) From ca4e747bedf48d9a93df9efedaa2e285c7db26e4 Mon Sep 17 00:00:00 2001 From: kolaente Date: Fri, 12 Jun 2026 10:31:01 +0200 Subject: [PATCH 25/53] refactor(files): extract WriteFileDownload shared by attachment download Split the generic file-download writer (ServeContent for seekable readers, manual 304 + io.Copy otherwise) out of WriteAttachmentDownload so other blob endpoints can reuse it. The attachment writer keeps its preview branch and cache override and delegates the rest. --- pkg/web/files/file.go | 57 ++++++++++++++++++++++++++++++++ pkg/web/files/task_attachment.go | 33 +++--------------- 2 files changed, 61 insertions(+), 29 deletions(-) create mode 100644 pkg/web/files/file.go diff --git a/pkg/web/files/file.go b/pkg/web/files/file.go new file mode 100644 index 000000000..fa2b4a334 --- /dev/null +++ b/pkg/web/files/file.go @@ -0,0 +1,57 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-present Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package files + +import ( + "io" + "mime" + "net/http" + "strconv" + + "code.vikunja.io/api/pkg/files" +) + +// WriteFileDownload streams a loaded file (its .File reader must be open) to the +// response as an attachment download: http.ServeContent for seekable local files +// (Range + If-Modified-Since for free), a manual 304 + io.Copy otherwise. It does +// not close the reader; the caller owns it. +func WriteFileDownload(w http.ResponseWriter, r *http.Request, f *files.File) { + mimeToReturn := f.Mime + if mimeToReturn == "" { + mimeToReturn = "application/octet-stream" + } + w.Header().Set("Content-Disposition", mime.FormatMediaType("attachment", map[string]string{"filename": f.Name})) + w.Header().Set("Content-Type", mimeToReturn) + w.Header().Set("Content-Length", strconv.FormatUint(f.Size, 10)) + w.Header().Set("Last-Modified", f.Created.UTC().Format(http.TimeFormat)) + + // Local files are *os.File (seekable), so ServeContent gives Range + + // If-Modified-Since for free; s3 (and the in-memory test storage) return a + // non-seekable reader, so check If-Modified-Since manually and io.Copy. + if seeker, ok := f.File.(io.ReadSeeker); ok { + http.ServeContent(w, r, f.Name, f.Created, seeker) + return + } + + if ifModSince := r.Header.Get("If-Modified-Since"); ifModSince != "" { + if t, parseErr := http.ParseTime(ifModSince); parseErr == nil && !f.Created.UTC().After(t) { + w.WriteHeader(http.StatusNotModified) + return + } + } + _, _ = io.Copy(w, f.File) +} diff --git a/pkg/web/files/task_attachment.go b/pkg/web/files/task_attachment.go index 3db78e62f..09306f59b 100644 --- a/pkg/web/files/task_attachment.go +++ b/pkg/web/files/task_attachment.go @@ -21,8 +21,6 @@ package files import ( - "io" - "mime" "net/http" "strconv" @@ -62,9 +60,9 @@ func toAttachmentUploadError(err error) AttachmentUploadError { return AttachmentUploadError{Message: err.Error()} } -// WriteAttachmentDownload streams the attachment (or its preview) to the response: -// http.ServeContent for seekable local files (Range + If-Modified-Since for free), -// a manual 304 + io.Copy otherwise. It closes the file reader. +// WriteAttachmentDownload streams the attachment (or its inline image preview) to +// the response and closes the file reader. The non-preview path delegates to +// WriteFileDownload, adding the cache override that lets browsers cache attachments. func WriteAttachmentDownload(w http.ResponseWriter, r *http.Request, ta *models.TaskAttachment, preview []byte) { defer func() { _ = ta.File.File.Close() }() @@ -75,30 +73,7 @@ func WriteAttachmentDownload(w http.ResponseWriter, r *http.Request, ta *models. return } - mimeToReturn := ta.File.Mime - if mimeToReturn == "" { - mimeToReturn = "application/octet-stream" - } - w.Header().Set("Content-Disposition", mime.FormatMediaType("attachment", map[string]string{"filename": ta.File.Name})) - w.Header().Set("Content-Type", mimeToReturn) - w.Header().Set("Content-Length", strconv.FormatUint(ta.File.Size, 10)) - w.Header().Set("Last-Modified", ta.File.Created.UTC().Format(http.TimeFormat)) // Override the global no-store directive so browsers can cache attachments. w.Header().Set("Cache-Control", "no-cache") - - // Local files are *os.File (seekable), so ServeContent gives Range + - // If-Modified-Since for free; s3 (and the in-memory test storage) return a - // non-seekable reader, so check If-Modified-Since manually and io.Copy. - if seeker, ok := ta.File.File.(io.ReadSeeker); ok { - http.ServeContent(w, r, ta.File.Name, ta.File.Created, seeker) - return - } - - if ifModSince := r.Header.Get("If-Modified-Since"); ifModSince != "" { - if t, parseErr := http.ParseTime(ifModSince); parseErr == nil && !ta.File.Created.UTC().After(t) { - w.WriteHeader(http.StatusNotModified) - return - } - } - _, _ = io.Copy(w, ta.File.File) + WriteFileDownload(w, r, ta.File) } From ac5e94252b6adced64a32509ad032f06ee79c662 Mon Sep 17 00:00:00 2001 From: kolaente Date: Fri, 12 Jun 2026 10:31:06 +0200 Subject: [PATCH 26/53] feat(api/v2): add totp qr code endpoint Port GET /user/settings/totp/qrcode to v2 as an image/jpeg blob, modeled in the OpenAPI spec. Extract the qr-to-jpeg encoding into user.GetTOTPQrCodeAsJpegForUser so v1 and v2 share it; refactor v1 onto it. The handler reuses the existing local-account guard, rejecting non-local users with 412. --- pkg/routes/api/v1/user_totp.go | 13 ++------ pkg/routes/api/v2/user_totp.go | 49 +++++++++++++++++++++++++++-- pkg/user/totp.go | 17 ++++++++++ pkg/webtests/huma_user_totp_test.go | 13 ++++++-- 4 files changed, 77 insertions(+), 15 deletions(-) diff --git a/pkg/routes/api/v1/user_totp.go b/pkg/routes/api/v1/user_totp.go index e3c0ae076..a3c9fc8c4 100644 --- a/pkg/routes/api/v1/user_totp.go +++ b/pkg/routes/api/v1/user_totp.go @@ -17,10 +17,8 @@ package v1 import ( - "bytes" "errors" "fmt" - "image/jpeg" "net/http" "code.vikunja.io/api/pkg/db" @@ -202,14 +200,7 @@ func UserTOTPQrCode(c *echo.Context) error { } defer s.Close() - qrcode, err := user.GetTOTPQrCodeForUser(s, u) - if err != nil { - _ = s.Rollback() - return err - } - - buff := &bytes.Buffer{} - err = jpeg.Encode(buff, qrcode, nil) + qrcode, err := user.GetTOTPQrCodeAsJpegForUser(s, u) if err != nil { _ = s.Rollback() return err @@ -220,7 +211,7 @@ func UserTOTPQrCode(c *echo.Context) error { return err } - return c.Blob(http.StatusOK, "image/jpeg", buff.Bytes()) + return c.Blob(http.StatusOK, "image/jpeg", qrcode) } // UserTOTP returns the current totp implementation if any is enabled. diff --git a/pkg/routes/api/v2/user_totp.go b/pkg/routes/api/v2/user_totp.go index d998a524e..dd3b0c575 100644 --- a/pkg/routes/api/v2/user_totp.go +++ b/pkg/routes/api/v2/user_totp.go @@ -49,10 +49,16 @@ type totpMessageBody struct { Body models.Message } +// totpQrCodeResponse carries the qr code jpeg bytes plus a fixed Content-Type. +// Huma writes the []byte Body straight to the wire; the header field overrides +// content negotiation so image/jpeg reaches the client (matching v1). +type totpQrCodeResponse struct { + ContentType string `header:"Content-Type"` + Body []byte +} + // RegisterTOTPRoutes wires the current-user totp (2FA) operations onto the Huma // API. Totp is a local-account feature; every handler rejects OIDC/LDAP users. -// The QR-code blob endpoint is intentionally not ported here (binary streaming, -// handled in a later wave). func RegisterTOTPRoutes(api huma.API) { if !config.ServiceEnableTotp.GetBool() { return @@ -100,6 +106,27 @@ func RegisterTOTPRoutes(api huma.API) { DefaultStatus: http.StatusOK, Tags: tags, }, totpDisable) + + Register(api, huma.Operation{ + OperationID: "totp-qrcode", + Summary: "Get the totp enrollment qr code", + Description: "Returns the qr code for the authenticated user's enrolled totp setting as a jpeg image, for scanning into an authenticator app. Requires a prior enrollment. Local accounts only.", + Method: http.MethodGet, + Path: "/user/settings/totp/qrcode", + Tags: tags, + // Spell out the binary response; a bare []byte Body would otherwise be + // modeled as a base64 JSON string instead of binary image data. + Responses: map[string]*huma.Response{ + "200": { + Description: "The qr code as a jpeg image.", + Content: map[string]*huma.MediaType{ + "image/jpeg": { + Schema: &huma.Schema{Type: huma.TypeString, Format: "binary"}, + }, + }, + }, + }, + }, totpQrCode) } func init() { AddRouteRegistrar(RegisterTOTPRoutes) } @@ -208,3 +235,21 @@ func totpDisable(ctx context.Context, in *totpDisableBody) (*totpMessageBody, er } return &totpMessageBody{Body: models.Message{Message: "TOTP was disabled successfully."}}, nil } + +func totpQrCode(ctx context.Context, _ *struct{}) (*totpQrCodeResponse, error) { + u, s, err := localUserFromCtx(ctx) + if err != nil { + return nil, err + } + defer s.Close() + + qrcode, err := user.GetTOTPQrCodeAsJpegForUser(s, u) + if err != nil { + _ = s.Rollback() + return nil, translateDomainError(err) + } + if err := s.Commit(); err != nil { + return nil, translateDomainError(err) + } + return &totpQrCodeResponse{ContentType: "image/jpeg", Body: qrcode}, nil +} diff --git a/pkg/user/totp.go b/pkg/user/totp.go index e18948443..98c4327cb 100644 --- a/pkg/user/totp.go +++ b/pkg/user/totp.go @@ -17,8 +17,10 @@ package user import ( + "bytes" "fmt" "image" + "image/jpeg" "strconv" "time" @@ -198,6 +200,21 @@ func GetTOTPQrCodeForUser(s *xorm.Session, user *User) (qrcode image.Image, err return key.Image(300, 300) } +// GetTOTPQrCodeAsJpegForUser renders the user's totp qr code to jpeg bytes, the +// wire format both API versions serve. +func GetTOTPQrCodeAsJpegForUser(s *xorm.Session, user *User) ([]byte, error) { + qrcode, err := GetTOTPQrCodeForUser(s, user) + if err != nil { + return nil, err + } + + buff := &bytes.Buffer{} + if err := jpeg.Encode(buff, qrcode, nil); err != nil { + return nil, err + } + return buff.Bytes(), nil +} + // HandleFailedTOTPAuth records a failed TOTP attempt and locks the account // after 10 consecutive failures. // diff --git a/pkg/webtests/huma_user_totp_test.go b/pkg/webtests/huma_user_totp_test.go index d5cc82f15..df244a23c 100644 --- a/pkg/webtests/huma_user_totp_test.go +++ b/pkg/webtests/huma_user_totp_test.go @@ -34,8 +34,7 @@ import ( var testuser14 = user.User{ID: 14, Username: "user14", Issuer: "https://some.service.com"} // TestHumaTOTP mirrors v1's TestUserTOTPLocalUser and adds the enable/disable -// flows plus the local-account-only guard. The QR-code endpoint is not ported -// to v2 (binary streaming, later wave), so there is no test for it here. +// flows, the qr-code blob endpoint, and the local-account-only guard. // // Fixture topology (pkg/db/fixtures/totp.yml + users.yml): // - user1: totp enrolled, not enabled (secret HXDMVJEC…). @@ -59,6 +58,15 @@ func TestHumaTOTP(t *testing.T) { require.Equal(t, http.StatusPreconditionFailed, rec.Code, "body: %s", rec.Body.String()) }) + t.Run("Get qr code for enrolled user", func(t *testing.T) { + e, err := setupTestEnv() + require.NoError(t, err) + rec := humaRequest(t, e, http.MethodGet, "/api/v2/user/settings/totp/qrcode", "", humaTokenFor(t, &testuser1), "") + require.Equal(t, http.StatusOK, rec.Code, "body: %s", rec.Body.String()) + assert.Equal(t, "image/jpeg", rec.Header().Get("Content-Type")) + assert.NotEmpty(t, rec.Body.Bytes(), "the qr code jpeg must have bytes") + }) + t.Run("Enroll a fresh user", func(t *testing.T) { e, err := setupTestEnv() require.NoError(t, err) @@ -123,6 +131,7 @@ func TestHumaTOTP(t *testing.T) { method, path, body string }{ {http.MethodGet, "/api/v2/user/settings/totp", ""}, + {http.MethodGet, "/api/v2/user/settings/totp/qrcode", ""}, {http.MethodPost, "/api/v2/user/settings/totp/enroll", ""}, {http.MethodPost, "/api/v2/user/settings/totp/enable", `{"passcode":"000000"}`}, {http.MethodPost, "/api/v2/user/settings/totp/disable", `{"password":"12345678"}`}, From 8c72e83a4d62cf71b51e44bd4bdcd2f196007c55 Mon Sep 17 00:00:00 2001 From: kolaente Date: Fri, 12 Jun 2026 10:31:16 +0200 Subject: [PATCH 27/53] feat(api/v2): add user data export endpoints Port POST /user/export/request, POST /user/export/download (zip stream) and GET /user/export (status) to v2. Extract the export-file loader and status builder into pkg/models (GetUserDataExportFile, GetUserDataExportStatus) with a shared ErrUserDataExportDoesNotExist, and refactor v1 onto them. The v2 download streams via the shared WriteFileDownload writer; local users confirm with their password, external-provider users are passed through. --- pkg/models/error.go | 29 +++++ pkg/models/export.go | 53 ++++++++ pkg/routes/api/v1/user_export.go | 49 ++----- pkg/routes/api/v2/user_export.go | 179 ++++++++++++++++++++++++++ pkg/webtests/huma_user_export_test.go | 125 ++++++++++++++++++ 5 files changed, 394 insertions(+), 41 deletions(-) create mode 100644 pkg/routes/api/v2/user_export.go create mode 100644 pkg/webtests/huma_user_export_test.go diff --git a/pkg/models/error.go b/pkg/models/error.go index 0b793aecc..8f1a47553 100644 --- a/pkg/models/error.go +++ b/pkg/models/error.go @@ -2624,3 +2624,32 @@ func (err ErrTimeEntryEndBeforeStart) HTTPError() web.HTTPError { Message: "A time entry's end time cannot be before its start time.", } } + +// ================= +// User export errors +// ================= + +// ErrUserDataExportDoesNotExist represents an error where a user has no ready data export to download. +type ErrUserDataExportDoesNotExist struct{} + +// IsErrUserDataExportDoesNotExist checks if an error is ErrUserDataExportDoesNotExist. +func IsErrUserDataExportDoesNotExist(err error) bool { + _, ok := err.(ErrUserDataExportDoesNotExist) + return ok +} + +func (err ErrUserDataExportDoesNotExist) Error() string { + return "No user data export found" +} + +// ErrCodeUserDataExportDoesNotExist holds the unique world-error code of this error +const ErrCodeUserDataExportDoesNotExist = 19001 + +// HTTPError holds the http error description +func (err ErrUserDataExportDoesNotExist) HTTPError() web.HTTPError { + return web.HTTPError{ + HTTPCode: http.StatusNotFound, + Code: ErrCodeUserDataExportDoesNotExist, + Message: "No user data export found.", + } +} diff --git a/pkg/models/export.go b/pkg/models/export.go index 4772fa2d5..65d1f2fae 100644 --- a/pkg/models/export.go +++ b/pkg/models/export.go @@ -404,6 +404,59 @@ func exportProjectBackgrounds(s *xorm.Session, u *user.User, wr *zip.Writer) (er return utils.WriteFilesToZip(backgroundFiles, wr) } +// GetUserDataExportFile loads the user's ready data export with its bytes open for +// reading. It returns ErrUserDataExportDoesNotExist when the user never requested an +// export or the underlying file is gone. The caller must close the returned reader. +func GetUserDataExportFile(u *user.User) (*files.File, error) { + if u.ExportFileID == 0 { + return nil, ErrUserDataExportDoesNotExist{} + } + + exportFile := &files.File{ID: u.ExportFileID} + if err := exportFile.LoadFileMetaByID(); err != nil { + if files.IsErrFileDoesNotExist(err) { + return nil, ErrUserDataExportDoesNotExist{} + } + return nil, err + } + if err := exportFile.LoadFileByID(); err != nil { + if os.IsNotExist(err) { + return nil, ErrUserDataExportDoesNotExist{} + } + return nil, err + } + + return exportFile, nil +} + +// GetUserDataExportStatus returns metadata about the user's current data export, or +// nil when none exists. The expiry mirrors the cleanup cron's 7-day retention. +func GetUserDataExportStatus(u *user.User) (*UserExportStatus, error) { + if u.ExportFileID == 0 { + return nil, nil + } + + exportFile := &files.File{ID: u.ExportFileID} + if err := exportFile.LoadFileMetaByID(); err != nil { + return nil, err + } + + return &UserExportStatus{ + ID: exportFile.ID, + Size: exportFile.Size, + Created: exportFile.Created, + Expires: exportFile.Created.Add(7 * 24 * time.Hour), + }, nil +} + +// UserExportStatus is the metadata returned for a user's current data export. +type UserExportStatus struct { + ID int64 `json:"id" readOnly:"true" doc:"The id of the export file."` + Size uint64 `json:"size" readOnly:"true" doc:"The size of the export file in bytes."` + Created time.Time `json:"created" readOnly:"true" doc:"When the export was created."` + Expires time.Time `json:"expires" readOnly:"true" doc:"When the export will be automatically deleted (7 days after creation)."` +} + func RegisterOldExportCleanupCron() { const logPrefix = "[User Export Cleanup Cron] " diff --git a/pkg/routes/api/v1/user_export.go b/pkg/routes/api/v1/user_export.go index 6efc311c0..405f98a29 100644 --- a/pkg/routes/api/v1/user_export.go +++ b/pkg/routes/api/v1/user_export.go @@ -19,14 +19,11 @@ package v1 import ( "io" "net/http" - "os" "strconv" - "time" "code.vikunja.io/api/pkg/config" "code.vikunja.io/api/pkg/db" "code.vikunja.io/api/pkg/events" - "code.vikunja.io/api/pkg/files" "code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/user" "github.com/labstack/echo/v5" @@ -127,25 +124,10 @@ func DownloadUserDataExport(c *echo.Context) error { return err } - // Check if user has an export file - exportNotFoundError := echo.NewHTTPError(http.StatusNotFound, "No user data export found.") - if u.ExportFileID == 0 { - return exportNotFoundError - } - - // Download - exportFile := &files.File{ID: u.ExportFileID} - err = exportFile.LoadFileMetaByID() + exportFile, err := models.GetUserDataExportFile(u) if err != nil { - if files.IsErrFileDoesNotExist(err) { - return exportNotFoundError - } - return err - } - err = exportFile.LoadFileByID() - if err != nil { - if os.IsNotExist(err) { - return exportNotFoundError + if models.IsErrUserDataExportDoesNotExist(err) { + return echo.NewHTTPError(http.StatusNotFound, "No user data export found.") } return err } @@ -163,19 +145,12 @@ func DownloadUserDataExport(c *echo.Context) error { return nil } -type UserExportStatus struct { - ID int64 `json:"id"` - Size uint64 `json:"size"` - Created time.Time `json:"created"` - Expires time.Time `json:"expires"` -} - // GetUserExportStatus returns metadata about the current user export if it exists // @Summary Get current user data export // @tags user // @Produce json // @Security JWTKeyAuth -// @Success 200 {object} v1.UserExportStatus +// @Success 200 {object} models.UserExportStatus // @Router /user/export [get] func GetUserExportStatus(c *echo.Context) error { s := db.NewSession() @@ -186,20 +161,12 @@ func GetUserExportStatus(c *echo.Context) error { return err } - if u.ExportFileID == 0 { - return c.JSON(http.StatusOK, struct{}{}) - } - - exportFile := &files.File{ID: u.ExportFileID} - if err := exportFile.LoadFileMetaByID(); err != nil { + status, err := models.GetUserDataExportStatus(u) + if err != nil { return err } - - status := UserExportStatus{ - ID: exportFile.ID, - Size: exportFile.Size, - Created: exportFile.Created, - Expires: exportFile.Created.Add(7 * 24 * time.Hour), + if status == nil { + return c.JSON(http.StatusOK, struct{}{}) } return c.JSON(http.StatusOK, status) diff --git a/pkg/routes/api/v2/user_export.go b/pkg/routes/api/v2/user_export.go new file mode 100644 index 000000000..820e883d0 --- /dev/null +++ b/pkg/routes/api/v2/user_export.go @@ -0,0 +1,179 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-present Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package apiv2 + +import ( + "context" + "net/http" + + "code.vikunja.io/api/pkg/db" + "code.vikunja.io/api/pkg/events" + "code.vikunja.io/api/pkg/models" + "code.vikunja.io/api/pkg/modules/humaecho5" + "code.vikunja.io/api/pkg/user" + webfiles "code.vikunja.io/api/pkg/web/files" + + "github.com/danielgtaylor/huma/v2" + "xorm.io/xorm" +) + +type userExportPasswordBody struct { + Body struct { + Password string `json:"password" doc:"The authenticated user's password. Required for local users; ignored for users authenticated via an external provider."` + } +} + +type userExportStatusBody struct { + Body *models.UserExportStatus +} + +func RegisterUserExportRoutes(api huma.API) { + tags := []string{"user"} + + Register(api, huma.Operation{ + OperationID: "user-export-request", + Summary: "Request a data export", + Description: "Starts building a full export of the authenticated user's data. Local users must confirm with their password. The export runs in the background; an email is sent when it is ready to download.", + Method: http.MethodPost, + Path: "/user/export/request", + Tags: tags, + DefaultStatus: http.StatusOK, + }, userExportRequest) + + Register(api, huma.Operation{ + OperationID: "user-export-download", + Summary: "Download the data export", + Description: "Streams the authenticated user's prepared data export as a zip file. Local users must confirm with their password. Fails with 404 if no export has been prepared. A POST (not GET) because the password is sent in the body.", + Method: http.MethodPost, + Path: "/user/export/download", + Tags: tags, + // Spell out the binary response; the default would be modeled as JSON. + Responses: map[string]*huma.Response{ + "200": { + Description: "The data export as a zip file.", + Content: map[string]*huma.MediaType{ + "application/zip": { + Schema: &huma.Schema{Type: huma.TypeString, Format: "binary"}, + }, + }, + }, + }, + }, userExportDownload) + + Register(api, huma.Operation{ + OperationID: "user-export-status", + Summary: "Get the current data export", + Description: "Returns metadata about the authenticated user's current data export (id, size, creation and expiry time), or null if none has been prepared.", + Method: http.MethodGet, + Path: "/user/export", + Tags: tags, + }, userExportStatus) +} + +func init() { AddRouteRegistrar(RegisterUserExportRoutes) } + +// confirmExportPassword resolves the full DB user and, for local accounts, verifies +// the supplied password — mirroring v1's checkExportRequest. External-provider users +// cannot supply a password and are passed through, as in v1. +func confirmExportPassword(ctx context.Context, s *xorm.Session, password string) (*user.User, error) { + u, err := authUserFromCtx(ctx, s) + if err != nil { + return nil, err + } + if u.IsLocalUser() { + if err := user.CheckUserPassword(u, password); err != nil { + return nil, translateDomainError(err) + } + } + return u, nil +} + +func userExportRequest(ctx context.Context, in *userExportPasswordBody) (*messageBody, error) { + s := db.NewSession() + defer s.Close() + + u, err := confirmExportPassword(ctx, s, in.Body.Password) + if err != nil { + _ = s.Rollback() + return nil, err + } + + events.DispatchOnCommit(s, &models.UserDataExportRequestedEvent{User: u}) + + if err := s.Commit(); err != nil { + _ = s.Rollback() + events.CleanupPending(s) + return nil, translateDomainError(err) + } + events.DispatchPending(ctx, s) + + out := &messageBody{} + out.Body.Message = "Successfully requested data export. We will send you an email when it's ready." + return out, nil +} + +func userExportDownload(ctx context.Context, in *userExportPasswordBody) (*huma.StreamResponse, error) { + s := db.NewSession() + defer s.Close() + + u, err := confirmExportPassword(ctx, s, in.Body.Password) + if err != nil { + _ = s.Rollback() + return nil, err + } + + exportFile, err := models.GetUserDataExportFile(u) + if err != nil { + _ = s.Rollback() + return nil, translateDomainError(err) + } + + // The file reader comes from object storage, not the DB session, so it stays + // valid after the commit; the StreamResponse callback runs after this returns. + if err := s.Commit(); err != nil { + _ = s.Rollback() + return nil, translateDomainError(err) + } + + return &huma.StreamResponse{Body: func(hctx huma.Context) { + defer func() { _ = exportFile.File.Close() }() + c := humaecho5.Unwrap(hctx) + webfiles.WriteFileDownload((*c).Response(), (*c).Request(), exportFile) + }}, nil +} + +func userExportStatus(ctx context.Context, _ *struct{}) (*userExportStatusBody, error) { + s := db.NewSession() + defer s.Close() + + u, err := authUserFromCtx(ctx, s) + if err != nil { + _ = s.Rollback() + return nil, err + } + + status, err := models.GetUserDataExportStatus(u) + if err != nil { + _ = s.Rollback() + return nil, translateDomainError(err) + } + if err := s.Commit(); err != nil { + return nil, translateDomainError(err) + } + + return &userExportStatusBody{Body: status}, nil +} diff --git a/pkg/webtests/huma_user_export_test.go b/pkg/webtests/huma_user_export_test.go new file mode 100644 index 000000000..4140f8aa6 --- /dev/null +++ b/pkg/webtests/huma_user_export_test.go @@ -0,0 +1,125 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-present Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package webtests + +import ( + "bytes" + "net/http" + "testing" + + "code.vikunja.io/api/pkg/files" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestHumaUserExport covers the v2 data-export endpoints. Fixture topology +// (pkg/db/fixtures/users.yml + files.yml): +// - user1: local, password 12345678, export_file_id 1 (file row exists, no bytes). +// - user14: non-local (OIDC), no password to confirm. +// - user15: local, no export. +func TestHumaUserExport(t *testing.T) { + t.Run("Request with the correct password", func(t *testing.T) { + e, err := setupTestEnv() + require.NoError(t, err) + rec := humaRequest(t, e, http.MethodPost, "/api/v2/user/export/request", + `{"password":"12345678"}`, humaTokenFor(t, &testuser1), "") + require.Equal(t, http.StatusOK, rec.Code, "body: %s", rec.Body.String()) + assert.Contains(t, rec.Body.String(), "requested data export") + }) + + t.Run("Request with a wrong password is refused", func(t *testing.T) { + e, err := setupTestEnv() + require.NoError(t, err) + rec := humaRequest(t, e, http.MethodPost, "/api/v2/user/export/request", + `{"password":"wrong-password"}`, humaTokenFor(t, &testuser1), "") + require.NotEqual(t, http.StatusOK, rec.Code, + "a wrong password must not start an export; body: %s", rec.Body.String()) + }) + + t.Run("Request as a non-local user skips the password", func(t *testing.T) { + e, err := setupTestEnv() + require.NoError(t, err) + rec := humaRequest(t, e, http.MethodPost, "/api/v2/user/export/request", + `{}`, humaTokenFor(t, &testuser14), "") + require.Equal(t, http.StatusOK, rec.Code, "body: %s", rec.Body.String()) + }) + + t.Run("Download streams the export bytes", func(t *testing.T) { + e, err := setupTestEnv() + require.NoError(t, err) + // user1's export points at file 1; setupTestEnv resets storage, so write + // real bytes for it (size matches the fixture's declared 100 bytes). + content := bytes.Repeat([]byte("v"), 100) + require.NoError(t, (&files.File{ID: 1, Size: uint64(len(content))}).Save(bytes.NewReader(content))) + + rec := humaRequest(t, e, http.MethodPost, "/api/v2/user/export/download", + `{"password":"12345678"}`, humaTokenFor(t, &testuser1), "") + require.Equal(t, http.StatusOK, rec.Code, "body: %s", rec.Body.String()) + assert.Equal(t, content, rec.Body.Bytes(), "the streamed export bytes must match") + assert.Contains(t, rec.Header().Get("Content-Disposition"), "test") + }) + + t.Run("Download with a wrong password is refused", func(t *testing.T) { + e, err := setupTestEnv() + require.NoError(t, err) + rec := humaRequest(t, e, http.MethodPost, "/api/v2/user/export/download", + `{"password":"wrong-password"}`, humaTokenFor(t, &testuser1), "") + require.NotEqual(t, http.StatusOK, rec.Code, "body: %s", rec.Body.String()) + }) + + t.Run("Download without an export returns 404", func(t *testing.T) { + e, err := setupTestEnv() + require.NoError(t, err) + rec := humaRequest(t, e, http.MethodPost, "/api/v2/user/export/download", + `{"password":"12345678"}`, humaTokenFor(t, &testuser15), "") + assert.Equal(t, http.StatusNotFound, rec.Code, "body: %s", rec.Body.String()) + }) + + t.Run("Download with a missing physical file returns 404", func(t *testing.T) { + // user1 has export_file_id 1, but setupTestEnv leaves its bytes unwritten. + e, err := setupTestEnv() + require.NoError(t, err) + rec := humaRequest(t, e, http.MethodPost, "/api/v2/user/export/download", + `{"password":"12345678"}`, humaTokenFor(t, &testuser1), "") + assert.Equal(t, http.StatusNotFound, rec.Code, "body: %s", rec.Body.String()) + }) + + t.Run("Status returns the export metadata", func(t *testing.T) { + e, err := setupTestEnv() + require.NoError(t, err) + rec := humaRequest(t, e, http.MethodGet, "/api/v2/user/export", "", humaTokenFor(t, &testuser1), "") + require.Equal(t, http.StatusOK, rec.Code, "body: %s", rec.Body.String()) + assert.Contains(t, rec.Body.String(), `"id":1`) + assert.Contains(t, rec.Body.String(), `"expires"`) + }) + + t.Run("Status without an export returns null", func(t *testing.T) { + e, err := setupTestEnv() + require.NoError(t, err) + rec := humaRequest(t, e, http.MethodGet, "/api/v2/user/export", "", humaTokenFor(t, &testuser15), "") + require.Equal(t, http.StatusOK, rec.Code, "body: %s", rec.Body.String()) + assert.JSONEq(t, "null", rec.Body.String()) + }) + + t.Run("Unauthenticated request is rejected", func(t *testing.T) { + e, err := setupTestEnv() + require.NoError(t, err) + rec := humaRequest(t, e, http.MethodGet, "/api/v2/user/export", "", "", "") + assert.Equal(t, http.StatusUnauthorized, rec.Code, "body: %s", rec.Body.String()) + }) +} From ee8dbf82ba24ac22256f14167594ca32b62a2db5 Mon Sep 17 00:00:00 2001 From: kolaente Date: Fri, 12 Jun 2026 11:08:53 +0200 Subject: [PATCH 28/53] fix(api/v2): close export reader when commit fails before streaming If s.Commit() fails after loading the export file, the StreamResponse callback that would close the reader never runs, leaking the open object-storage/file handle. Close it explicitly on that error path. --- pkg/routes/api/v2/user_export.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/routes/api/v2/user_export.go b/pkg/routes/api/v2/user_export.go index 820e883d0..952f8127b 100644 --- a/pkg/routes/api/v2/user_export.go +++ b/pkg/routes/api/v2/user_export.go @@ -146,6 +146,8 @@ func userExportDownload(ctx context.Context, in *userExportPasswordBody) (*huma. // valid after the commit; the StreamResponse callback runs after this returns. if err := s.Commit(); err != nil { _ = s.Rollback() + // The stream callback (which closes the reader) won't run on this error path. + _ = exportFile.File.Close() return nil, translateDomainError(err) } From 4b92f2332962216ec80484987a5f36b46a8eb712 Mon Sep 17 00:00:00 2001 From: kolaente Date: Wed, 17 Jun 2026 14:24:41 +0200 Subject: [PATCH 29/53] fix(files): never cache file downloads in v1 or v2 Move the Cache-Control: no-cache header into the shared WriteFileDownload so every export and attachment download carries it, and add it to the standalone v1 export download writer too. Downloads must never be cached. --- pkg/routes/api/v1/user_export.go | 4 ++++ pkg/web/files/file.go | 4 ++++ pkg/web/files/task_attachment.go | 6 +++--- pkg/webtests/huma_user_export_test.go | 1 + 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/pkg/routes/api/v1/user_export.go b/pkg/routes/api/v1/user_export.go index 405f98a29..1c2fd4117 100644 --- a/pkg/routes/api/v1/user_export.go +++ b/pkg/routes/api/v1/user_export.go @@ -132,6 +132,10 @@ func DownloadUserDataExport(c *echo.Context) error { return err } + // Downloads must never be cached; no-cache overrides the global no-store + // directive while still allowing revalidation. + c.Response().Header().Set("Cache-Control", "no-cache") + if config.FilesType.GetString() == "s3" { c.Response().Header().Set("Content-Disposition", "attachment; filename=\""+exportFile.Name+"\"") c.Response().Header().Set("Content-Type", exportFile.Mime) diff --git a/pkg/web/files/file.go b/pkg/web/files/file.go index fa2b4a334..461fe2780 100644 --- a/pkg/web/files/file.go +++ b/pkg/web/files/file.go @@ -30,6 +30,10 @@ import ( // (Range + If-Modified-Since for free), a manual 304 + io.Copy otherwise. It does // not close the reader; the caller owns it. func WriteFileDownload(w http.ResponseWriter, r *http.Request, f *files.File) { + // Downloads must never be cached. no-cache overrides the global no-store + // directive so revalidation (If-Modified-Since) still works. + w.Header().Set("Cache-Control", "no-cache") + mimeToReturn := f.Mime if mimeToReturn == "" { mimeToReturn = "application/octet-stream" diff --git a/pkg/web/files/task_attachment.go b/pkg/web/files/task_attachment.go index 09306f59b..55945fe23 100644 --- a/pkg/web/files/task_attachment.go +++ b/pkg/web/files/task_attachment.go @@ -62,18 +62,18 @@ func toAttachmentUploadError(err error) AttachmentUploadError { // WriteAttachmentDownload streams the attachment (or its inline image preview) to // the response and closes the file reader. The non-preview path delegates to -// WriteFileDownload, adding the cache override that lets browsers cache attachments. +// WriteFileDownload, which sets Cache-Control: no-cache; the preview branch returns +// early, so it sets the same header itself. func WriteAttachmentDownload(w http.ResponseWriter, r *http.Request, ta *models.TaskAttachment, preview []byte) { defer func() { _ = ta.File.File.Close() }() if preview != nil { + w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Content-Type", "image/png") w.Header().Set("Content-Length", strconv.Itoa(len(preview))) _, _ = w.Write(preview) return } - // Override the global no-store directive so browsers can cache attachments. - w.Header().Set("Cache-Control", "no-cache") WriteFileDownload(w, r, ta.File) } diff --git a/pkg/webtests/huma_user_export_test.go b/pkg/webtests/huma_user_export_test.go index 4140f8aa6..ee9104d5b 100644 --- a/pkg/webtests/huma_user_export_test.go +++ b/pkg/webtests/huma_user_export_test.go @@ -72,6 +72,7 @@ func TestHumaUserExport(t *testing.T) { require.Equal(t, http.StatusOK, rec.Code, "body: %s", rec.Body.String()) assert.Equal(t, content, rec.Body.Bytes(), "the streamed export bytes must match") assert.Contains(t, rec.Header().Get("Content-Disposition"), "test") + assert.Equal(t, "no-cache", rec.Header().Get("Cache-Control"), "downloads must never be cached") }) t.Run("Download with a wrong password is refused", func(t *testing.T) { From 02e7a134cccd3a7e12b2ebf2965de8bd406f4722 Mon Sep 17 00:00:00 2001 From: kolaente Date: Wed, 17 Jun 2026 14:25:03 +0200 Subject: [PATCH 30/53] fix(api): close the user data export reader after download DownloadUserDataExport obtained an open file reader from GetUserDataExportFile but never closed it on either the s3 io.Copy or the http.ServeContent branch, leaking a file descriptor on every download. Defer the close right after the file is obtained so both branches and the error paths cover it. --- pkg/routes/api/v1/user_export.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/routes/api/v1/user_export.go b/pkg/routes/api/v1/user_export.go index 1c2fd4117..b01b1fdf3 100644 --- a/pkg/routes/api/v1/user_export.go +++ b/pkg/routes/api/v1/user_export.go @@ -131,6 +131,7 @@ func DownloadUserDataExport(c *echo.Context) error { } return err } + defer func() { _ = exportFile.File.Close() }() // Downloads must never be cached; no-cache overrides the global no-store // directive while still allowing revalidation. From 55ca06ca3d34e23c1033b1019b473e1ab88bc857 Mon Sep 17 00:00:00 2001 From: kolaente Date: Wed, 17 Jun 2026 14:25:40 +0200 Subject: [PATCH 31/53] fix(export): treat a missing export meta row as no export in the status GetUserDataExportStatus propagated the raw LoadFileMetaByID error when the meta row was gone, so /user/export could 500. The download path already maps that case to ErrUserDataExportDoesNotExist (404); make status consistent by returning nil (no export), matching the documented contract. --- pkg/models/export.go | 5 ++++ pkg/models/export_test.go | 53 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 pkg/models/export_test.go diff --git a/pkg/models/export.go b/pkg/models/export.go index 65d1f2fae..2d9b57651 100644 --- a/pkg/models/export.go +++ b/pkg/models/export.go @@ -438,6 +438,11 @@ func GetUserDataExportStatus(u *user.User) (*UserExportStatus, error) { exportFile := &files.File{ID: u.ExportFileID} if err := exportFile.LoadFileMetaByID(); err != nil { + // A missing meta row means there is no export — mirror the download path + // (404 there) instead of surfacing a 500. + if files.IsErrFileDoesNotExist(err) { + return nil, nil + } return nil, err } diff --git a/pkg/models/export_test.go b/pkg/models/export_test.go new file mode 100644 index 000000000..f2d0fa2fb --- /dev/null +++ b/pkg/models/export_test.go @@ -0,0 +1,53 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-present Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package models + +import ( + "testing" + + "code.vikunja.io/api/pkg/db" + "code.vikunja.io/api/pkg/user" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetUserDataExportStatus(t *testing.T) { + t.Run("no export", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + status, err := GetUserDataExportStatus(&user.User{ID: 15}) + require.NoError(t, err) + assert.Nil(t, status) + }) + + t.Run("with export", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + status, err := GetUserDataExportStatus(&user.User{ID: 1, ExportFileID: 1}) + require.NoError(t, err) + require.NotNil(t, status) + assert.Equal(t, int64(1), status.ID) + }) + + t.Run("export points at a missing file", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + // A dangling ExportFileID must read as "no export" rather than erroring, + // matching the download path which 404s the same case. + status, err := GetUserDataExportStatus(&user.User{ID: 15, ExportFileID: 9999}) + require.NoError(t, err) + assert.Nil(t, status) + }) +} From 434b5d9fe3366c1322de5242841dba4cd0ef0e17 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 17 Jun 2026 18:43:06 +0000 Subject: [PATCH 32/53] chore(deps): update dev-dependencies to v10.5.0 --- frontend/package.json | 2 +- frontend/pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index d2ce35838..fdafc836a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -105,7 +105,7 @@ "zhyswan-vuedraggable": "4.1.3" }, "devDependencies": { - "@faker-js/faker": "10.4.0", + "@faker-js/faker": "10.5.0", "@histoire/plugin-screenshot": "1.0.0-beta.1", "@histoire/plugin-vue": "1.0.0-beta.1", "@playwright/test": "1.58.2", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 5d8174584..8fa9e43e6 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -182,8 +182,8 @@ importers: version: 4.1.3(vue@3.5.27(typescript@5.9.3)) devDependencies: '@faker-js/faker': - specifier: 10.4.0 - version: 10.4.0 + specifier: 10.5.0 + version: 10.5.0 '@histoire/plugin-screenshot': specifier: 1.0.0-beta.1 version: 1.0.0-beta.1(histoire@1.0.0-beta.1(@types/node@24.13.2)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.31.6)(vite@7.3.5(@types/node@24.13.2)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.31.6)(yaml@2.8.3))(yaml@2.8.3))(typescript@5.9.3) @@ -1542,8 +1542,8 @@ packages: '@exodus/crypto': optional: true - '@faker-js/faker@10.4.0': - resolution: {integrity: sha512-sDBWI3yLy8EcDzgobvJTWq1MJYzAkQdpjXuPukga9wXonhpMRvd1Izuo2Qgwey2OiEoRIBr35RMU9HJRoOHzpw==} + '@faker-js/faker@10.5.0': + resolution: {integrity: sha512-bsxD8WLS5lIj7aaoCx1YJkktqYj5vlBUE6HWzu2Q51ksrGJ0H737ECCKlFU7Yf8Br45z9t99frBp/J7kzbMPAg==} engines: {node: ^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0, npm: '>=10'} '@floating-ui/core@1.7.3': @@ -8176,7 +8176,7 @@ snapshots: '@exodus/bytes@1.8.0': {} - '@faker-js/faker@10.4.0': {} + '@faker-js/faker@10.5.0': {} '@floating-ui/core@1.7.3': dependencies: From 59a5a2c1e761fa4b44186d1fbacf59af5a03ff17 Mon Sep 17 00:00:00 2001 From: "Frederick [Bot]" Date: Wed, 17 Jun 2026 19:43:01 +0000 Subject: [PATCH 33/53] [skip ci] Updated swagger docs --- pkg/swagger/docs.go | 36 ++++++++++++++++++------------------ pkg/swagger/swagger.json | 36 ++++++++++++++++++------------------ pkg/swagger/swagger.yaml | 24 ++++++++++++------------ 3 files changed, 48 insertions(+), 48 deletions(-) diff --git a/pkg/swagger/docs.go b/pkg/swagger/docs.go index 8f6f0e673..e976b4d74 100644 --- a/pkg/swagger/docs.go +++ b/pkg/swagger/docs.go @@ -7342,7 +7342,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/v1.UserExportStatus" + "$ref": "#/definitions/models.UserExportStatus" } } } @@ -10579,6 +10579,23 @@ const docTemplate = `{ } } }, + "models.UserExportStatus": { + "type": "object", + "properties": { + "created": { + "type": "string" + }, + "expires": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "size": { + "type": "integer" + } + } + }, "models.UserGeneralSettings": { "type": "object", "properties": { @@ -11141,23 +11158,6 @@ const docTemplate = `{ } } }, - "v1.UserExportStatus": { - "type": "object", - "properties": { - "created": { - "type": "string" - }, - "expires": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "size": { - "type": "integer" - } - } - }, "v1.UserPassword": { "type": "object", "properties": { diff --git a/pkg/swagger/swagger.json b/pkg/swagger/swagger.json index 4cab9fb5e..98f528565 100644 --- a/pkg/swagger/swagger.json +++ b/pkg/swagger/swagger.json @@ -7334,7 +7334,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/v1.UserExportStatus" + "$ref": "#/definitions/models.UserExportStatus" } } } @@ -10571,6 +10571,23 @@ } } }, + "models.UserExportStatus": { + "type": "object", + "properties": { + "created": { + "type": "string" + }, + "expires": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "size": { + "type": "integer" + } + } + }, "models.UserGeneralSettings": { "type": "object", "properties": { @@ -11133,23 +11150,6 @@ } } }, - "v1.UserExportStatus": { - "type": "object", - "properties": { - "created": { - "type": "string" - }, - "expires": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "size": { - "type": "integer" - } - } - }, "v1.UserPassword": { "type": "object", "properties": { diff --git a/pkg/swagger/swagger.yaml b/pkg/swagger/swagger.yaml index af2b209dd..a926e18b8 100644 --- a/pkg/swagger/swagger.yaml +++ b/pkg/swagger/swagger.yaml @@ -1292,6 +1292,17 @@ definitions: this value. type: string type: object + models.UserExportStatus: + properties: + created: + type: string + expires: + type: string + id: + type: integer + size: + type: integer + type: object models.UserGeneralSettings: properties: default_project_id: @@ -1708,17 +1719,6 @@ definitions: token: type: string type: object - v1.UserExportStatus: - properties: - created: - type: string - expires: - type: string - id: - type: integer - size: - type: integer - type: object v1.UserPassword: properties: new_password: @@ -6860,7 +6860,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/v1.UserExportStatus' + $ref: '#/definitions/models.UserExportStatus' security: - JWTKeyAuth: [] summary: Get current user data export From 78f79accb5cfacb70d596adf9ee115144977db08 Mon Sep 17 00:00:00 2001 From: kolaente Date: Fri, 12 Jun 2026 10:28:21 +0200 Subject: [PATCH 34/53] refactor(auth): extract transport-agnostic login, logout and OIDC cores Pull the credential/TOTP check, session deletion, user-token issuance and OIDC callback flow out of the v1 echo handlers and into reusable helpers so both /api/v1 and the upcoming /api/v2 share one implementation: - auth.IssueUserToken + auth.WriteUserAuthCookies split the token/cookie machinery from the echo response; NewUserAuthTokenResponse now wraps them. - auth.SessionIDFromContext reads the sid claim for logout. - shared.AuthenticateUserCredentials, shared.DeleteSession hold the login and logout cores. - openid.AuthenticateCallback holds the OIDC exchange/getOrCreate/TOTP/team sync, returning the user; HandleCallback issues the token as before. v1 behaviour is unchanged on the wire. --- pkg/modules/auth/auth.go | 77 ++++++++++++++---- pkg/modules/auth/openid/openid.go | 81 +++++++++++-------- pkg/routes/api/shared/auth.go | 125 ++++++++++++++++++++++++++++++ pkg/routes/api/v1/login.go | 97 +---------------------- 4 files changed, 241 insertions(+), 139 deletions(-) diff --git a/pkg/modules/auth/auth.go b/pkg/modules/auth/auth.go index 97429aa13..f94537158 100644 --- a/pkg/modules/auth/auth.go +++ b/pkg/modules/auth/auth.go @@ -100,46 +100,75 @@ func ClearRefreshTokenCookie(c *echo.Context) { SetRefreshTokenCookie(c, "", -1) } -// NewUserAuthTokenResponse creates a new user auth token response from a user object. -func NewUserAuthTokenResponse(u *user.User, c *echo.Context, long bool) error { +// IssuedUserToken bundles a freshly minted access token with the matching +// refresh token and the cookie max-age both v1 and v2 use to set the +// HttpOnly refresh cookie. +type IssuedUserToken struct { + AccessToken string + RefreshToken string + CookieMaxAge int +} + +// IssueUserToken creates a session for the user and mints a JWT access token plus +// a refresh token for it. It is the transport-agnostic core both v1 (which writes +// the echo response) and v2 (Huma) call; callers set the refresh cookie and the +// Cache-Control header themselves via WriteUserAuthCookies. +func IssueUserToken(ctx context.Context, u *user.User, deviceInfo, ipAddress string, long bool) (*IssuedUserToken, error) { s := db.NewSession() defer s.Close() - deviceInfo := c.Request().UserAgent() - ipAddress := c.RealIP() - session, err := models.CreateSession(s, u.ID, deviceInfo, ipAddress, long) if err != nil { _ = s.Rollback() - return err + return nil, err } t, err := NewUserJWTAuthtoken(u, session.ID) if err != nil { _ = s.Rollback() - return err + return nil, err } if err := s.Commit(); err != nil { _ = s.Rollback() - return err + return nil, err } - if err := events.DispatchWithContext(c.Request().Context(), &user.LoginSucceededEvent{User: u}); err != nil { + if err := events.DispatchWithContext(ctx, &user.LoginSucceededEvent{User: u}); err != nil { log.Errorf("Could not dispatch login succeeded event: %s", err) } - // Set the refresh token as an HttpOnly cookie. The cookie is path-scoped - // to the refresh endpoint, so the browser only sends it there. JavaScript - // never sees the refresh token — this protects it from XSS. cookieMaxAge := int(config.ServiceJWTTTL.GetInt64()) if long { cookieMaxAge = int(config.ServiceJWTTTLLong.GetInt64()) } - SetRefreshTokenCookie(c, session.RefreshToken, cookieMaxAge) + return &IssuedUserToken{ + AccessToken: t, + RefreshToken: session.RefreshToken, + CookieMaxAge: cookieMaxAge, + }, nil +} + +// WriteUserAuthCookies sets the HttpOnly refresh-token cookie and the +// Cache-Control: no-store header on a response. The cookie is path-scoped to the +// refresh endpoint, so the browser only sends it there; JavaScript never sees the +// refresh token, which protects it from XSS. Shared by the v1 echo handlers and +// the v2 Huma handlers (which reach the echo context via humaecho5.Unwrap). +func WriteUserAuthCookies(c *echo.Context, token *IssuedUserToken) { + SetRefreshTokenCookie(c, token.RefreshToken, token.CookieMaxAge) c.Response().Header().Set("Cache-Control", "no-store") - return c.JSON(http.StatusOK, Token{Token: t}) +} + +// NewUserAuthTokenResponse creates a new user auth token response from a user object. +func NewUserAuthTokenResponse(u *user.User, c *echo.Context, long bool) error { + token, err := IssueUserToken(c.Request().Context(), u, c.Request().UserAgent(), c.RealIP(), long) + if err != nil { + return err + } + + WriteUserAuthCookies(c, token) + return c.JSON(http.StatusOK, Token{Token: token.AccessToken}) } // NewUserJWTAuthtoken generates and signs a new short-lived jwt token for a user. @@ -392,6 +421,26 @@ func RefreshSession(rawRefreshToken string) (*RefreshResult, error) { }, nil } +// SessionIDFromContext reads the session id (the `sid` claim) off the user JWT +// in the echo context. It returns "" when there is no user JWT or no sid claim +// (API tokens and link shares carry no session), which callers treat as a no-op. +func SessionIDFromContext(c *echo.Context) string { + raw := c.Get("user") + if raw == nil { + return "" + } + jwtinf, ok := raw.(*jwt.Token) + if !ok { + return "" + } + claims, ok := jwtinf.Claims.(jwt.MapClaims) + if !ok { + return "" + } + sid, _ := claims["sid"].(string) + return sid +} + // GetAuthFromContext retrieves the authenticated web.Auth from a plain // context.Context, bridging Huma handlers to Vikunja's echo JWT flow. The // humaecho5 adapter stashes the *echo.Context under EchoContextKey first. diff --git a/pkg/modules/auth/openid/openid.go b/pkg/modules/auth/openid/openid.go index b1fa3961a..47ade7dc9 100644 --- a/pkg/modules/auth/openid/openid.go +++ b/pkg/modules/auth/openid/openid.go @@ -168,8 +168,12 @@ func enforceTOTPIfRequired(s *xorm.Session, u *user.User, totpPasscode string) e // @Failure 500 {object} models.Message "Internal error" // @Router /auth/openid/{provider}/callback [post] func HandleCallback(c *echo.Context) error { + cb := &Callback{} + if err := c.Bind(cb); err != nil { + return &models.ErrOpenIDBadRequest{Message: "Bad data"} + } - provider, cb, oauthToken, idToken, err := getProviderAndOidcTokens(c) + u, err := AuthenticateCallback(c.Request().Context(), cb, c.Param("provider")) if err != nil { var detailedErr *models.ErrOpenIDBadRequestWithDetails if errors.As(err, &detailedErr) { @@ -181,9 +185,29 @@ func HandleCallback(c *echo.Context) error { return err } - cl, err := getClaims(provider, oauthToken, idToken) + // Create token + return auth.NewUserAuthTokenResponse(u, c, false) +} + +// AuthenticateCallback resolves an OpenID Connect callback to an authenticated +// user: it exchanges the auth code, verifies the ID token, creates or updates the +// matching local user, enforces the account-status and TOTP gates, and syncs the +// user's external teams. It is the transport-agnostic core shared by the v1 echo +// handler and the v2 Huma handler; the caller issues the auth token. The +// ErrOpenIDBadRequestWithDetails error keeps its provider detail so v1 can render +// its bespoke body and v2 can map it to RFC 9457. +func AuthenticateCallback(ctx context.Context, cb *Callback, providerKey string) (*user.User, error) { + // ctx is threaded through only to dispatch the login event; the OIDC token + // exchange, claim verification and user/avatar sync run on their own + // background contexts, exactly as the v1 callback always did. + provider, oauthToken, idToken, err := exchangeOidcTokens(cb, providerKey) //nolint:contextcheck if err != nil { - return err + return nil, err + } + + cl, err := getClaims(provider, oauthToken, idToken) //nolint:contextcheck + if err != nil { + return nil, err } s := db.NewSession() @@ -193,20 +217,20 @@ func HandleCallback(c *echo.Context) error { defer events.CleanupPending(s) // Check if we have seen this user before - u, err := getOrCreateUser(s, cl, provider, idToken) + u, err := getOrCreateUser(s, cl, provider, idToken) //nolint:contextcheck if err != nil { _ = s.Rollback() log.Errorf("Error creating new user for provider %s: %v", provider.Name, err) - return err + return nil, err } if u.Status == user.StatusDisabled { _ = s.Rollback() - return &user.ErrAccountDisabled{UserID: u.ID} + return nil, &user.ErrAccountDisabled{UserID: u.ID} } if u.Status == user.StatusAccountLocked { _ = s.Rollback() - return &user.ErrAccountLocked{UserID: u.ID} + return nil, &user.ErrAccountLocked{UserID: u.ID} } // Must run before team sync so a failed 2FA attempt cannot mutate team @@ -218,32 +242,31 @@ func HandleCallback(c *echo.Context) error { log.Errorf("Error committing session after failed OIDC TOTP attempt for user %d: %v", u.ID, commitErr) } else { // The user creation above was committed, so its events are real. - events.DispatchPending(c.Request().Context(), s) + events.DispatchPending(ctx, s) } if user.IsErrInvalidTOTPPasscode(err) { user.HandleFailedTOTPAuth(u) } - return err + return nil, err } teamData := getTeamDataFromToken(cl.VikunjaGroups, provider) err = models.SyncExternalTeamsForUser(s, u, teamData, idToken.Issuer, provider.Name) if err != nil { - return err + return nil, err } err = s.Commit() if err != nil { _ = s.Rollback() log.Errorf("Error creating new team for provider %s: %v", provider.Name, err) - return err + return nil, err } - events.DispatchPending(c.Request().Context(), s) + events.DispatchPending(ctx, s) - // Create token - return auth.NewUserAuthTokenResponse(u, c, false) + return u, nil } func getTeamDataFromToken(groups []map[string]interface{}, provider *Provider) (teamData []*models.Team) { @@ -516,21 +539,17 @@ func getClaims(provider *Provider, oauth2Token *oauth2.Token, idToken *oidc.IDTo return cl, nil } -func getProviderAndOidcTokens(c *echo.Context) (*Provider, *Callback, *oauth2.Token, *oidc.IDToken, error) { - - cb := &Callback{} - if err := c.Bind(cb); err != nil { - return nil, nil, nil, nil, &models.ErrOpenIDBadRequest{Message: "Bad data"} - } - - // Check if the provider exists - providerKey := c.Param("provider") +// exchangeOidcTokens resolves the provider, exchanges the callback's auth code, +// and verifies the returned ID token. It takes an already-bound Callback so it +// can be shared by the v1 echo handler (which binds from the request) and the v2 +// Huma handler (which binds via its typed body). +func exchangeOidcTokens(cb *Callback, providerKey string) (*Provider, *oauth2.Token, *oidc.IDToken, error) { provider, err := GetProvider(providerKey) if err != nil { - return nil, cb, nil, nil, err + return nil, nil, nil, err } if provider == nil { - return nil, cb, nil, nil, &models.ErrOpenIDBadRequest{Message: "Provider does not exist"} + return nil, nil, nil, &models.ErrOpenIDBadRequest{Message: "Provider does not exist"} } log.Debugf("Trying to authenticate user using provider: %s", provider.Key) @@ -546,25 +565,25 @@ func getProviderAndOidcTokens(c *echo.Context) (*Provider, *Callback, *oauth2.To if err := json.Unmarshal(rerr.Body, &details); err != nil { log.Errorf("Error unmarshalling token for provider %s: %v", provider.Name, err) log.Debugf("Raw token value is %s", rerr.Body) - return nil, cb, nil, nil, err + return nil, nil, nil, err } log.Errorf("Error retrieving token: %s", err) log.Debugf("Raw token value is %s", rerr.Body) - return nil, cb, nil, nil, &models.ErrOpenIDBadRequestWithDetails{ + return nil, nil, nil, &models.ErrOpenIDBadRequestWithDetails{ Message: "Could not authenticate against third party.", Details: details, } } - return nil, cb, nil, nil, err + return nil, nil, nil, err } // Extract the ID Token from OAuth2 token. rawIDToken, ok := oauth2Token.Extra("id_token").(string) if !ok { log.Debugf("Could not get id_token, raw token is %v", oauth2Token) - return nil, cb, nil, nil, &models.ErrOpenIDBadRequest{Message: "Missing token"} + return nil, nil, nil, &models.ErrOpenIDBadRequest{Message: "Missing token"} } verifier := provider.openIDProvider.Verifier(&oidc.Config{ClientID: provider.ClientID}) @@ -573,8 +592,8 @@ func getProviderAndOidcTokens(c *echo.Context) (*Provider, *Callback, *oauth2.To idToken, err := verifier.Verify(context.Background(), rawIDToken) if err != nil { log.Errorf("Error verifying token for provider %s: %v", provider.Name, err) - return nil, cb, nil, nil, err + return nil, nil, nil, err } - return provider, cb, oauth2Token, idToken, nil + return provider, oauth2Token, idToken, nil } diff --git a/pkg/routes/api/shared/auth.go b/pkg/routes/api/shared/auth.go index 925a533d8..acfa47ce0 100644 --- a/pkg/routes/api/shared/auth.go +++ b/pkg/routes/api/shared/auth.go @@ -26,7 +26,11 @@ import ( "code.vikunja.io/api/pkg/metrics" "code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/modules/auth" + "code.vikunja.io/api/pkg/modules/auth/ldap" + "code.vikunja.io/api/pkg/modules/keyvalue" "code.vikunja.io/api/pkg/user" + + "xorm.io/xorm" ) // UserRegister carries the fields accepted by the public registration endpoint: @@ -78,6 +82,127 @@ func RegisterUser(ctx context.Context, in *UserRegister) (*user.User, error) { return newUser, nil } +// AuthenticateUserCredentials verifies a login against local (and, if configured, +// LDAP) credentials and enforces the account-status and TOTP gates, returning the +// authenticated user on success. It is the transport-agnostic core of the login +// flow shared by v1 and v2; the caller issues the token and sets the cookie. The +// returned errors carry their own HTTP semantics (wrong credentials, disabled +// account, missing/invalid TOTP) so both APIs surface them identically. +func AuthenticateUserCredentials(ctx context.Context, login *user.Login) (*user.User, error) { + s := db.NewSession() + defer s.Close() + // Discards events queued during a rolled-back transaction (e.g. LDAP user + // creation); a no-op once DispatchPending has run. + defer events.CleanupPending(s) + + u, err := resolveLoginUser(ctx, s, login) + if err != nil { + _ = s.Rollback() + return nil, err + } + + if u.Status == user.StatusDisabled || u.Status == user.StatusAccountLocked { + _ = s.Rollback() + return nil, &user.ErrAccountDisabled{UserID: u.ID} + } + + if err := enforceLoginTOTP(s, u, login.TOTPPasscode); err != nil { + return nil, err + } + + if err := keyvalue.Del(u.GetFailedTOTPAttemptsKey()); err != nil { + return nil, err + } + if err := keyvalue.Del(u.GetFailedPasswordAttemptsKey()); err != nil { + return nil, err + } + + if err := s.Commit(); err != nil { + _ = s.Rollback() + return nil, err + } + + events.DispatchPending(ctx, s) + + return u, nil +} + +// resolveLoginUser authenticates the credentials against LDAP (when enabled) and +// then against local accounts, mirroring v1's order so local users keep working +// alongside LDAP. Bots are rejected before bcrypt runs because they have no +// password hash. +func resolveLoginUser(ctx context.Context, s *xorm.Session, login *user.Login) (*user.User, error) { + if config.AuthLdapEnabled.GetBool() { + u, err := ldap.AuthenticateUserInLDAP(s, login.Username, login.Password, config.AuthLdapGroupSyncEnabled.GetBool(), config.AuthLdapAvatarSyncAttribute.GetString()) + if err != nil && !user.IsErrWrongUsernameOrPassword(err) { + return nil, err + } + if u != nil { + return u, nil + } + } + + existingUser, lookupErr := user.GetUserByUsername(s, login.Username) + if lookupErr == nil && existingUser.IsBot() { + return nil, &user.ErrAccountIsBot{UserID: existingUser.ID} + } + + return user.CheckUserCredentials(ctx, s, login) +} + +// enforceLoginTOTP runs the TOTP gate for users who have it enabled, mirroring +// v1: a missing passcode is rejected, and a wrong one trips the failed-attempt +// lockout via HandleFailedTOTPAuth. The session is rolled back before +// HandleFailedTOTPAuth so its dedicated session can acquire a write lock on +// SQLite shared-cache (the lockout write is decoupled from this transaction — +// see GHSA-fgfv-pv97-6cmj). +func enforceLoginTOTP(s *xorm.Session, u *user.User, passcode string) error { + totpEnabled, err := user.TOTPEnabledForUser(s, u) + if err != nil { + _ = s.Rollback() + return err + } + if !totpEnabled { + return nil + } + + if passcode == "" { + _ = s.Rollback() + return user.ErrInvalidTOTPPasscode{} + } + + _, err = user.ValidateTOTPPasscode(s, &user.TOTPPasscode{User: u, Passcode: passcode}) + if err != nil { + _ = s.Rollback() + if user.IsErrInvalidTOTPPasscode(err) { + user.HandleFailedTOTPAuth(u) + } + return err + } + + return nil +} + +// DeleteSession removes the session with the given id, logging the user out +// server-side. An empty sid is a no-op (the token carried no session, e.g. an +// API token or a link share), matching v1. Shared by v1 and v2; the caller is +// responsible for clearing the refresh cookie. +func DeleteSession(sid string) error { + if sid == "" { + return nil + } + + s := db.NewSession() + defer s.Close() + + if _, err := s.Where("id = ?", sid).Delete(&models.Session{}); err != nil { + _ = s.Rollback() + return err + } + + return s.Commit() +} + // ResetPassword resets a user's password from a previously issued reset token // and invalidates all of that user's sessions, so a leaked password cannot be // used after a reset. Shared by v1 and v2. diff --git a/pkg/routes/api/v1/login.go b/pkg/routes/api/v1/login.go index ae92e1d72..2d740ffdf 100644 --- a/pkg/routes/api/v1/login.go +++ b/pkg/routes/api/v1/login.go @@ -25,8 +25,7 @@ import ( "code.vikunja.io/api/pkg/log" "code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/modules/auth" - "code.vikunja.io/api/pkg/modules/auth/ldap" - "code.vikunja.io/api/pkg/modules/keyvalue" + "code.vikunja.io/api/pkg/routes/api/shared" user2 "code.vikunja.io/api/pkg/user" "github.com/golang-jwt/jwt/v5" @@ -51,87 +50,11 @@ func Login(c *echo.Context) (err error) { return c.JSON(http.StatusBadRequest, models.Message{Message: "Please provide a username and password."}) } - s := db.NewSession() - defer s.Close() - // Discards events queued during a rolled-back transaction (e.g. LDAP user - // creation); a no-op once DispatchPending has run. - defer events.CleanupPending(s) - - var user *user2.User - if config.AuthLdapEnabled.GetBool() { - user, err = ldap.AuthenticateUserInLDAP(s, u.Username, u.Password, config.AuthLdapGroupSyncEnabled.GetBool(), config.AuthLdapAvatarSyncAttribute.GetString()) - if err != nil && !user2.IsErrWrongUsernameOrPassword(err) { - _ = s.Rollback() - return err - } - } - - if user == nil { - // Check if the user is a bot before attempting password verification, - // because bots have no password hash and bcrypt would fail with a - // misleading error. - existingUser, lookupErr := user2.GetUserByUsername(s, u.Username) - if lookupErr == nil && existingUser.IsBot() { - _ = s.Rollback() - return &user2.ErrAccountIsBot{UserID: existingUser.ID} - } - - // This allows us to still have local users while ldap is enabled - user, err = user2.CheckUserCredentials(c.Request().Context(), s, &u) - if err != nil { - _ = s.Rollback() - return err - } - } - - if user.Status == user2.StatusDisabled || user.Status == user2.StatusAccountLocked { - _ = s.Rollback() - return &user2.ErrAccountDisabled{UserID: user.ID} - } - - totpEnabled, err := user2.TOTPEnabledForUser(s, user) + user, err := shared.AuthenticateUserCredentials(c.Request().Context(), &u) if err != nil { - _ = s.Rollback() return err } - if totpEnabled { - if u.TOTPPasscode == "" { - _ = s.Rollback() - return user2.ErrInvalidTOTPPasscode{} - } - - _, err = user2.ValidateTOTPPasscode(s, &user2.TOTPPasscode{ - User: user, - Passcode: u.TOTPPasscode, - }) - if err != nil { - // Rollback before HandleFailedTOTPAuth so its dedicated session - // can acquire a write lock on SQLite shared-cache. The lockout - // write is decoupled from this handler's transaction — see - // GHSA-fgfv-pv97-6cmj. - _ = s.Rollback() - if user2.IsErrInvalidTOTPPasscode(err) { - user2.HandleFailedTOTPAuth(user) - } - return err - } - } - - if err := keyvalue.Del(user.GetFailedTOTPAttemptsKey()); err != nil { - return err - } - if err := keyvalue.Del(user.GetFailedPasswordAttemptsKey()); err != nil { - return err - } - - if err := s.Commit(); err != nil { - _ = s.Rollback() - return err - } - - events.DispatchPending(c.Request().Context(), s) - // Create token return auth.NewUserAuthTokenResponse(user, c, u.LongToken) } @@ -254,21 +177,7 @@ func Logout(c *echo.Context) (err error) { } } - if sid == "" { - return c.JSON(http.StatusOK, models.Message{Message: "Successfully logged out."}) - } - - s := db.NewSession() - defer s.Close() - - _, err = s.Where("id = ?", sid).Delete(&models.Session{}) - if err != nil { - _ = s.Rollback() - return err - } - - if err := s.Commit(); err != nil { - _ = s.Rollback() + if err := shared.DeleteSession(sid); err != nil { return err } From d4ab43807323725cf31c9e0f7385bf5614267611 Mon Sep 17 00:00:00 2001 From: kolaente Date: Fri, 12 Jun 2026 10:28:29 +0200 Subject: [PATCH 35/53] feat(api/v2): add login and logout on /api/v2 Port the cookie-setting login and logout endpoints to Huma. Both reuse the shared auth cores; the HttpOnly refresh cookie and Cache-Control: no-store header are set via the unwrapped echo context (the cookie stays out of the OpenAPI schema, matching v1). The token response inlines the JWT to avoid a schema-name collision with user.Token. login is public (LDAP-only deployments log in here too); logout inherits the global JWT auth and no-ops for tokens that carry no session. --- pkg/routes/api/v2/auth_login.go | 129 ++++++++++++++++++++++++++++++++ pkg/routes/api/v2/oauth.go | 11 ++- pkg/routes/routes.go | 14 ++-- 3 files changed, 142 insertions(+), 12 deletions(-) create mode 100644 pkg/routes/api/v2/auth_login.go diff --git a/pkg/routes/api/v2/auth_login.go b/pkg/routes/api/v2/auth_login.go new file mode 100644 index 000000000..d6ff0ff19 --- /dev/null +++ b/pkg/routes/api/v2/auth_login.go @@ -0,0 +1,129 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-present Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package apiv2 + +import ( + "context" + "net/http" + + "code.vikunja.io/api/pkg/modules/auth" + "code.vikunja.io/api/pkg/modules/humaecho5" + "code.vikunja.io/api/pkg/routes/api/shared" + "code.vikunja.io/api/pkg/user" + + "github.com/danielgtaylor/huma/v2" + "github.com/labstack/echo/v5" +) + +// authTokenBody wraps the issued user JWT. The token is inlined rather than +// embedding auth.Token because Huma derives schema names from the bare Go type +// name and a top-level auth.Token body would collide with user.Token (the +// caldav-token schema, also named "Token"). The refresh token is delivered out +// of band as an HttpOnly cookie, so it is intentionally absent from the schema. +type authTokenBody struct { + // Cache-Control: no-store keeps the access token out of any shared cache. + CacheControl string `header:"Cache-Control"` + Body struct { + Token string `json:"token" readOnly:"true" doc:"The short-lived JWT auth token. Send it as a bearer token on subsequent requests."` + } +} + +// logoutBody confirms a successful logout. +type logoutBody struct { + Body struct { + Message string `json:"message" readOnly:"true" doc:"A human-readable confirmation message."` + } +} + +func init() { AddRouteRegistrar(RegisterLoginRoutes) } + +// RegisterLoginRoutes wires the local/LDAP login and logout endpoints. Login is +// always registered (LDAP-only deployments still log in here); logout inherits +// the global JWT auth. +func RegisterLoginRoutes(api huma.API) { + tags := []string{"auth"} + + Register(api, huma.Operation{ + OperationID: "auth-login", + Summary: "Login", + Description: "Logs a user in with username and password (and a TOTP passcode when 2FA is enabled), returning a short-lived JWT. A long-lived refresh token is set as an HttpOnly cookie scoped to the refresh endpoint.", + Method: http.MethodPost, + Path: "/login", + DefaultStatus: http.StatusOK, + Tags: tags, + Security: publicSecurity, + }, authLogin) + + Register(api, huma.Operation{ + OperationID: "auth-logout", + Summary: "Logout", + Description: "Destroys the current session server-side and clears the refresh-token cookie. A no-op for API tokens and link shares, which carry no session.", + Method: http.MethodPost, + Path: "/logout", + DefaultStatus: http.StatusOK, + Tags: tags, + }, authLogout) +} + +func authLogin(ctx context.Context, in *struct{ Body user.Login }) (*authTokenBody, error) { + u, err := shared.AuthenticateUserCredentials(ctx, &in.Body) + if err != nil { + return nil, translateDomainError(err) + } + + deviceInfo, ipAddress := requestClientInfo(ctx) + token, err := auth.IssueUserToken(ctx, u, deviceInfo, ipAddress, in.Body.LongToken) + if err != nil { + return nil, translateDomainError(err) + } + + if ec := echoContextFromCtx(ctx); ec != nil { + auth.WriteUserAuthCookies(ec, token) + } + + out := &authTokenBody{CacheControl: "no-store"} + out.Body.Token = token.AccessToken + return out, nil +} + +func authLogout(ctx context.Context, _ *struct{}) (*logoutBody, error) { + var sid string + if ec := echoContextFromCtx(ctx); ec != nil { + auth.ClearRefreshTokenCookie(ec) + sid = auth.SessionIDFromContext(ec) + } + + if err := shared.DeleteSession(sid); err != nil { + return nil, translateDomainError(err) + } + + out := &logoutBody{} + out.Body.Message = "Successfully logged out." + return out, nil +} + +// echoContextFromCtx pulls the underlying *echo.Context off a Huma request +// context so a handler can set cookies and headers the OpenAPI schema does not +// model (the refresh-token cookie). Returns nil when the context carries no echo +// context (it always does under the humaecho5 adapter). +func echoContextFromCtx(ctx context.Context) *echo.Context { + ec, ok := ctx.Value(humaecho5.EchoContextKey).(*echo.Context) + if !ok || ec == nil { + return nil + } + return ec +} diff --git a/pkg/routes/api/v2/oauth.go b/pkg/routes/api/v2/oauth.go index 45d1efe57..a67441ad3 100644 --- a/pkg/routes/api/v2/oauth.go +++ b/pkg/routes/api/v2/oauth.go @@ -21,11 +21,9 @@ import ( "net/http" "code.vikunja.io/api/pkg/modules/auth/oauth2server" - "code.vikunja.io/api/pkg/modules/humaecho5" "code.vikunja.io/api/pkg/user" "github.com/danielgtaylor/huma/v2" - "github.com/labstack/echo/v5" ) // oauthTokenBody wraps the OAuth 2.0 token response. @@ -101,11 +99,12 @@ func oauthAuthorize(ctx context.Context, in *struct{ Body oauth2server.Authorize } // requestClientInfo pulls the user agent and client IP off the underlying Echo -// request so the authorization_code grant can record them on the session it -// creates, mirroring v1. Both fall back to "" when the context is unavailable. +// request so the authorization_code grant (and login) can record them on the +// session they create, mirroring v1. Both fall back to "" when the context is +// unavailable. func requestClientInfo(ctx context.Context) (deviceInfo, ipAddress string) { - ec, ok := ctx.Value(humaecho5.EchoContextKey).(*echo.Context) - if !ok || ec == nil { + ec := echoContextFromCtx(ctx) + if ec == nil { return "", "" } return (*ec).Request().UserAgent(), (*ec).RealIP() diff --git a/pkg/routes/routes.go b/pkg/routes/routes.go index e232cbbfa..d20b17f29 100644 --- a/pkg/routes/routes.go +++ b/pkg/routes/routes.go @@ -354,12 +354,14 @@ var unauthenticatedAPIPaths = map[string]bool{ "/api/v2/schemas/:schema": true, "/api/v2/info": true, - "/api/v2/register": true, - "/api/v2/user/password/token": true, - "/api/v2/user/password/reset": true, - "/api/v2/user/confirm": true, - "/api/v2/shares/:share/auth": true, - "/api/v2/oauth/token": true, + "/api/v2/register": true, + "/api/v2/user/password/token": true, + "/api/v2/user/password/reset": true, + "/api/v2/user/confirm": true, + "/api/v2/shares/:share/auth": true, + "/api/v2/oauth/token": true, + "/api/v2/login": true, + "/api/v2/auth/openid/:provider/callback": true, // Testing endpoints authenticate with the testing token via a custom // Authorization header, not a JWT; mounted only when that token is set. From 422d504a0727036f2952cdfe4daf4d59f315fcd2 Mon Sep 17 00:00:00 2001 From: kolaente Date: Fri, 12 Jun 2026 10:28:36 +0200 Subject: [PATCH 36/53] feat(api/v2): add OpenID Connect callback on /api/v2 Port the OIDC callback to Huma, reusing openid.AuthenticateCallback. The route is only registered when OpenID is enabled; unknown providers still 404 per request. v1's bespoke {message, details} error body is replaced by standard RFC 9457, folding the provider detail into the structured error. --- pkg/routes/api/v2/auth_openid.go | 93 ++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 pkg/routes/api/v2/auth_openid.go diff --git a/pkg/routes/api/v2/auth_openid.go b/pkg/routes/api/v2/auth_openid.go new file mode 100644 index 000000000..b52d7dca1 --- /dev/null +++ b/pkg/routes/api/v2/auth_openid.go @@ -0,0 +1,93 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-present Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package apiv2 + +import ( + "context" + "errors" + "net/http" + + "code.vikunja.io/api/pkg/config" + "code.vikunja.io/api/pkg/models" + "code.vikunja.io/api/pkg/modules/auth" + "code.vikunja.io/api/pkg/modules/auth/openid" + + "github.com/danielgtaylor/huma/v2" +) + +func init() { AddRouteRegistrar(RegisterOpenIDRoutes) } + +// RegisterOpenIDRoutes wires the OpenID Connect callback endpoint. It is only +// registered when OpenID is enabled; individual providers are still resolved per +// request, so an unknown provider key 404s even when others are configured. +func RegisterOpenIDRoutes(api huma.API) { + if !config.AuthOpenIDEnabled.GetBool() { + return + } + + Register(api, huma.Operation{ + OperationID: "auth-openid-callback", + Summary: "Authenticate with OpenID Connect", + Description: "Exchanges the authorization code returned by an OpenID Connect provider for a Vikunja JWT, creating or updating the matching user. A long-lived refresh token is set as an HttpOnly cookie. When the resolved user has 2FA enabled, the call returns 412 and must be retried with totp_passcode set.", + Method: http.MethodPost, + Path: "/auth/openid/{provider}/callback", + DefaultStatus: http.StatusOK, + Tags: []string{"auth"}, + Security: publicSecurity, + }, authOpenIDCallback) +} + +func authOpenIDCallback(ctx context.Context, in *struct { + Provider string `path:"provider" doc:"The OpenID Connect provider key as returned by the /info endpoint."` + Body openid.Callback `doc:"The provider callback, carrying the authorization code."` +}) (*authTokenBody, error) { + u, err := openid.AuthenticateCallback(ctx, &in.Body, in.Provider) //nolint:contextcheck // resolves providers from a cached, context-less map and runs OIDC discovery on its own background context, like the v1 callback. + if err != nil { + return nil, translateOpenIDError(err) + } + + deviceInfo, ipAddress := requestClientInfo(ctx) + // OIDC logins are not "remember me" sessions; v1 always issues a short one. + token, err := auth.IssueUserToken(ctx, u, deviceInfo, ipAddress, false) + if err != nil { + return nil, translateDomainError(err) + } + + if ec := echoContextFromCtx(ctx); ec != nil { + auth.WriteUserAuthCookies(ec, token) + } + + out := &authTokenBody{CacheControl: "no-store"} + out.Body.Token = token.AccessToken + return out, nil +} + +// translateOpenIDError maps OIDC callback errors to RFC 9457 responses. +// ErrOpenIDBadRequestWithDetails carries no HTTP semantics of its own (v1 renders +// it with a bespoke {message, details} body), so v2 maps it to a 400 with the +// provider detail attached as a structured error detail rather than porting the +// bespoke shape. Everything else flows through translateDomainError. +func translateOpenIDError(err error) error { + var detailedErr *models.ErrOpenIDBadRequestWithDetails + if errors.As(err, &detailedErr) { + return huma.Error400BadRequest(detailedErr.Message, &huma.ErrorDetail{ + Message: "The identity provider rejected the request.", + Value: detailedErr.Details, + }) + } + return translateDomainError(err) +} From 9aa0687288dfdfe4ccd6b44e517f6f04598c7c45 Mon Sep 17 00:00:00 2001 From: kolaente Date: Fri, 12 Jun 2026 10:28:36 +0200 Subject: [PATCH 37/53] test(api/v2): cover v2 login, logout and OIDC gating MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Login asserts the token, the HttpOnly refresh cookie, the no-store header and the credential/TOTP gates. Logout asserts the session is deleted and the cookie cleared. OIDC coverage is the registrar gate (404 when disabled, public route when enabled) — the full provider flow needs a live OIDC server, as the existing openid package tests show. --- pkg/webtests/huma_auth_login_test.go | 184 +++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 pkg/webtests/huma_auth_login_test.go diff --git a/pkg/webtests/huma_auth_login_test.go b/pkg/webtests/huma_auth_login_test.go new file mode 100644 index 000000000..423c66071 --- /dev/null +++ b/pkg/webtests/huma_auth_login_test.go @@ -0,0 +1,184 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-present Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package webtests + +import ( + "net/http" + "net/http/httptest" + "testing" + "time" + + "code.vikunja.io/api/pkg/config" + "code.vikunja.io/api/pkg/db" + "code.vikunja.io/api/pkg/models" + "code.vikunja.io/api/pkg/modules/auth" + "code.vikunja.io/api/pkg/user" + + "github.com/pquerna/otp/totp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// refreshCookie returns the Set-Cookie value for the refresh-token cookie, or "" +// if the response set no such cookie. +func refreshCookie(rec *httptest.ResponseRecorder) *http.Cookie { + for _, c := range rec.Result().Cookies() { + if c.Name == auth.RefreshTokenCookieName { + return c + } + } + return nil +} + +// TestHumaLogin ports the v1 login coverage to /api/v2: it asserts the token +// response, the HttpOnly refresh cookie, the no-store header, and the credential +// and TOTP gates. +func TestHumaLogin(t *testing.T) { + e, err := setupTestEnv() + require.NoError(t, err) + + login := func(body string) *httptest.ResponseRecorder { + return humaRequest(t, e, http.MethodPost, "/api/v2/login", body, "", "") + } + + t.Run("normal login", func(t *testing.T) { + rec := login(`{"username":"user1","password":"12345678"}`) + require.Equal(t, http.StatusOK, rec.Code, rec.Body.String()) + assert.Contains(t, rec.Body.String(), `"token":"`) + + assert.Equal(t, "no-store", rec.Header().Get("Cache-Control")) + + cookie := refreshCookie(rec) + require.NotNil(t, cookie, "login must set the refresh-token cookie") + assert.NotEmpty(t, cookie.Value) + assert.True(t, cookie.HttpOnly, "refresh cookie must be HttpOnly") + }) + + t.Run("wrong password", func(t *testing.T) { + rec := login(`{"username":"user1","password":"wrong"}`) + assert.Equal(t, http.StatusForbidden, rec.Code) + assert.Equal(t, user.ErrCodeWrongUsernameOrPassword, problemCode(t, rec)) + assert.Nil(t, refreshCookie(rec), "a failed login must not set a refresh cookie") + }) + + t.Run("nonexistent user", func(t *testing.T) { + rec := login(`{"username":"userWhichDoesNotExist","password":"12345678"}`) + assert.Equal(t, http.StatusForbidden, rec.Code) + assert.Equal(t, user.ErrCodeWrongUsernameOrPassword, problemCode(t, rec)) + }) + + t.Run("unconfirmed email", func(t *testing.T) { + rec := login(`{"username":"user5","password":"12345678"}`) + assert.Equal(t, http.StatusPreconditionFailed, rec.Code) + assert.Equal(t, user.ErrCodeEmailNotConfirmed, problemCode(t, rec)) + }) + + t.Run("TOTP required but missing", func(t *testing.T) { + rec := login(`{"username":"user10","password":"12345678"}`) + assert.Equal(t, http.StatusPreconditionFailed, rec.Code) + assert.Equal(t, user.ErrCodeInvalidTOTPPasscode, problemCode(t, rec)) + }) + + t.Run("TOTP wrong", func(t *testing.T) { + rec := login(`{"username":"user10","password":"12345678","totp_passcode":"000000"}`) + assert.Equal(t, http.StatusPreconditionFailed, rec.Code) + assert.Equal(t, user.ErrCodeInvalidTOTPPasscode, problemCode(t, rec)) + }) + + t.Run("TOTP correct", func(t *testing.T) { + code, err := totp.GenerateCode("JBSWY3DPEHPK3PXP", time.Now()) + require.NoError(t, err) + rec := login(`{"username":"user10","password":"12345678","totp_passcode":"` + code + `"}`) + require.Equal(t, http.StatusOK, rec.Code, rec.Body.String()) + assert.Contains(t, rec.Body.String(), `"token":"`) + assert.NotNil(t, refreshCookie(rec)) + }) +} + +// TestHumaLogout proves the v2 logout deletes the session server-side and clears +// the refresh-token cookie. +func TestHumaLogout(t *testing.T) { + e, err := setupTestEnv() + require.NoError(t, err) + + // Create a session so logout has something to delete, then mint a JWT whose + // sid claim points at it. + s := db.NewSession() + session, err := models.CreateSession(s, testuser1.ID, "test", "127.0.0.1", false) + require.NoError(t, err) + require.NoError(t, s.Commit()) + require.NoError(t, s.Close()) + + token, err := auth.NewUserJWTAuthtoken(&testuser1, session.ID) + require.NoError(t, err) + + rec := humaRequest(t, e, http.MethodPost, "/api/v2/logout", "", token, "") + require.Equal(t, http.StatusOK, rec.Code, rec.Body.String()) + assert.Contains(t, rec.Body.String(), "Successfully logged out.") + + cookie := refreshCookie(rec) + require.NotNil(t, cookie, "logout must clear the refresh cookie") + assert.Empty(t, cookie.Value, "cleared cookie has no value") + assert.Negative(t, cookie.MaxAge, "cleared cookie is expired") + + // The session must be gone. + check := db.NewSession() + defer check.Close() + exists, err := check.Where("id = ?", session.ID).Exist(&models.Session{}) + require.NoError(t, err) + assert.False(t, exists, "logout must delete the session") +} + +// TestHumaLoginUnauthenticated proves login needs no token (it is a public op). +func TestHumaLoginUnauthenticated(t *testing.T) { + e, err := setupTestEnv() + require.NoError(t, err) + + rec := humaRequest(t, e, http.MethodPost, "/api/v2/login", `{"username":"user1","password":"12345678"}`, "", "") + require.Equal(t, http.StatusOK, rec.Code, rec.Body.String()) +} + +// TestHumaOpenIDGating proves the OIDC callback route only exists when OpenID is +// enabled, mirroring the registrar gate. +func TestHumaOpenIDGating(t *testing.T) { + body := `{"code":"abc","redirect_url":"https://example.com"}` + + t.Run("disabled returns 404", func(t *testing.T) { + config.AuthOpenIDEnabled.Set(false) + + e, err := setupTestEnv() + require.NoError(t, err) + + rec := humaRequest(t, e, http.MethodPost, "/api/v2/auth/openid/test/callback", body, "", "") + assert.Equal(t, http.StatusNotFound, rec.Code) + }) + + t.Run("enabled does not require auth", func(t *testing.T) { + config.AuthOpenIDEnabled.Set(true) + defer config.AuthOpenIDEnabled.Set(false) + + e, err := setupTestEnv() + require.NoError(t, err) + + // No provider is configured, so the call fails downstream — but it must + // not 404 as an unknown route nor 401 for missing auth, which proves the + // public route is registered. + rec := humaRequest(t, e, http.MethodPost, "/api/v2/auth/openid/doesnotexist/callback", body, "", "") + assert.NotEqual(t, http.StatusNotFound, rec.Code) + assert.NotEqual(t, http.StatusUnauthorized, rec.Code) + }) +} From a32d8d6492c5b270ddf2ac901071031c573b9669 Mon Sep 17 00:00:00 2001 From: kolaente Date: Fri, 12 Jun 2026 11:05:14 +0200 Subject: [PATCH 38/53] fix(auth): roll back on commit failure in DeleteSession Restore the rollback-on-commit-failure that v1's Logout handler had before this session-deletion logic was extracted, so a failed commit does not leave the transaction open longer than the deferred Close. --- pkg/routes/api/shared/auth.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pkg/routes/api/shared/auth.go b/pkg/routes/api/shared/auth.go index acfa47ce0..d43deb358 100644 --- a/pkg/routes/api/shared/auth.go +++ b/pkg/routes/api/shared/auth.go @@ -200,7 +200,12 @@ func DeleteSession(sid string) error { return err } - return s.Commit() + if err := s.Commit(); err != nil { + _ = s.Rollback() + return err + } + + return nil } // ResetPassword resets a user's password from a previously issued reset token From 5b7924b1f6eef04679175265781133695658fd56 Mon Sep 17 00:00:00 2001 From: kolaente Date: Fri, 12 Jun 2026 11:08:36 +0200 Subject: [PATCH 39/53] fix(auth): return ErrAccountLocked for locked accounts on login The login status check mapped a locked account to ErrAccountDisabled, surfacing the disabled-account error code and message even though a dedicated ErrAccountLocked exists (and the OIDC flow already uses it). Map the locked status to ErrAccountLocked so credential login is consistent with OIDC across both /api/v1 and /api/v2. Disabled accounts still return ErrAccountDisabled. This changes the v1 login error code for locked accounts on the wire (1020 -> 1026); the change is intentional and approved. --- pkg/routes/api/shared/auth.go | 6 +++++- pkg/webtests/huma_auth_login_test.go | 12 ++++++++++++ pkg/webtests/login_test.go | 16 ++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/pkg/routes/api/shared/auth.go b/pkg/routes/api/shared/auth.go index d43deb358..153d851e8 100644 --- a/pkg/routes/api/shared/auth.go +++ b/pkg/routes/api/shared/auth.go @@ -101,10 +101,14 @@ func AuthenticateUserCredentials(ctx context.Context, login *user.Login) (*user. return nil, err } - if u.Status == user.StatusDisabled || u.Status == user.StatusAccountLocked { + if u.Status == user.StatusDisabled { _ = s.Rollback() return nil, &user.ErrAccountDisabled{UserID: u.ID} } + if u.Status == user.StatusAccountLocked { + _ = s.Rollback() + return nil, &user.ErrAccountLocked{UserID: u.ID} + } if err := enforceLoginTOTP(s, u, login.TOTPPasscode); err != nil { return nil, err diff --git a/pkg/webtests/huma_auth_login_test.go b/pkg/webtests/huma_auth_login_test.go index 423c66071..ad83fc811 100644 --- a/pkg/webtests/huma_auth_login_test.go +++ b/pkg/webtests/huma_auth_login_test.go @@ -87,6 +87,18 @@ func TestHumaLogin(t *testing.T) { assert.Equal(t, user.ErrCodeEmailNotConfirmed, problemCode(t, rec)) }) + t.Run("disabled account", func(t *testing.T) { + rec := login(`{"username":"user17","password":"12345678"}`) + assert.Equal(t, http.StatusPreconditionFailed, rec.Code) + assert.Equal(t, user.ErrCodeAccountDisabled, problemCode(t, rec)) + }) + + t.Run("locked account", func(t *testing.T) { + rec := login(`{"username":"user18","password":"12345678"}`) + assert.Equal(t, http.StatusPreconditionFailed, rec.Code) + assert.Equal(t, user.ErrCodeAccountLocked, problemCode(t, rec)) + }) + t.Run("TOTP required but missing", func(t *testing.T) { rec := login(`{"username":"user10","password":"12345678"}`) assert.Equal(t, http.StatusPreconditionFailed, rec.Code) diff --git a/pkg/webtests/login_test.go b/pkg/webtests/login_test.go index f271e1727..17b0f9b07 100644 --- a/pkg/webtests/login_test.go +++ b/pkg/webtests/login_test.go @@ -68,6 +68,22 @@ func TestLogin(t *testing.T) { require.Error(t, err) assertHandlerErrorCode(t, err, user.ErrCodeEmailNotConfirmed) }) + t.Run("disabled account", func(t *testing.T) { + _, err := newTestRequest(t, http.MethodPost, apiv1.Login, `{ + "username": "user17", + "password": "12345678" +}`, nil, nil) + require.Error(t, err) + assertHandlerErrorCode(t, err, user.ErrCodeAccountDisabled) + }) + t.Run("locked account", func(t *testing.T) { + _, err := newTestRequest(t, http.MethodPost, apiv1.Login, `{ + "username": "user18", + "password": "12345678" +}`, nil, nil) + require.Error(t, err) + assertHandlerErrorCode(t, err, user.ErrCodeAccountLocked) + }) } func TestLoginTOTPLockout(t *testing.T) { From 2cc7c0b6f01264b7b97b860b9dada79877008f51 Mon Sep 17 00:00:00 2001 From: kolaente Date: Wed, 17 Jun 2026 21:42:21 +0200 Subject: [PATCH 40/53] fix(frontend): auto-refresh relative dates as time passes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Relative dates ("5 minutes ago", "in 2 hours") were computed once via dayjs().fromNow() and never recomputed, so a view left open kept showing the value from the moment it was rendered. Compute the relative string against the shared, ticking `now` from useGlobalNow() instead. This makes every reactive caller — , direct formatDateSince() calls, and formatDisplayDate() when the user's date display is set to relative — re-render on the existing 60s tick. Absolute date formats don't read `now`, so they never needlessly re-render. useGlobalNow can now be initialised from a plain helper rather than only from a component, so its route-update hook is guarded with getCurrentInstance(). --- frontend/src/composables/useGlobalNow.ts | 14 +++++++++----- frontend/src/helpers/time/formatDate.ts | 8 +++++++- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/frontend/src/composables/useGlobalNow.ts b/frontend/src/composables/useGlobalNow.ts index 83d9cf9ef..c5e3510e7 100644 --- a/frontend/src/composables/useGlobalNow.ts +++ b/frontend/src/composables/useGlobalNow.ts @@ -1,4 +1,4 @@ -import { ref } from 'vue' +import { getCurrentInstance, ref } from 'vue' import { createGlobalState, useIntervalFn } from '@vueuse/core' import { onBeforeRouteUpdate } from 'vue-router' @@ -18,10 +18,14 @@ export const useGlobalNow = createGlobalState(() => { useIntervalFn(update, GLOBAL_NOW_INTERVAL, { immediate: true }) - // ensure the now value is refreshed when the route changes - onBeforeRouteUpdate(() => { - update() - }) + // Now that this state can be initialised from a plain helper (formatDateSince), the + // first caller is not guaranteed to be a component — guard the route hook accordingly. + if (getCurrentInstance()) { + // ensure the now value is refreshed when the route changes + onBeforeRouteUpdate(() => { + update() + }) + } return { now, diff --git a/frontend/src/helpers/time/formatDate.ts b/frontend/src/helpers/time/formatDate.ts index 4ff9a4da5..ed7f4a3d7 100644 --- a/frontend/src/helpers/time/formatDate.ts +++ b/frontend/src/helpers/time/formatDate.ts @@ -5,6 +5,7 @@ import {i18n} from '@/i18n' import {createSharedComposable} from '@vueuse/core' import {computed, toValue, type MaybeRefOrGetter} from 'vue' import {useDateDisplay} from '@/composables/useDateDisplay' +import {useGlobalNow} from '@/composables/useGlobalNow' import {useTimeFormat} from '@/composables/useTimeFormat' import {DATE_DISPLAY, type DateDisplay} from '@/constants/dateDisplay' import {TIME_FORMAT, type TimeFormat} from '@/constants/timeFormat' @@ -49,8 +50,13 @@ export const formatDateSince = (date: Date | string | null) => { const locale = DAYJS_LOCALE_MAPPING[i18n.global.locale.value.toLowerCase()] ?? 'en' + // Computing the relative string against the shared, ticking `now` (instead of fromNow's + // internal Date.now()) makes every reactive caller re-render on the 60s tick, so open views + // don't keep showing a stale "x minutes ago". + const {now} = useGlobalNow() + return date - ? dayjs(date).locale(locale).fromNow() + ? dayjs(date).locale(locale).from(now.value) : '' } From 20d8d2347439aa6f99a76c46a0b9ec88daaa0328 Mon Sep 17 00:00:00 2001 From: kolaente Date: Wed, 17 Jun 2026 22:33:24 +0200 Subject: [PATCH 41/53] chore(agents): remove CRUSH.md crush actually checks the AGENTS.md as well --- CRUSH.md | 1 - 1 file changed, 1 deletion(-) delete mode 120000 CRUSH.md diff --git a/CRUSH.md b/CRUSH.md deleted file mode 120000 index 47dc3e3d8..000000000 --- a/CRUSH.md +++ /dev/null @@ -1 +0,0 @@ -AGENTS.md \ No newline at end of file From 7c11c2dc29d1f4703c4d663dbf182f1e9e474cc9 Mon Sep 17 00:00:00 2001 From: kolaente Date: Wed, 17 Jun 2026 22:11:03 +0200 Subject: [PATCH 42/53] feat(api/v2): port refresh-token endpoint to /api/v2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit POST /api/v2/user/token/refresh reads the HttpOnly refresh cookie, rotates the session, mints a new JWT, and sets the new cookie — reusing the shared auth.RefreshSession core (no v1 change) and the #2912 cookie helpers / authTokenBody response shape. The cookie is set via the unwrapped echo ctx, not the OpenAPI spec. translateDomainError now maps *echo.HTTPError (which RefreshSession returns for missing/invalid/expired/replayed tokens) so those land as the right status instead of a 500. Completes the v1→v2 REST migration. --- pkg/routes/api/v2/auth_refresh.go | 75 +++++++++++++++++++++ pkg/routes/api/v2/errors.go | 12 ++++ pkg/routes/routes.go | 1 + pkg/webtests/huma_auth_refresh_test.go | 92 ++++++++++++++++++++++++++ 4 files changed, 180 insertions(+) create mode 100644 pkg/routes/api/v2/auth_refresh.go create mode 100644 pkg/webtests/huma_auth_refresh_test.go diff --git a/pkg/routes/api/v2/auth_refresh.go b/pkg/routes/api/v2/auth_refresh.go new file mode 100644 index 000000000..d264da7c1 --- /dev/null +++ b/pkg/routes/api/v2/auth_refresh.go @@ -0,0 +1,75 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-present Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package apiv2 + +import ( + "context" + "net/http" + + "code.vikunja.io/api/pkg/config" + "code.vikunja.io/api/pkg/modules/auth" + user2 "code.vikunja.io/api/pkg/user" + + "github.com/danielgtaylor/huma/v2" +) + +func init() { AddRouteRegistrar(RegisterRefreshTokenRoutes) } + +// RegisterRefreshTokenRoutes wires the refresh-token endpoint. It authenticates +// via the HttpOnly refresh cookie rather than a JWT, so it is a public operation. +func RegisterRefreshTokenRoutes(api huma.API) { + Register(api, huma.Operation{ + OperationID: "auth-refresh-token", + Summary: "Refresh user token", + Description: "Exchanges the refresh-token cookie for a new short-lived JWT. The refresh token is rotated on every call, so the previous one stops working. A new HttpOnly refresh cookie is set on the response.", + Method: http.MethodPost, + Path: "/user/token/refresh", + DefaultStatus: http.StatusOK, + Tags: []string{"auth"}, + Security: publicSecurity, + }, authRefreshToken) +} + +func authRefreshToken(ctx context.Context, _ *struct{}) (*authTokenBody, error) { + ec := echoContextFromCtx(ctx) + if ec == nil { + return nil, huma.Error401Unauthorized("No refresh token provided.") + } + + cookie, err := ec.Cookie(auth.RefreshTokenCookieName) + if err != nil || cookie.Value == "" { + return nil, huma.Error401Unauthorized("No refresh token provided.") + } + + result, err := auth.RefreshSession(cookie.Value) + if err != nil { + if user2.IsErrUserStatusError(err) { + auth.ClearRefreshTokenCookie(ec) + } + return nil, translateDomainError(err) + } + + cookieMaxAge := int(config.ServiceJWTTTL.GetInt64()) + if result.IsLongSession { + cookieMaxAge = int(config.ServiceJWTTTLLong.GetInt64()) + } + auth.SetRefreshTokenCookie(ec, result.NewRefreshToken, cookieMaxAge) + + out := &authTokenBody{CacheControl: "no-store"} + out.Body.Token = result.AccessToken + return out, nil +} diff --git a/pkg/routes/api/v2/errors.go b/pkg/routes/api/v2/errors.go index 3292b2e2b..24b73a19d 100644 --- a/pkg/routes/api/v2/errors.go +++ b/pkg/routes/api/v2/errors.go @@ -28,6 +28,7 @@ import ( "code.vikunja.io/api/pkg/web" "github.com/danielgtaylor/huma/v2" + "github.com/labstack/echo/v5" ) // authFromCtx retrieves the authed user from a Huma handler context, @@ -80,6 +81,17 @@ func translateDomainError(err error) error { } return se } + // Shared transport-agnostic cores (e.g. auth.RefreshSession) signal HTTP + // semantics with *echo.HTTPError. v1 lets echo's error handler render it; + // without this it would fall through as a 500 on v2. + var he *echo.HTTPError + if errors.As(err, &he) { + msg := he.Message + if msg == "" { + msg = http.StatusText(he.Code) + } + return huma.NewError(he.Code, msg) + } return err } diff --git a/pkg/routes/routes.go b/pkg/routes/routes.go index d20b17f29..a01e12be7 100644 --- a/pkg/routes/routes.go +++ b/pkg/routes/routes.go @@ -361,6 +361,7 @@ var unauthenticatedAPIPaths = map[string]bool{ "/api/v2/shares/:share/auth": true, "/api/v2/oauth/token": true, "/api/v2/login": true, + "/api/v2/user/token/refresh": true, "/api/v2/auth/openid/:provider/callback": true, // Testing endpoints authenticate with the testing token via a custom diff --git a/pkg/webtests/huma_auth_refresh_test.go b/pkg/webtests/huma_auth_refresh_test.go new file mode 100644 index 000000000..48fad31c6 --- /dev/null +++ b/pkg/webtests/huma_auth_refresh_test.go @@ -0,0 +1,92 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-present Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package webtests + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + + "code.vikunja.io/api/pkg/modules/auth" + + "github.com/labstack/echo/v5" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// refreshRequest posts to the v2 refresh endpoint with the given refresh-token +// cookie value (empty value omits the cookie entirely), driving the full +// echo+Huma stack so cookie reading and Set-Cookie writing are exercised. +func refreshRequest(e *echo.Echo, refreshToken string) *httptest.ResponseRecorder { + req := httptest.NewRequest(http.MethodPost, "/api/v2/user/token/refresh", strings.NewReader("")) + req.Header.Set("Content-Type", "application/json") + if refreshToken != "" { + req.AddCookie(&http.Cookie{Name: auth.RefreshTokenCookieName, Value: refreshToken}) + } + rec := httptest.NewRecorder() + e.ServeHTTP(rec, req) + return rec +} + +// TestHumaRefreshToken ports the v1 refresh-token coverage to /api/v2: a valid +// cookie yields a new JWT and a rotated HttpOnly cookie, the old token then stops +// working, and missing/invalid cookies map to the same 401 v1 returns. +func TestHumaRefreshToken(t *testing.T) { + e, err := setupTestEnv() + require.NoError(t, err) + + t.Run("valid refresh token", func(t *testing.T) { + rec := refreshRequest(e, "testtoken_session1") + require.Equal(t, http.StatusOK, rec.Code, rec.Body.String()) + assert.Contains(t, rec.Body.String(), `"token":"`) + assert.Equal(t, "no-store", rec.Header().Get("Cache-Control")) + + cookie := refreshCookie(rec) + require.NotNil(t, cookie, "refresh must set a new refresh-token cookie") + assert.NotEmpty(t, cookie.Value) + assert.NotEqual(t, "testtoken_session1", cookie.Value, "refresh token must be rotated") + assert.True(t, cookie.HttpOnly, "refresh cookie must be HttpOnly") + }) + + t.Run("rotation invalidates the old token", func(t *testing.T) { + // session2 is a separate session so this case does not depend on the + // one above. The first refresh succeeds and rotates the token. + first := refreshRequest(e, "testtoken_session2") + require.Equal(t, http.StatusOK, first.Code, first.Body.String()) + newCookie := refreshCookie(first) + require.NotNil(t, newCookie) + + // Replaying the now-rotated token must fail. + replay := refreshRequest(e, "testtoken_session2") + assert.Equal(t, http.StatusUnauthorized, replay.Code) + + // The freshly rotated token still works. + next := refreshRequest(e, newCookie.Value) + assert.Equal(t, http.StatusOK, next.Code, next.Body.String()) + }) + + t.Run("missing cookie", func(t *testing.T) { + rec := refreshRequest(e, "") + assert.Equal(t, http.StatusUnauthorized, rec.Code) + }) + + t.Run("invalid cookie", func(t *testing.T) { + rec := refreshRequest(e, "garbage") + assert.Equal(t, http.StatusUnauthorized, rec.Code) + }) +} From 1a4f03bbc8e7ea28208866a4c1bebf665cc005fc Mon Sep 17 00:00:00 2001 From: kolaente Date: Wed, 17 Jun 2026 21:37:48 +0200 Subject: [PATCH 43/53] feat(api/v2): expose healthcheck as a documented endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds GET /api/v2/health as a Huma operation so it appears in the v2 OpenAPI spec with a clean JSON schema ({"status": "OK"}). It runs the same health.Check() probe as the v1 healthcheck and is public — it opts out of the global bearer auth and is listed in unauthenticatedAPIPaths. --- pkg/routes/api/v2/health.go | 61 +++++++++++++++++++++++++++++++++++++ pkg/routes/routes.go | 3 ++ 2 files changed, 64 insertions(+) create mode 100644 pkg/routes/api/v2/health.go diff --git a/pkg/routes/api/v2/health.go b/pkg/routes/api/v2/health.go new file mode 100644 index 000000000..674dc7b85 --- /dev/null +++ b/pkg/routes/api/v2/health.go @@ -0,0 +1,61 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-present Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package apiv2 + +import ( + "context" + "net/http" + + "code.vikunja.io/api/pkg/health" + + "github.com/danielgtaylor/huma/v2" +) + +type healthBody struct { + Body struct { + Status string `json:"status" doc:"\"OK\" when the service and its dependencies are reachable." example:"OK"` + } +} + +// RegisterHealthRoutes wires the public healthcheck endpoint onto the Huma API. +func RegisterHealthRoutes(api huma.API) { + Register(api, huma.Operation{ + OperationID: "health", + Summary: "Healthcheck", + Description: "Reports whether the service and its dependencies (database) are reachable. Returns 200 with status \"OK\" when healthy, 500 otherwise. Public — no authentication required.", + Method: http.MethodGet, + Path: "/health", + Tags: []string{"service"}, + // Public: opt out of the globally-applied auth. The path is also listed + // in unauthenticatedAPIPaths so the token middleware lets it through. + Security: []map[string][]string{}, + }, healthcheck) +} + +func init() { AddRouteRegistrar(RegisterHealthRoutes) } + +func healthcheck(_ context.Context, _ *struct{}) (*healthBody, error) { + //nolint:contextcheck // health.Check is the shared v1/v2 probe; it takes no context and uses background contexts for its own pings. + if err := health.Check(); err != nil { + // Mirror v1: a failed check is an internal error; the cause is logged, + // not leaked to the client. + return nil, huma.Error500InternalServerError("Internal server error", err) + } + out := &healthBody{} + out.Body.Status = "OK" + return out, nil +} diff --git a/pkg/routes/routes.go b/pkg/routes/routes.go index a01e12be7..0b98918a0 100644 --- a/pkg/routes/routes.go +++ b/pkg/routes/routes.go @@ -368,6 +368,9 @@ var unauthenticatedAPIPaths = map[string]bool{ // Authorization header, not a JWT; mounted only when that token is set. "/api/v2/test/all": true, "/api/v2/test/:table": true, + + // Public infra healthcheck (a Huma op that opts out of the global auth). + "/api/v2/health": true, } // collectRoutesForAPITokens collects all routes for API token permission checking. From 4614e18e7a5f0e3d3ce515a86a14596e7f6edbca Mon Sep 17 00:00:00 2001 From: kolaente Date: Wed, 17 Jun 2026 21:37:54 +0200 Subject: [PATCH 44/53] refactor(feeds): extract atom feed builder + basic-auth validator for reuse Splits the transport-agnostic cores out of the v1 echo handlers so the v2 Huma endpoints can share them: - AuthenticateFeedToken(s, username, password) holds the token validation (prefix/length guard, owner match, feeds scope, bot rejection); BasicAuth now creates the session and delegates to it. - BuildNotificationsAtomFeed(s, u) renders the Atom XML; NotificationsAtomFeed reads the context user and delegates to it. - AtomContentType is shared so both transports set the same header. The v1 handlers keep identical observable behavior. --- pkg/routes/feeds/auth.go | 39 ++++++++++++++++++++++++------------- pkg/routes/feeds/handler.go | 38 +++++++++++++++++++++++------------- 2 files changed, 51 insertions(+), 26 deletions(-) diff --git a/pkg/routes/feeds/auth.go b/pkg/routes/feeds/auth.go index 419fd2ccd..a0142317b 100644 --- a/pkg/routes/feeds/auth.go +++ b/pkg/routes/feeds/auth.go @@ -23,9 +23,9 @@ import ( "code.vikunja.io/api/pkg/log" "code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/user" - "xorm.io/xorm" "github.com/labstack/echo/v5" + "xorm.io/xorm" ) func checkAPIToken(s *xorm.Session, username, token string) (*user.User, error) { @@ -50,35 +50,48 @@ func checkAPIToken(s *xorm.Session, username, token string) (*user.User, error) return u, nil } -// BasicAuth authenticates feed requests. Only API tokens are accepted — -// password and LDAP credentials are rejected outright because feed URLs are -// commonly exported, shared, or cached by feed readers. -func BasicAuth(c *echo.Context, username, password string) (bool, error) { +// AuthenticateFeedToken validates feed credentials against an existing session. +// Only API tokens are accepted — password and LDAP credentials are rejected +// outright because feed URLs are commonly exported, shared, or cached by feed +// readers. It returns the authenticated user, or nil for any rejection so +// callers can treat "invalid" and "unknown" identically. +func AuthenticateFeedToken(s *xorm.Session, username, password string) (*user.User, error) { if !strings.HasPrefix(password, models.APITokenPrefix) { - return false, nil + return nil, nil } // GetTokenFromTokenString slices password[len-8:] without a length check, // so a stray "tk_" or other short prefix-only string would panic before // the credentials could be rejected. Real tokens are far longer than // prefix+8, so anything shorter is invalid by construction. if len(password) < len(models.APITokenPrefix)+8 { - return false, nil + return nil, nil } - s := db.NewSession() - defer s.Close() - u, err := checkAPIToken(s, username, password) if err != nil { log.Errorf("Error during API token auth for feeds: %v", err) - return false, nil + return nil, nil } if u == nil { - return false, nil + return nil, nil } if u.IsBot() { log.Warningf("Feed auth rejected for bot user %d", u.ID) - return false, nil + return nil, nil + } + + return u, nil +} + +// BasicAuth authenticates feed requests for echo's BasicAuth middleware. The +// validation logic is shared with the v2 handler via AuthenticateFeedToken. +func BasicAuth(c *echo.Context, username, password string) (bool, error) { + s := db.NewSession() + defer s.Close() + + u, err := AuthenticateFeedToken(s, username, password) + if err != nil || u == nil { + return false, err } c.Set("userBasicAuth", u) diff --git a/pkg/routes/feeds/handler.go b/pkg/routes/feeds/handler.go index 9d0794c76..2a5ce289d 100644 --- a/pkg/routes/feeds/handler.go +++ b/pkg/routes/feeds/handler.go @@ -30,24 +30,22 @@ import ( "github.com/gorilla/feeds" "github.com/labstack/echo/v5" + "xorm.io/xorm" ) const feedItemLimit = 50 -// NotificationsAtomFeed serves the authenticated user's notifications as an -// Atom feed. Notifications are not marked as read by being fetched here. -func NotificationsAtomFeed(c *echo.Context) error { - u, ok := c.Get("userBasicAuth").(*user.User) - if !ok { - return echo.NewHTTPError(http.StatusUnauthorized, http.StatusText(http.StatusUnauthorized)) - } - - s := db.NewSession() - defer s.Close() +// AtomContentType is the content type of the notifications Atom feed. Shared so +// the v1 echo handler and the v2 Huma op set the same header. +const AtomContentType = "application/atom+xml; charset=utf-8" +// BuildNotificationsAtomFeed renders the user's latest notifications as Atom XML +// against an existing session. Notifications are not marked as read by being +// fetched here. Shared by the v1 echo handler and the v2 Huma op. +func BuildNotificationsAtomFeed(s *xorm.Session, u *user.User) (string, error) { rows, _, _, err := notifications.GetNotificationsForUser(s, u.ID, feedItemLimit, 0) if err != nil { - return err + return "", err } publicURL := config.ServicePublicURL.GetString() @@ -85,11 +83,25 @@ func NotificationsAtomFeed(c *echo.Context) error { }) } - atom, err := feed.ToAtom() + return feed.ToAtom() +} + +// NotificationsAtomFeed serves the authenticated user's notifications as an +// Atom feed. Notifications are not marked as read by being fetched here. +func NotificationsAtomFeed(c *echo.Context) error { + u, ok := c.Get("userBasicAuth").(*user.User) + if !ok { + return echo.NewHTTPError(http.StatusUnauthorized, http.StatusText(http.StatusUnauthorized)) + } + + s := db.NewSession() + defer s.Close() + + atom, err := BuildNotificationsAtomFeed(s, u) if err != nil { return err } - c.Response().Header().Set(echo.HeaderContentType, "application/atom+xml; charset=utf-8") + c.Response().Header().Set(echo.HeaderContentType, AtomContentType) return c.String(http.StatusOK, atom) } From 40f2900e9dd7707fd4a47295cfbc2270ecb0fc7e Mon Sep 17 00:00:00 2001 From: kolaente Date: Wed, 17 Jun 2026 21:38:16 +0200 Subject: [PATCH 45/53] feat(api/v2): expose notifications atom feed in the OpenAPI spec Adds GET /api/v2/notifications.atom as a Huma operation producing application/atom+xml, so the feed shows in the v2 OpenAPI spec with an opaque XML body schema. It mirrors /feeds/notifications.atom on the wire. Feed readers can't carry a bearer header, so the op declares an HTTP Basic security scheme (BasicAuth) and authenticates inside the handler: it parses the Authorization: Basic header and validates the API token via the shared feeds.AuthenticateFeedToken, returning a 401 with a Basic challenge on failure, then streams feeds.BuildNotificationsAtomFeed. The path is in unauthenticatedAPIPaths so the JWT middleware lets it through. --- pkg/routes/api/v2/huma.go | 8 ++ pkg/routes/api/v2/notifications_feed.go | 103 ++++++++++++++++++++++++ pkg/routes/routes.go | 4 + 3 files changed, 115 insertions(+) create mode 100644 pkg/routes/api/v2/notifications_feed.go diff --git a/pkg/routes/api/v2/huma.go b/pkg/routes/api/v2/huma.go index 7a7dc3514..9c674c1a7 100644 --- a/pkg/routes/api/v2/huma.go +++ b/pkg/routes/api/v2/huma.go @@ -104,6 +104,14 @@ func NewAPI(e *echo.Echo, g *echo.Group) huma.API { Scheme: "bearer", Description: "Vikunja API token (tk_ prefix) with scoped permissions. Created via /api/v1/tokens.", } + // HTTP Basic, used only by the notifications Atom feed: feed readers can't + // carry a bearer header, so the feed accepts the API token as the Basic + // password (username = token owner). See notifications_feed.go. + oapi.Components.SecuritySchemes["BasicAuth"] = &huma.SecurityScheme{ + Type: "http", + Scheme: "basic", + Description: "HTTP Basic auth used by the notifications Atom feed: the username is the token owner and the password is a feeds-scoped Vikunja API token (tk_ prefix).", + } // Applied globally; public endpoints (spec, docs) opt out with an empty Security list. oapi.Security = []map[string][]string{ {"JWTKeyAuth": {}}, diff --git a/pkg/routes/api/v2/notifications_feed.go b/pkg/routes/api/v2/notifications_feed.go new file mode 100644 index 000000000..d6195def2 --- /dev/null +++ b/pkg/routes/api/v2/notifications_feed.go @@ -0,0 +1,103 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-present Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package apiv2 + +import ( + "context" + "net/http" + + "code.vikunja.io/api/pkg/db" + "code.vikunja.io/api/pkg/modules/humaecho5" + "code.vikunja.io/api/pkg/routes/feeds" + + "github.com/danielgtaylor/huma/v2" + "github.com/labstack/echo/v5" +) + +// RegisterNotificationsFeedRoutes wires the Atom notifications feed onto the +// Huma API. It documents HTTP Basic auth (a feeds-scoped API token) because +// feed readers can't carry a bearer header. +func RegisterNotificationsFeedRoutes(api huma.API) { + Register(api, huma.Operation{ + OperationID: "notifications-atom-feed", + Summary: "Notifications Atom feed", + Description: "Returns the authenticated user's latest notifications as an Atom feed. Authenticated with HTTP Basic auth: the username is the token owner and the password is a feeds-scoped Vikunja API token (tk_ prefix) — password and LDAP credentials are rejected because feed URLs are commonly shared or cached. Fetching the feed does not mark notifications as read.", + Method: http.MethodGet, + Path: "/notifications.atom", + Tags: []string{"service"}, + // This op carries its own HTTP Basic auth instead of the global bearer + // schemes; the path is in unauthenticatedAPIPaths so the JWT middleware + // lets it through and the handler authenticates itself. + Security: []map[string][]string{{"BasicAuth": {}}}, + Responses: map[string]*huma.Response{ + "200": { + Description: "The notifications Atom feed.", + Content: map[string]*huma.MediaType{ + "application/atom+xml": { + Schema: &huma.Schema{Type: huma.TypeString, Format: "binary"}, + }, + }, + }, + }, + }, notificationsAtomFeed) +} + +func init() { AddRouteRegistrar(RegisterNotificationsFeedRoutes) } + +// notificationsAtomFeed authenticates with HTTP Basic (sharing the feeds +// validator) and streams the Atom feed; there is no handler.Do* for a non-JSON +// body and the auth can't ride the group's JWT middleware. +func notificationsAtomFeed(ctx context.Context, _ *struct{}) (*huma.StreamResponse, error) { + c, ok := ctx.Value(humaecho5.EchoContextKey).(*echo.Context) + if !ok { + return nil, huma.Error500InternalServerError("could not resolve request context") + } + + username, password, ok := (*c).Request().BasicAuth() + if !ok { + return nil, basicAuthChallenge(c) + } + + s := db.NewSession() + defer s.Close() + + u, err := feeds.AuthenticateFeedToken(s, username, password) + if err != nil { + return nil, translateDomainError(err) + } + if u == nil { + return nil, basicAuthChallenge(c) + } + + atom, err := feeds.BuildNotificationsAtomFeed(s, u) + if err != nil { + return nil, translateDomainError(err) + } + + return &huma.StreamResponse{Body: func(hctx huma.Context) { + ec := humaecho5.Unwrap(hctx) + (*ec).Response().Header().Set(echo.HeaderContentType, feeds.AtomContentType) + _, _ = (*ec).Response().Write([]byte(atom)) + }}, nil +} + +// basicAuthChallenge returns a 401 carrying a WWW-Authenticate Basic challenge, +// mirroring v1's BasicAuth middleware so feed readers prompt for credentials. +func basicAuthChallenge(c *echo.Context) error { + (*c).Response().Header().Set(echo.HeaderWWWAuthenticate, `Basic realm="Restricted"`) + return huma.Error401Unauthorized(http.StatusText(http.StatusUnauthorized)) +} diff --git a/pkg/routes/routes.go b/pkg/routes/routes.go index 0b98918a0..3cdc47f1b 100644 --- a/pkg/routes/routes.go +++ b/pkg/routes/routes.go @@ -371,6 +371,10 @@ var unauthenticatedAPIPaths = map[string]bool{ // Public infra healthcheck (a Huma op that opts out of the global auth). "/api/v2/health": true, + + // Atom feed (a Huma op) authenticates itself with HTTP Basic auth (a + // feeds-scoped API token), like its /feeds counterpart, not a JWT. + "/api/v2/notifications.atom": true, } // collectRoutesForAPITokens collects all routes for API token permission checking. From 9cad4f388ce300f1d15fc3f54e3045db48e67d3c Mon Sep 17 00:00:00 2001 From: kolaente Date: Wed, 17 Jun 2026 21:38:27 +0200 Subject: [PATCH 46/53] feat(api/v2): expose websocket endpoint under /api/v2 Adds GET /api/v2/ws as a raw echo route reusing the v1 upgrade handler. WebSockets can't be modeled in OpenAPI and Huma has no WS support, so it stays outside the Huma spec; it authenticates via its first message, so unauthenticatedAPIPaths exempts it from the group's JWT middleware. Also adds webtests covering all three /api/v2 non-CRUD endpoints: health returns OK, ws is reachable without a JWT, and the atom feed is basic-auth-gated. A spec test asserts /health and /notifications.atom appear in the generated OpenAPI paths (atom with its application/atom+xml response and BasicAuth security) while /ws is absent. --- pkg/routes/routes.go | 11 ++ pkg/webtests/huma_non_crud_aliases_test.go | 151 +++++++++++++++++++++ 2 files changed, 162 insertions(+) create mode 100644 pkg/webtests/huma_non_crud_aliases_test.go diff --git a/pkg/routes/routes.go b/pkg/routes/routes.go index 3cdc47f1b..bcdea1bdb 100644 --- a/pkg/routes/routes.go +++ b/pkg/routes/routes.go @@ -375,6 +375,10 @@ var unauthenticatedAPIPaths = map[string]bool{ // Atom feed (a Huma op) authenticates itself with HTTP Basic auth (a // feeds-scoped API token), like its /feeds counterpart, not a JWT. "/api/v2/notifications.atom": true, + + // WebSocket upgrade (a raw echo route — OpenAPI can't model WebSockets); + // it authenticates via its first message, so the upgrade needs no JWT. + "/api/v2/ws": true, } // collectRoutesForAPITokens collects all routes for API token permission checking. @@ -447,6 +451,13 @@ func registerAPIRoutesV2(e *echo.Echo, a *echo.Group) { a.GET("/docs", apiv2.ScalarUI) a.GET("/docs/scalar.standalone.js", apiv2.ScalarJS) + // WebSockets can't be modeled in OpenAPI and Huma has no WS support, so the + // upgrade endpoint stays a raw echo route (outside the Huma spec). It + // authenticates via its first message, so unauthenticatedAPIPaths exempts it + // from the group's JWT middleware. Health and the Atom feed are Huma ops and + // self-register via init()/RegisterAll. + a.GET("/ws", ws.UpgradeHandler) + // Resources self-register via init(); RegisterAll runs them all + AutoPatch. apiv2.RegisterAll(api) } diff --git a/pkg/webtests/huma_non_crud_aliases_test.go b/pkg/webtests/huma_non_crud_aliases_test.go new file mode 100644 index 000000000..1377507cd --- /dev/null +++ b/pkg/webtests/huma_non_crud_aliases_test.go @@ -0,0 +1,151 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-present Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package webtests + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/labstack/echo/v5" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// feedsTokenUser13 is a feeds-scoped API token for user 13 (see the feeds +// fixtures); it authenticates the v2 notifications Atom feed via HTTP Basic. +const feedsTokenUser13 = "tk_feeds_access_token_user_0013_feed0013" + +// TestHumaNonCRUDAliases covers the three non-REST endpoints mounted under +// /api/v2. Health and the Atom feed are Huma operations (so they appear in the +// OpenAPI spec); the WebSocket upgrade stays a raw echo route (OpenAPI can't +// model WebSockets). Each authenticates itself, so the group's JWT middleware +// must let them through. +func TestHumaNonCRUDAliases(t *testing.T) { + t.Run("health is public and returns OK", func(t *testing.T) { + e, err := setupTestEnv() + require.NoError(t, err) + + rec := humaRequest(t, e, http.MethodGet, "/api/v2/health", "", "", "") + require.Equal(t, http.StatusOK, rec.Code, "body: %s", rec.Body.String()) + assert.Contains(t, rec.Body.String(), "OK") + }) + + t.Run("ws is reachable without a JWT", func(t *testing.T) { + e, err := setupTestEnv() + require.NoError(t, err) + + // A plain GET without the upgrade headers makes websocket.Accept reject + // the request (typically 400). The point is that it reaches the handler + // at all — not a 401 from the JWT middleware nor a 404 for an unmounted + // route. + rec := humaRequest(t, e, http.MethodGet, "/api/v2/ws", "", "", "") + assert.NotEqual(t, http.StatusUnauthorized, rec.Code, "ws must not be blocked by v2 JWT auth") + assert.NotEqual(t, http.StatusNotFound, rec.Code, "ws must be mounted under /api/v2") + }) + + t.Run("atom feed is basic-auth-gated, not JWT-gated", func(t *testing.T) { + e, err := setupTestEnv() + require.NoError(t, err) + + t.Run("without credentials returns a basic-auth challenge", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/api/v2/notifications.atom", nil) + rec := httptest.NewRecorder() + e.ServeHTTP(rec, req) + + // The JWT middleware skips this path, so the handler's own HTTP Basic + // auth gates it instead: a 401 carrying a Basic challenge, not the JWT + // middleware's JSON error. + require.Equal(t, http.StatusUnauthorized, rec.Code) + assert.Contains(t, strings.ToLower(rec.Header().Get(echo.HeaderWWWAuthenticate)), "basic", + "expected a Basic auth challenge, got %q", rec.Header().Get(echo.HeaderWWWAuthenticate)) + }) + + t.Run("with a feeds API token returns an atom feed", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/api/v2/notifications.atom", nil) + req.SetBasicAuth("user13", feedsTokenUser13) + rec := httptest.NewRecorder() + e.ServeHTTP(rec, req) + + require.Equal(t, http.StatusOK, rec.Code, "body: %s", rec.Body.String()) + assert.True(t, strings.HasPrefix(rec.Header().Get(echo.HeaderContentType), "application/atom+xml"), + "expected atom content type, got %q", rec.Header().Get(echo.HeaderContentType)) + assert.Contains(t, rec.Body.String(), " Date: Wed, 17 Jun 2026 15:52:15 +0200 Subject: [PATCH 47/53] fix(tasks): prevent duplicate task_positions rows and stale identifiers A task could end up with more than one task_positions row for the same (task_id, project_view_id): rapid/concurrent creation raced the check-then-insert paths, and the create path could insert a position that a triggered RecalculateTaskPositions had already persisted for the new task. The table had no unique constraint, so the duplicates were stored silently (#2844). In the table view this made the LEFT JOIN on task_positions emit the task twice; getTasksForProjects enriched only the map entry, so the duplicate slice row kept an empty identifier and rendered as "#N" instead of "PREFIX-N" (#2725). - Add a unique index on task_positions(task_id, project_view_id) via a dedup migration (mirrors the task_buckets fix in 20250624092830) plus the unique(task_view) struct tag so fresh installs get it too. - Harden the create path: only queue a position insert when one does not already exist for the task+view, and dedupe within the batch. - Dedupe the task slice returned by getTasksForProjects by id, returning the enriched entry, so duplicate position rows can never surface a task twice or with a missing identifier. Fixes #2844 Fixes #2725 --- pkg/migration/20260617153629.go | 120 ++++++++++++++++++++++ pkg/models/kanban.go | 2 +- pkg/models/project.go | 2 +- pkg/models/saved_filter_positions_test.go | 11 +- pkg/models/task_collection_test.go | 2 +- pkg/models/task_position.go | 38 ++++++- pkg/models/task_search_bench_test.go | 2 +- pkg/models/tasks.go | 37 +++++-- 8 files changed, 200 insertions(+), 14 deletions(-) create mode 100644 pkg/migration/20260617153629.go diff --git a/pkg/migration/20260617153629.go b/pkg/migration/20260617153629.go new file mode 100644 index 000000000..f6e0cc9d7 --- /dev/null +++ b/pkg/migration/20260617153629.go @@ -0,0 +1,120 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-present Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package migration + +import ( + "src.techknowlogick.com/xormigrate" + "xorm.io/xorm" + "xorm.io/xorm/schemas" +) + +type taskPosition20260617153629 struct { + TaskID int64 `xorm:"bigint not null index"` + ProjectViewID int64 `xorm:"bigint not null index"` + Position float64 `xorm:"double not null"` +} + +func (taskPosition20260617153629) TableName() string { + return "task_positions" +} + +func init() { + migrations = append(migrations, &xormigrate.Migration{ + ID: "20260617153629", + Description: "deduplicate task positions and add a unique index on task_id + project_view_id", + Migrate: func(tx *xorm.Engine) error { + + s := tx.NewSession() + defer s.Close() + + err := s.Begin() + if err != nil { + return err + } + + // First remove all duplicate entries. A task may only ever have a + // single position per view; rapid task creation could race and + // insert more than one row before this constraint existed. + duplicates := []taskPosition20260617153629{} + err = s. + Select("task_id, project_view_id"). + GroupBy("task_id, project_view_id"). + Having("count(*) > 1"). + Find(&duplicates) + if err != nil { + _ = s.Rollback() + return err + } + + // Keep the lowest position of each group so the result is + // deterministic across databases. + kept := []taskPosition20260617153629{} + for _, dup := range duplicates { + row := taskPosition20260617153629{} + has, err := s. + Where("task_id = ? AND project_view_id = ?", dup.TaskID, dup.ProjectViewID). + OrderBy("position ASC"). + Get(&row) + if err != nil { + _ = s.Rollback() + return err + } + if !has { + continue + } + kept = append(kept, row) + } + + for _, dup := range duplicates { + _, err = s. + Where("task_id = ? AND project_view_id = ?", dup.TaskID, dup.ProjectViewID). + Delete(&taskPosition20260617153629{}) + if err != nil { + _ = s.Rollback() + return err + } + } + + for _, position := range kept { + _, err = s.Insert(&position) + if err != nil { + _ = s.Rollback() + return err + } + } + + err = s.Commit() + if err != nil { + return err + } + + // Then create the unique index + var query string + switch tx.Dialect().URI().DBType { + case schemas.MYSQL: + query = "CREATE UNIQUE INDEX UQE_task_positions_task_project_view ON task_positions (task_id, project_view_id)" + default: + query = "CREATE UNIQUE INDEX IF NOT EXISTS UQE_task_positions_task_project_view ON task_positions (task_id, project_view_id)" + } + _, err = tx.Exec(query) + return err + }, + Rollback: func(_ *xorm.Engine) error { + return nil + }, + }) +} diff --git a/pkg/models/kanban.go b/pkg/models/kanban.go index 01a6a145f..29b0b5705 100644 --- a/pkg/models/kanban.go +++ b/pkg/models/kanban.go @@ -257,7 +257,7 @@ func GetTasksInBucketsForView(s *xorm.Session, view *ProjectView, projects []*Pr } } - ts, _, total, err := getRawTasksForProjects(s, projects, auth, opts) + ts, total, err := getRawTasksForProjects(s, projects, auth, opts) if err != nil { return nil, err } diff --git a/pkg/models/project.go b/pkg/models/project.go index a799d1815..554f9d920 100644 --- a/pkg/models/project.go +++ b/pkg/models/project.go @@ -1370,7 +1370,7 @@ func (p *Project) Delete(s *xorm.Session, a web.Auth) (err error) { // Delete all tasks on that project // Using the loop to make sure all related entities to all tasks are properly deleted as well. - tasks, _, _, err := getRawTasksForProjects(s, []*Project{p}, a, &taskSearchOptions{}) + tasks, _, err := getRawTasksForProjects(s, []*Project{p}, a, &taskSearchOptions{}) if err != nil { return } diff --git a/pkg/models/saved_filter_positions_test.go b/pkg/models/saved_filter_positions_test.go index d233460d2..91bf09d9c 100644 --- a/pkg/models/saved_filter_positions_test.go +++ b/pkg/models/saved_filter_positions_test.go @@ -79,8 +79,17 @@ func TestCronInsertsNonZeroPosition(t *testing.T) { require.NoError(t, err) require.True(t, exists) + // Force the task to a zero position in this view to simulate the unhealed + // state. A task only ever has one position row per view, so update it if it + // already exists (e.g. created with the filter) instead of inserting a duplicate. tp := &TaskPosition{TaskID: task.ID, ProjectViewID: view.ID, Position: 0} - _, err = s.Insert(tp) + hasPosition, err := s.Where("task_id = ? AND project_view_id = ?", task.ID, view.ID).Exist(&TaskPosition{}) + require.NoError(t, err) + if hasPosition { + _, err = s.Where("task_id = ? AND project_view_id = ?", task.ID, view.ID).Cols("position").Update(tp) + } else { + _, err = s.Insert(tp) + } require.NoError(t, err) _, err = calculateNewPositionForTask(s, u, task, view) diff --git a/pkg/models/task_collection_test.go b/pkg/models/task_collection_test.go index e66d945b2..db918175e 100644 --- a/pkg/models/task_collection_test.go +++ b/pkg/models/task_collection_test.go @@ -1898,7 +1898,7 @@ func TestTaskSearchWithExpandSubtasks(t *testing.T) { expand: []TaskCollectionExpandable{TaskCollectionExpandSubtasks}, } - tasks, _, _, err := getRawTasksForProjects(s, []*Project{project}, &user.User{ID: 15}, opts) + tasks, _, err := getRawTasksForProjects(s, []*Project{project}, &user.User{ID: 15}, opts) require.NoError(t, err) require.NotEmpty(t, tasks) } diff --git a/pkg/models/task_position.go b/pkg/models/task_position.go index 07c2839cc..8ec58a3dd 100644 --- a/pkg/models/task_position.go +++ b/pkg/models/task_position.go @@ -33,9 +33,9 @@ const MinPositionSpacing = 0.01 type TaskPosition struct { // The ID of the task this position is for - TaskID int64 `xorm:"bigint not null index" json:"task_id" param:"task" readOnly:"true" doc:"The numeric id of the task this position belongs to. Taken from the URL; ignored in the request body."` + TaskID int64 `xorm:"bigint not null index unique(task_view)" json:"task_id" param:"task" readOnly:"true" doc:"The numeric id of the task this position belongs to. Taken from the URL; ignored in the request body."` // The project view this task is related to - ProjectViewID int64 `xorm:"bigint not null index" json:"project_view_id" doc:"The id of the project view this position applies to. Positions are stored per view, so the same task has an independent position in each of its project's views."` + ProjectViewID int64 `xorm:"bigint not null index unique(task_view)" json:"project_view_id" doc:"The id of the project view this position applies to. Positions are stored per view, so the same task has an independent position in each of its project's views."` // The position of the task - any task project can be sorted as usual by this parameter. // When accessing tasks via kanban buckets, this is primarily used to sort them based on a range // We're using a float64 here to make it possible to put any task within any two other tasks (by changing the number). @@ -341,6 +341,40 @@ func calculateNewPositionForTask(s *xorm.Session, a web.Auth, t *Task, view *Pro }, nil } +type taskPositionKey struct { + taskID int64 + viewID int64 +} + +// filterNewTaskPositions returns the positions whose (task_id, project_view_id) +// row does not exist yet, also deduplicating within the slice. Position creation +// during task creation can trigger a full recalculation (calculateNewPositionForTask +// or moveTaskToDoneBuckets) that already persists rows for the new task, so inserting +// the queued positions unconditionally would violate the unique index on +// (task_id, project_view_id). +func filterNewTaskPositions(s *xorm.Session, positions []*TaskPosition) ([]*TaskPosition, error) { + filtered := make([]*TaskPosition, 0, len(positions)) + seen := make(map[taskPositionKey]bool, len(positions)) + for _, p := range positions { + key := taskPositionKey{taskID: p.TaskID, viewID: p.ProjectViewID} + if seen[key] { + continue + } + seen[key] = true + + exists, err := s. + Where("task_id = ? AND project_view_id = ?", p.TaskID, p.ProjectViewID). + Exist(&TaskPosition{}) + if err != nil { + return nil, err + } + if !exists { + filtered = append(filtered, p) + } + } + return filtered, nil +} + // DeleteOrphanedTaskPositions removes task position records that reference // tasks or project views that no longer exist. // If dryRun is true, it counts the orphaned records without deleting them. diff --git a/pkg/models/task_search_bench_test.go b/pkg/models/task_search_bench_test.go index 142a58e41..443cf179e 100644 --- a/pkg/models/task_search_bench_test.go +++ b/pkg/models/task_search_bench_test.go @@ -139,7 +139,7 @@ func BenchmarkTaskSearch(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { s := db.NewSession() - resultSlice, _, _, err := getRawTasksForProjects(s, projects, auth, opts) + resultSlice, _, err := getRawTasksForProjects(s, projects, auth, opts) if len(resultSlice) == 0 { b.Fatalf("no results found for needle %q", needle) } diff --git a/pkg/models/tasks.go b/pkg/models/tasks.go index eb2989694..d57b98b85 100644 --- a/pkg/models/tasks.go +++ b/pkg/models/tasks.go @@ -288,11 +288,11 @@ func getTaskIndexFromSearchString(s string) (index int64) { return } -func getRawTasksForProjects(s *xorm.Session, projects []*Project, a web.Auth, opts *taskSearchOptions) (tasks []*Task, resultCount int, totalItems int64, err error) { +func getRawTasksForProjects(s *xorm.Session, projects []*Project, a web.Auth, opts *taskSearchOptions) (tasks []*Task, totalItems int64, err error) { // If the user does not have any projects, don't try to get any tasks if len(projects) == 0 { - return nil, 0, 0, nil + return nil, 0, nil } // Get all project IDs and get the tasks @@ -324,17 +324,18 @@ func getRawTasksForProjects(s *xorm.Session, projects []*Project, a web.Auth, op } tasks, totalItems, err = dbSearcher.Search(opts) - return tasks, len(tasks), totalItems, err + return tasks, totalItems, err } func getTasksForProjects(s *xorm.Session, projects []*Project, a web.Auth, opts *taskSearchOptions, view *ProjectView) (tasks []*Task, resultCount int, totalItems int64, err error) { - tasks, resultCount, totalItems, err = getRawTasksForProjects(s, projects, a, opts) + tasks, totalItems, err = getRawTasksForProjects(s, projects, a, opts) if err != nil { return nil, 0, 0, err } - taskMap := make(map[int64]*Task, len(tasks)) - for _, t := range tasks { + rawTasks := tasks + taskMap := make(map[int64]*Task, len(rawTasks)) + for _, t := range rawTasks { taskMap[t.ID] = t } @@ -343,7 +344,22 @@ func getTasksForProjects(s *xorm.Session, projects []*Project, a web.Auth, opts return nil, 0, 0, err } - return tasks, resultCount, totalItems, err + // A task can appear more than once in the raw result when it has duplicate + // task_positions rows for the view (the LEFT JOIN multiplies it). Return one + // entry per task, in the original sort order, referencing the enriched map + // value so its identifier and other data are set. totalItems already counts + // distinct tasks, so this also aligns the page size with it. + tasks = make([]*Task, 0, len(taskMap)) + seen := make(map[int64]bool, len(taskMap)) + for _, t := range rawTasks { + if seen[t.ID] { + continue + } + seen[t.ID] = true + tasks = append(tasks, taskMap[t.ID]) + } + + return tasks, len(tasks), totalItems, err } // GetTaskByIDSimple returns a raw task without extra data by the task ID @@ -984,6 +1000,13 @@ func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool, setB return err } + if len(positions) > 0 { + positions, err = filterNewTaskPositions(s, positions) + if err != nil { + return err + } + } + if len(positions) > 0 { _, err = s.Insert(&positions) if err != nil { From 647f1f4def822d1d52f28b1a04d334792dbe3d81 Mon Sep 17 00:00:00 2001 From: kolaente Date: Wed, 17 Jun 2026 22:55:27 +0200 Subject: [PATCH 48/53] fix(migration): fail loudly if a deduplicated position pair has no row A pair returned by the GroupBy was just reported as duplicated, so a row must exist. Continuing on !has would let the delete loop drop every row for that pair without re-inserting one, silently losing positions. Abort the migration instead. --- pkg/migration/20260617153629.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pkg/migration/20260617153629.go b/pkg/migration/20260617153629.go index f6e0cc9d7..d5ae3d772 100644 --- a/pkg/migration/20260617153629.go +++ b/pkg/migration/20260617153629.go @@ -17,6 +17,8 @@ package migration import ( + "fmt" + "src.techknowlogick.com/xormigrate" "xorm.io/xorm" "xorm.io/xorm/schemas" @@ -74,7 +76,12 @@ func init() { return err } if !has { - continue + // The pair was just reported as duplicated by the GroupBy above, + // so a row must exist. If it doesn't, fail instead of continuing — + // the delete loop below would otherwise drop every row for the pair + // without re-inserting one. + _ = s.Rollback() + return fmt.Errorf("no task_positions row found for task %d and project view %d while deduplicating positions", dup.TaskID, dup.ProjectViewID) } kept = append(kept, row) } From 99d025399c780b6b3d7382d7756fdab159fa0768 Mon Sep 17 00:00:00 2001 From: kolaente Date: Wed, 17 Jun 2026 22:55:32 +0200 Subject: [PATCH 49/53] perf(tasks): batch task position existence check into one query filterNewTaskPositions ran one Exist query per position. createTask calls it in loops (bulk import, project duplication), so this was O(tasks * views) queries. Fetch all existing rows for the involved tasks once and filter in memory instead. --- pkg/models/task_position.go | 39 ++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/pkg/models/task_position.go b/pkg/models/task_position.go index 8ec58a3dd..bcc09884b 100644 --- a/pkg/models/task_position.go +++ b/pkg/models/task_position.go @@ -353,24 +353,41 @@ type taskPositionKey struct { // the queued positions unconditionally would violate the unique index on // (task_id, project_view_id). func filterNewTaskPositions(s *xorm.Session, positions []*TaskPosition) ([]*TaskPosition, error) { + if len(positions) == 0 { + return positions, nil + } + + taskIDs := make([]int64, 0, len(positions)) + seenTask := make(map[int64]bool, len(positions)) + for _, p := range positions { + if seenTask[p.TaskID] { + continue + } + seenTask[p.TaskID] = true + taskIDs = append(taskIDs, p.TaskID) + } + + // Fetch all existing rows for the involved tasks in one query so this stays + // cheap when createTask runs in a loop (bulk import, project duplication). + existing := []*TaskPosition{} + err := s.In("task_id", taskIDs).Find(&existing) + if err != nil { + return nil, err + } + + seen := make(map[taskPositionKey]bool, len(positions)+len(existing)) + for _, e := range existing { + seen[taskPositionKey{taskID: e.TaskID, viewID: e.ProjectViewID}] = true + } + filtered := make([]*TaskPosition, 0, len(positions)) - seen := make(map[taskPositionKey]bool, len(positions)) for _, p := range positions { key := taskPositionKey{taskID: p.TaskID, viewID: p.ProjectViewID} if seen[key] { continue } seen[key] = true - - exists, err := s. - Where("task_id = ? AND project_view_id = ?", p.TaskID, p.ProjectViewID). - Exist(&TaskPosition{}) - if err != nil { - return nil, err - } - if !exists { - filtered = append(filtered, p) - } + filtered = append(filtered, p) } return filtered, nil } From 7b7c850dd85292b379ad821c9eed6fa30be77bd5 Mon Sep 17 00:00:00 2001 From: kolaente Date: Wed, 17 Jun 2026 22:55:37 +0200 Subject: [PATCH 50/53] refactor(tasks): drop in-memory task dedup, rely on unique index The duplicate task rows getTasksForProjects deduplicated came from the LEFT JOIN multiplying when duplicate task_positions rows existed. The new unique index on (task_id, project_view_id) removes the root cause at the SQL layer (the migration also runs before serving), so the join can no longer multiply. Revert getTasksForProjects and getRawTasksForProjects to their pre-dedup shape. --- pkg/models/kanban.go | 2 +- pkg/models/project.go | 2 +- pkg/models/task_collection_test.go | 2 +- pkg/models/task_search_bench_test.go | 2 +- pkg/models/tasks.go | 30 +++++++--------------------- 5 files changed, 11 insertions(+), 27 deletions(-) diff --git a/pkg/models/kanban.go b/pkg/models/kanban.go index 29b0b5705..01a6a145f 100644 --- a/pkg/models/kanban.go +++ b/pkg/models/kanban.go @@ -257,7 +257,7 @@ func GetTasksInBucketsForView(s *xorm.Session, view *ProjectView, projects []*Pr } } - ts, total, err := getRawTasksForProjects(s, projects, auth, opts) + ts, _, total, err := getRawTasksForProjects(s, projects, auth, opts) if err != nil { return nil, err } diff --git a/pkg/models/project.go b/pkg/models/project.go index 554f9d920..a799d1815 100644 --- a/pkg/models/project.go +++ b/pkg/models/project.go @@ -1370,7 +1370,7 @@ func (p *Project) Delete(s *xorm.Session, a web.Auth) (err error) { // Delete all tasks on that project // Using the loop to make sure all related entities to all tasks are properly deleted as well. - tasks, _, err := getRawTasksForProjects(s, []*Project{p}, a, &taskSearchOptions{}) + tasks, _, _, err := getRawTasksForProjects(s, []*Project{p}, a, &taskSearchOptions{}) if err != nil { return } diff --git a/pkg/models/task_collection_test.go b/pkg/models/task_collection_test.go index db918175e..e66d945b2 100644 --- a/pkg/models/task_collection_test.go +++ b/pkg/models/task_collection_test.go @@ -1898,7 +1898,7 @@ func TestTaskSearchWithExpandSubtasks(t *testing.T) { expand: []TaskCollectionExpandable{TaskCollectionExpandSubtasks}, } - tasks, _, err := getRawTasksForProjects(s, []*Project{project}, &user.User{ID: 15}, opts) + tasks, _, _, err := getRawTasksForProjects(s, []*Project{project}, &user.User{ID: 15}, opts) require.NoError(t, err) require.NotEmpty(t, tasks) } diff --git a/pkg/models/task_search_bench_test.go b/pkg/models/task_search_bench_test.go index 443cf179e..142a58e41 100644 --- a/pkg/models/task_search_bench_test.go +++ b/pkg/models/task_search_bench_test.go @@ -139,7 +139,7 @@ func BenchmarkTaskSearch(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { s := db.NewSession() - resultSlice, _, err := getRawTasksForProjects(s, projects, auth, opts) + resultSlice, _, _, err := getRawTasksForProjects(s, projects, auth, opts) if len(resultSlice) == 0 { b.Fatalf("no results found for needle %q", needle) } diff --git a/pkg/models/tasks.go b/pkg/models/tasks.go index d57b98b85..a49e4175e 100644 --- a/pkg/models/tasks.go +++ b/pkg/models/tasks.go @@ -288,11 +288,11 @@ func getTaskIndexFromSearchString(s string) (index int64) { return } -func getRawTasksForProjects(s *xorm.Session, projects []*Project, a web.Auth, opts *taskSearchOptions) (tasks []*Task, totalItems int64, err error) { +func getRawTasksForProjects(s *xorm.Session, projects []*Project, a web.Auth, opts *taskSearchOptions) (tasks []*Task, resultCount int, totalItems int64, err error) { // If the user does not have any projects, don't try to get any tasks if len(projects) == 0 { - return nil, 0, nil + return nil, 0, 0, nil } // Get all project IDs and get the tasks @@ -324,18 +324,17 @@ func getRawTasksForProjects(s *xorm.Session, projects []*Project, a web.Auth, op } tasks, totalItems, err = dbSearcher.Search(opts) - return tasks, totalItems, err + return tasks, len(tasks), totalItems, err } func getTasksForProjects(s *xorm.Session, projects []*Project, a web.Auth, opts *taskSearchOptions, view *ProjectView) (tasks []*Task, resultCount int, totalItems int64, err error) { - tasks, totalItems, err = getRawTasksForProjects(s, projects, a, opts) + tasks, resultCount, totalItems, err = getRawTasksForProjects(s, projects, a, opts) if err != nil { return nil, 0, 0, err } - rawTasks := tasks - taskMap := make(map[int64]*Task, len(rawTasks)) - for _, t := range rawTasks { + taskMap := make(map[int64]*Task, len(tasks)) + for _, t := range tasks { taskMap[t.ID] = t } @@ -344,22 +343,7 @@ func getTasksForProjects(s *xorm.Session, projects []*Project, a web.Auth, opts return nil, 0, 0, err } - // A task can appear more than once in the raw result when it has duplicate - // task_positions rows for the view (the LEFT JOIN multiplies it). Return one - // entry per task, in the original sort order, referencing the enriched map - // value so its identifier and other data are set. totalItems already counts - // distinct tasks, so this also aligns the page size with it. - tasks = make([]*Task, 0, len(taskMap)) - seen := make(map[int64]bool, len(taskMap)) - for _, t := range rawTasks { - if seen[t.ID] { - continue - } - seen[t.ID] = true - tasks = append(tasks, taskMap[t.ID]) - } - - return tasks, len(tasks), totalItems, err + return tasks, resultCount, totalItems, err } // GetTaskByIDSimple returns a raw task without extra data by the task ID From 7f53be410567269eaa3ec24333d0fcdd51f57436 Mon Sep 17 00:00:00 2001 From: kolaente Date: Wed, 17 Jun 2026 21:35:25 +0200 Subject: [PATCH 51/53] fix(notifications): refresh embedded users when reading notifications Notifications stored before the acting user was resolved with its full profile (#2720) were serialized with only id+username, so they kept rendering the auto-generated username instead of the display name. Reload every embedded user from the database when reading a user's notifications, healing already-stored rows at read time. The refresh is not persisted; a per-page cache fetches each user once. --- .golangci.yml | 3 + pkg/models/notifications_database.go | 8 +- pkg/models/notifications_refresh.go | 122 +++++++++++++++++++++++ pkg/models/notifications_refresh_test.go | 109 ++++++++++++++++++++ 4 files changed, 241 insertions(+), 1 deletion(-) create mode 100644 pkg/models/notifications_refresh.go create mode 100644 pkg/models/notifications_refresh_test.go diff --git a/.golangci.yml b/.golangci.yml index 19ee2f531..7c75f08de 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -80,6 +80,9 @@ linters: - linters: - exhaustive path: pkg/models/task_collection_filter\.go + - linters: + - exhaustive + path: pkg/models/notifications_refresh\.go - linters: - gosec path: pkg/utils/random_string\.go diff --git a/pkg/models/notifications_database.go b/pkg/models/notifications_database.go index aaa71103c..75beef580 100644 --- a/pkg/models/notifications_database.go +++ b/pkg/models/notifications_database.go @@ -53,7 +53,13 @@ func (d *DatabaseNotifications) ReadAll(s *xorm.Session, a web.Auth, _ string, p } limit, start := getLimitFromPageIndex(page, perPage) - return notifications.GetNotificationsForUser(s, a.GetID(), limit, start) + ns, resultCount, total, err := notifications.GetNotificationsForUser(s, a.GetID(), limit, start) + if err != nil { + return nil, 0, 0, err + } + + refreshNotificationsUsers(s, ns) + return ns, resultCount, total, nil } // CanUpdate checks if a user can mark a notification as read. diff --git a/pkg/models/notifications_refresh.go b/pkg/models/notifications_refresh.go new file mode 100644 index 000000000..81e8a9609 --- /dev/null +++ b/pkg/models/notifications_refresh.go @@ -0,0 +1,122 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-present Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package models + +import ( + "encoding/json" + "reflect" + + "code.vikunja.io/api/pkg/log" + "code.vikunja.io/api/pkg/notifications" + "code.vikunja.io/api/pkg/user" + + "xorm.io/xorm" +) + +// maxNotificationUserRefreshDepth bounds the reflection walk so an unexpectedly +// deep payload cannot recurse without end. +const maxNotificationUserRefreshDepth = 8 + +// refreshNotificationsUsers reloads every embedded user of each notification +// from the database. Notifications serialized before the acting user was +// resolved with its full profile (#2720) stored only id+username, so without +// this they keep rendering the auto-generated username instead of the display +// name. It runs at read time and is not persisted; one cache is shared across +// the batch so a user recurring across notifications is fetched only once. +func refreshNotificationsUsers(s *xorm.Session, dbNotifications []*notifications.DatabaseNotification) { + cache := make(map[int64]*user.User) + for _, dbn := range dbNotifications { + refreshNotificationUsers(s, dbn, cache) + } +} + +func refreshNotificationUsers(s *xorm.Session, dbn *notifications.DatabaseNotification, cache map[int64]*user.User) { + typed, ok := notifications.Lookup(dbn.Name) + if !ok { + return + } + + raw, err := json.Marshal(dbn.Notification) + if err != nil { + log.Errorf("Could not marshal notification %d to refresh its users: %v", dbn.ID, err) + return + } + if err := json.Unmarshal(raw, typed); err != nil { + log.Errorf("Could not unmarshal notification %d to refresh its users: %v", dbn.ID, err) + return + } + + refreshUsersInValue(s, reflect.ValueOf(typed), cache, 0) + dbn.Notification = typed +} + +func refreshUsersInValue(s *xorm.Session, v reflect.Value, cache map[int64]*user.User, depth int) { + if depth > maxNotificationUserRefreshDepth || !v.IsValid() { + return + } + + switch v.Kind() { + case reflect.Ptr: + if v.IsNil() { + return + } + if u, is := v.Interface().(*user.User); is { + refreshUser(s, u, cache) + return + } + refreshUsersInValue(s, v.Elem(), cache, depth+1) + case reflect.Struct: + for i := 0; i < v.NumField(); i++ { + if !v.Type().Field(i).IsExported() { + continue + } + refreshUsersInValue(s, v.Field(i), cache, depth+1) + } + case reflect.Slice, reflect.Array: + for i := 0; i < v.Len(); i++ { + refreshUsersInValue(s, v.Index(i), cache, depth+1) + } + case reflect.Map: + for _, key := range v.MapKeys() { + refreshUsersInValue(s, v.MapIndex(key), cache, depth+1) + } + } +} + +// refreshUser overwrites the user in place with its current database row. A +// disabled or locked account is still returned fully populated, so only a +// missing user or a real database error leaves the stored value untouched. +func refreshUser(s *xorm.Session, u *user.User, cache map[int64]*user.User) { + if u == nil || u.ID == 0 { + return + } + + fresh, cached := cache[u.ID] + if !cached { + loaded, err := user.GetUserByID(s, u.ID) + if err != nil && !user.IsErrUserStatusError(err) { + cache[u.ID] = nil + return + } + fresh = loaded + cache[u.ID] = fresh + } + + if fresh != nil { + *u = *fresh + } +} diff --git a/pkg/models/notifications_refresh_test.go b/pkg/models/notifications_refresh_test.go new file mode 100644 index 000000000..b0cef2a43 --- /dev/null +++ b/pkg/models/notifications_refresh_test.go @@ -0,0 +1,109 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-present Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package models + +import ( + "encoding/json" + "testing" + + "code.vikunja.io/api/pkg/db" + "code.vikunja.io/api/pkg/notifications" + "code.vikunja.io/api/pkg/user" + + "github.com/stretchr/testify/require" + "xorm.io/xorm" +) + +// TestDatabaseNotifications_ReadAll_RefreshesUsers guards #2720 for notifications +// already in the database: those were serialized with a partial doer (id + +// username, no display Name), so reading them must reload the embedded users so +// the display name is shown. The fix in the dispatch path only helps new +// notifications; old rows are healed here at read time. +func TestDatabaseNotifications_ReadAll_RefreshesUsers(t *testing.T) { + t.Run("fills in the display name from the database", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + s := db.NewSession() + defer s.Close() + + // user12 has the display name "Name with spaces" in the fixtures. + insertStoredNotification(t, s, 1, &TaskAssignedNotification{ + Doer: &user.User{ID: 12, Username: "user12"}, + Assignee: &user.User{ID: 12, Username: "user12"}, + Task: &Task{ID: 1}, + }) + + got := readAssignedNotification(t, s, 1) + require.Equal(t, "Name with spaces", got.Doer.GetName()) + require.Equal(t, "Name with spaces", got.Assignee.GetName()) + }) + + t.Run("keeps the stored value when the user no longer exists", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + s := db.NewSession() + defer s.Close() + + insertStoredNotification(t, s, 1, &TaskAssignedNotification{ + Doer: &user.User{ID: 999999, Username: "ghost"}, + Task: &Task{ID: 1}, + }) + + got := readAssignedNotification(t, s, 1) + require.Equal(t, "ghost", got.Doer.Username) + }) + + t.Run("refreshes a disabled user", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + s := db.NewSession() + defer s.Close() + + // user17 is disabled in the fixtures; the reload must still win over the + // stale stored value. + insertStoredNotification(t, s, 1, &TaskAssignedNotification{ + Doer: &user.User{ID: 17, Username: "stale"}, + Task: &Task{ID: 1}, + }) + + got := readAssignedNotification(t, s, 1) + require.Equal(t, "user17", got.Doer.Username) + }) +} + +func insertStoredNotification(t *testing.T, s *xorm.Session, notifiableID int64, n notifications.Notification) { + t.Helper() + content, err := json.Marshal(n) + require.NoError(t, err) + _, err = s.Insert(¬ifications.DatabaseNotification{ + NotifiableID: notifiableID, + Notification: json.RawMessage(content), + Name: n.Name(), + }) + require.NoError(t, err) +} + +func readAssignedNotification(t *testing.T, s *xorm.Session, notifiableID int64) *TaskAssignedNotification { + t.Helper() + result, _, _, err := (&DatabaseNotifications{}).ReadAll(s, &user.User{ID: notifiableID}, "", 1, 50) + require.NoError(t, err) + + for _, dbn := range result.([]*notifications.DatabaseNotification) { + if n, is := dbn.Notification.(*TaskAssignedNotification); is { + return n + } + } + t.Fatal("no task.assigned notification was returned") + return nil +} From aac4dd845e6265fe0d4a3e4a4374f6545dff1fdd Mon Sep 17 00:00:00 2001 From: kolaente Date: Wed, 17 Jun 2026 22:47:38 +0200 Subject: [PATCH 52/53] refactor(notifications): refresh users via an explicit type switch Reflection over reflect.Kind was overkill: only top-level doer/assignee/ member fields are ever rendered, and the walk forced an exhaustive linter exclusion. List the user fields per notification type instead, which drops the reflect dependency and the .golangci.yml carve-out. --- .golangci.yml | 3 -- pkg/models/notifications_refresh.go | 69 +++++++++++------------------ 2 files changed, 27 insertions(+), 45 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 7c75f08de..19ee2f531 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -80,9 +80,6 @@ linters: - linters: - exhaustive path: pkg/models/task_collection_filter\.go - - linters: - - exhaustive - path: pkg/models/notifications_refresh\.go - linters: - gosec path: pkg/utils/random_string\.go diff --git a/pkg/models/notifications_refresh.go b/pkg/models/notifications_refresh.go index 81e8a9609..17af513f6 100644 --- a/pkg/models/notifications_refresh.go +++ b/pkg/models/notifications_refresh.go @@ -18,7 +18,6 @@ package models import ( "encoding/json" - "reflect" "code.vikunja.io/api/pkg/log" "code.vikunja.io/api/pkg/notifications" @@ -27,16 +26,12 @@ import ( "xorm.io/xorm" ) -// maxNotificationUserRefreshDepth bounds the reflection walk so an unexpectedly -// deep payload cannot recurse without end. -const maxNotificationUserRefreshDepth = 8 - -// refreshNotificationsUsers reloads every embedded user of each notification -// from the database. Notifications serialized before the acting user was -// resolved with its full profile (#2720) stored only id+username, so without -// this they keep rendering the auto-generated username instead of the display -// name. It runs at read time and is not persisted; one cache is shared across -// the batch so a user recurring across notifications is fetched only once. +// refreshNotificationsUsers reloads each notification's embedded users from the +// database. Notifications serialized before the acting user was resolved with +// its full profile (#2720) stored only id+username, so without this they keep +// rendering the auto-generated username instead of the display name. It runs at +// read time and is not persisted; one cache is shared across the batch so a +// user recurring across notifications is fetched only once. func refreshNotificationsUsers(s *xorm.Session, dbNotifications []*notifications.DatabaseNotification) { cache := make(map[int64]*user.User) for _, dbn := range dbNotifications { @@ -60,40 +55,30 @@ func refreshNotificationUsers(s *xorm.Session, dbn *notifications.DatabaseNotifi return } - refreshUsersInValue(s, reflect.ValueOf(typed), cache, 0) + for _, u := range notificationUsers(typed) { + refreshUser(s, u, cache) + } dbn.Notification = typed } -func refreshUsersInValue(s *xorm.Session, v reflect.Value, cache map[int64]*user.User, depth int) { - if depth > maxNotificationUserRefreshDepth || !v.IsValid() { - return - } - - switch v.Kind() { - case reflect.Ptr: - if v.IsNil() { - return - } - if u, is := v.Interface().(*user.User); is { - refreshUser(s, u, cache) - return - } - refreshUsersInValue(s, v.Elem(), cache, depth+1) - case reflect.Struct: - for i := 0; i < v.NumField(); i++ { - if !v.Type().Field(i).IsExported() { - continue - } - refreshUsersInValue(s, v.Field(i), cache, depth+1) - } - case reflect.Slice, reflect.Array: - for i := 0; i < v.Len(); i++ { - refreshUsersInValue(s, v.Index(i), cache, depth+1) - } - case reflect.Map: - for _, key := range v.MapKeys() { - refreshUsersInValue(s, v.MapIndex(key), cache, depth+1) - } +// notificationUsers returns the user fields a stored notification renders, so +// they can be reloaded. New notification types carrying a user belong here. +func notificationUsers(n notifications.Notification) []*user.User { + switch n := n.(type) { + case *TaskCommentNotification: + return []*user.User{n.Doer} + case *TaskAssignedNotification: + return []*user.User{n.Doer, n.Assignee} + case *TaskDeletedNotification: + return []*user.User{n.Doer} + case *ProjectCreatedNotification: + return []*user.User{n.Doer} + case *TeamMemberAddedNotification: + return []*user.User{n.Doer, n.Member} + case *UserMentionedInTaskNotification: + return []*user.User{n.Doer} + default: + return nil } } From 37a34cc5cf04a80a3d37b6e193c3c473a9b866ff Mon Sep 17 00:00:00 2001 From: kolaente Date: Wed, 17 Jun 2026 22:49:21 +0200 Subject: [PATCH 53/53] fix(notifications): log unexpected user refresh failures A transient database error while reloading a notification's user was swallowed silently, leaving stale names with no trace. Log everything except the expected "user was deleted" case. --- pkg/models/notifications_refresh.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/models/notifications_refresh.go b/pkg/models/notifications_refresh.go index 17af513f6..cc119aeae 100644 --- a/pkg/models/notifications_refresh.go +++ b/pkg/models/notifications_refresh.go @@ -94,6 +94,9 @@ func refreshUser(s *xorm.Session, u *user.User, cache map[int64]*user.User) { if !cached { loaded, err := user.GetUserByID(s, u.ID) if err != nil && !user.IsErrUserStatusError(err) { + if !user.IsErrUserDoesNotExist(err) { + log.Errorf("Could not refresh user %d for a notification: %v", u.ID, err) + } cache[u.ID] = nil return }