From a3421f3e9dbfe3a42fc8812cc31b1c6fafb1150b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 11:50:41 +0900 Subject: [PATCH 01/16] fix(deps): bump next to 16.3.4 and sharp override to 0.35.4 --- frontend/package.json | 4 +- frontend/pnpm-lock.yaml | 370 +++++++++++++++++++---------------- frontend/pnpm-workspace.yaml | 2 +- 3 files changed, 199 insertions(+), 177 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 191b7c90b..c86342aa1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -22,7 +22,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^1.27.0", - "next": "16.2.12", + "next": "16.3.4", "react": "19.2.8", "react-dom": "19.2.8", "react-resizable-panels": "^4.12.2", @@ -39,7 +39,7 @@ "@types/react-dom": "^19", "@vitest/coverage-v8": "4.1.10", "eslint": "^9", - "eslint-config-next": "16.2.12", + "eslint-config-next": "16.3.4", "fast-check": "^4.9.0", "jsdom": "^30.0.1", "postcss": "8.5.24", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 610a0e7ca..add535314 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -7,7 +7,7 @@ settings: overrides: brace-expansion: 5.0.9 postcss: 8.5.24 - sharp: 0.35.0 + sharp: 0.35.4 undici: 8.9.0 pnpmfileChecksum: sha256-RXPq3MmEdRb3xD3rhbER9kciz9nBr/i0J/uMUjql5t0= @@ -38,8 +38,8 @@ importers: specifier: ^1.27.0 version: 1.27.0(react@19.2.8) next: - specifier: 16.2.12 - version: 16.2.12(@babel/core@7.29.7)(@playwright/test@1.62.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + specifier: 16.3.4 + version: 16.3.4(@babel/core@7.29.7)(@playwright/test@1.62.0)(@types/node@26.1.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) react: specifier: 19.2.8 version: 19.2.8 @@ -84,8 +84,8 @@ importers: specifier: ^9 version: 9.39.5(jiti@2.7.0) eslint-config-next: - specifier: 16.2.12 - version: 16.2.12(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3) + specifier: 16.3.4 + version: 16.3.4(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3) fast-check: specifier: ^4.9.0 version: 4.9.0 @@ -289,6 +289,12 @@ packages: peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/regexpp@4.12.2': resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} @@ -369,160 +375,160 @@ packages: resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} engines: {node: '>=18'} - '@img/sharp-darwin-arm64@0.35.0': - resolution: {integrity: sha512-ZgaYEwaj+lx/5n4W8GmZ2IYz0PQHjN5eqRcfijWGB+2Aq7ZInZGa0qJyAn6DEtyLuWHRSrmWOqT9q3qqTBvmUQ==} + '@img/sharp-darwin-arm64@0.35.4': + resolution: {integrity: sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [darwin] - '@img/sharp-darwin-x64@0.35.0': - resolution: {integrity: sha512-c1z9LFpKB0slQW3RchwBE8iSVzGp70TNjUUO9k4BZwwW4HH7JBGHeIy4b+kk4n/kcBASb9evKCE3/7Slmslgiw==} + '@img/sharp-darwin-x64@0.35.4': + resolution: {integrity: sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==} engines: {node: '>=20.9.0'} cpu: [x64] os: [darwin] - '@img/sharp-freebsd-wasm32@0.35.0': - resolution: {integrity: sha512-Li2KTev0H90kEtnJHkI9xQojXt1AqWmFBMXiPw5kqd1jQgP7gi5HVK/qC5Rmh/59NuAwUuPzzPITmX22NomYYQ==} + '@img/sharp-freebsd-wasm32@0.35.4': + resolution: {integrity: sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==} engines: {node: '>=20.9.0'} os: [freebsd] - '@img/sharp-libvips-darwin-arm64@1.3.0': - resolution: {integrity: sha512-EKbmBKtyTH+GPFDRw2TgK2oV6hyxxlJVIar4hoTYSNmIwipgMFdxPQqR392GmfdsPGWga0mCFN1cCKjRb9cljw==} + '@img/sharp-libvips-darwin-arm64@1.3.3': + resolution: {integrity: sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==} cpu: [arm64] os: [darwin] - '@img/sharp-libvips-darwin-x64@1.3.0': - resolution: {integrity: sha512-Pl2OmOvrJ42adUllESxBsG54PfXLo1OYg9i3c5/5Ln/qJ0gZuTM9YMhQJPIbXqwidLRc/c2zuHt4RsrymmNv7A==} + '@img/sharp-libvips-darwin-x64@1.3.3': + resolution: {integrity: sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==} cpu: [x64] os: [darwin] - '@img/sharp-libvips-linux-arm64@1.3.0': - resolution: {integrity: sha512-C0SqjoFKnszqa44EQ7xoaT48nnO0lOyXEULfXMWi8krrjOPGYkeK30Okzla6ATbBYsyZ0ySinK0FVkpv3DwzfQ==} + '@img/sharp-libvips-linux-arm64@1.3.3': + resolution: {integrity: sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==} cpu: [arm64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-arm@1.3.0': - resolution: {integrity: sha512-A8UpHoUDW4DwnXoV6+q3C1s7QLRAHtPDEjWuNZjwHMyoCNZnm0GeNN8ls9f/bsEYTRQRW96C/n34XJQHJ2fT7A==} + '@img/sharp-libvips-linux-arm@1.3.3': + resolution: {integrity: sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==} cpu: [arm] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-ppc64@1.3.0': - resolution: {integrity: sha512-WOpkVxAjFd369iaIzEgNRreFD+gWdUMIGD5zplhNKNeqS6mm5dac3q2AFyCBmzYoAdouzZvRBgxy4z8QHZb4/A==} + '@img/sharp-libvips-linux-ppc64@1.3.3': + resolution: {integrity: sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==} cpu: [ppc64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-riscv64@1.3.0': - resolution: {integrity: sha512-DRWw0mOHusrCCuw2rqP87oLg6PGlkomVDFqw2hIwsSfwWpu4k3XLcBPaKKl6ct/GtL/cwNkgwjV/tc0Mqht3VA==} + '@img/sharp-libvips-linux-riscv64@1.3.3': + resolution: {integrity: sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==} cpu: [riscv64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-s390x@1.3.0': - resolution: {integrity: sha512-9APy+nFWhHS+kzLgWZfLcyrUd7YqnAQVa4BPOo4xkoHpdoktOAPG4cEr9+Jpl0TtqfVmcMJimNL5qNTyyOHZNA==} + '@img/sharp-libvips-linux-s390x@1.3.3': + resolution: {integrity: sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==} cpu: [s390x] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-x64@1.3.0': - resolution: {integrity: sha512-y9RNUYDe2A1UAdhLyfeOodGRszQdaEoe4nfOpp/sNVPl2CWIcUyFaDoCh4vPLPxu19803j2naLqZup2WxDXCLA==} + '@img/sharp-libvips-linux-x64@1.3.3': + resolution: {integrity: sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==} cpu: [x64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linuxmusl-arm64@1.3.0': - resolution: {integrity: sha512-cC1wkC0Mlucd0KSiGrLkJnB/ZqPvZCntc/Lk7ZnYO5ZSbF2euNek4Xvxafojq+wN1q/W0eprdpUIjUr/EV2PBg==} + '@img/sharp-libvips-linuxmusl-arm64@1.3.3': + resolution: {integrity: sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==} cpu: [arm64] os: [linux] libc: [musl] - '@img/sharp-libvips-linuxmusl-x64@1.3.0': - resolution: {integrity: sha512-LiYMhUZicB1QG//+RvmYZpXJO8fYRENfp+MZUCnG9aw+AKvGAy9gPaCnuwsPcBFs8EV66M0NNxj9VHcNklE8zw==} + '@img/sharp-libvips-linuxmusl-x64@1.3.3': + resolution: {integrity: sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==} cpu: [x64] os: [linux] libc: [musl] - '@img/sharp-linux-arm64@0.35.0': - resolution: {integrity: sha512-4+4XHLNT5wDT0roYlHTEmH9lDKt0acf9Tv+3hM3iceOirkxrR404/3WjAYZ9F9CkHrxeRcGLJXbi4vluMZ9O+A==} + '@img/sharp-linux-arm64@0.35.4': + resolution: {integrity: sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] libc: [glibc] - '@img/sharp-linux-arm@0.35.0': - resolution: {integrity: sha512-VVlpEWwizEFIOom0zdoeKuO5nuTswzVE5uHcBNvHzmeHUpNFajY3HFfbQ+zIH4E2kVaZ/yVxmsShW56TtEy4uA==} + '@img/sharp-linux-arm@0.35.4': + resolution: {integrity: sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==} engines: {node: '>=20.9.0'} cpu: [arm] os: [linux] libc: [glibc] - '@img/sharp-linux-ppc64@0.35.0': - resolution: {integrity: sha512-N3hzbEpUTJC8pWpPVJvgzGxM+so/MAXc8O2s/53B0LL9ZGpfXpME7Wizkc5d/8fRBlBtkDjzoZGDCqqNDHqLEw==} + '@img/sharp-linux-ppc64@0.35.4': + resolution: {integrity: sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==} engines: {node: '>=20.9.0'} cpu: [ppc64] os: [linux] libc: [glibc] - '@img/sharp-linux-riscv64@0.35.0': - resolution: {integrity: sha512-l6vmKVPnbS0RhVMbyxP5meAARsbhCnBN4fy31qz0+3a6Rv4jEqfzDrT89y6ZPkCi0AJGnwp2En528yXo401Hpw==} + '@img/sharp-linux-riscv64@0.35.4': + resolution: {integrity: sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==} engines: {node: '>=20.9.0'} cpu: [riscv64] os: [linux] libc: [glibc] - '@img/sharp-linux-s390x@0.35.0': - resolution: {integrity: sha512-MYlMiPFiv/EKPAHnp3yNZ9AAWFsxga9c5Bkc6wkar6bqzHLlkGVJHRm0u1ei+VXnZxp3Mz9MG9ZIsI8vSOf3sQ==} + '@img/sharp-linux-s390x@0.35.4': + resolution: {integrity: sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==} engines: {node: '>=20.9.0'} cpu: [s390x] os: [linux] libc: [glibc] - '@img/sharp-linux-x64@0.35.0': - resolution: {integrity: sha512-TYaItB5oj1ioXjhyn2xrR208vf+YuIIcHptQWRRaBmFhvIvL9D72DXN8w75xup0KXA8UdEAhQ9Qb2S49FD/9Cw==} + '@img/sharp-linux-x64@0.35.4': + resolution: {integrity: sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==} engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] libc: [glibc] - '@img/sharp-linuxmusl-arm64@0.35.0': - resolution: {integrity: sha512-DSTb6ijQzqe6DdAaOBVqJ/SYf1vO8EW5bK6X6LRXufEBebf2722VCdvBUtZ3rtV0x2ApfPNDy/p7LrrjaWjiyQ==} + '@img/sharp-linuxmusl-arm64@0.35.4': + resolution: {integrity: sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] libc: [musl] - '@img/sharp-linuxmusl-x64@0.35.0': - resolution: {integrity: sha512-K7ykQ+26Rt6+4BTU80AuGgTPIYX86UxiAKT4rcXX/WNTo7k1ZxpKz+TguHnwVpCqQK3B5PK0vZ0ZBe6nz/ib1w==} + '@img/sharp-linuxmusl-x64@0.35.4': + resolution: {integrity: sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==} engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] libc: [musl] - '@img/sharp-wasm32@0.35.0': - resolution: {integrity: sha512-9woLIFORERCr+6cWu87dQ22J34EExkhc73U1kZW0c+RclQqWetoodByp4dWZ/hN8/KVmTRAx2HOnUwib8AwZdA==} + '@img/sharp-wasm32@0.35.4': + resolution: {integrity: sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==} engines: {node: '>=20.9.0'} - '@img/sharp-webcontainers-wasm32@0.35.0': - resolution: {integrity: sha512-t+kie1TOyaDM6Dho+f+y0VqIUNhYQaKCUahuZVi0E0frgdiaOaPsDxDW3wfKacUdaNBCnK/ZDBMg33ydvHj8uA==} + '@img/sharp-webcontainers-wasm32@0.35.4': + resolution: {integrity: sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==} engines: {node: '>=20.9.0'} cpu: [wasm32] - '@img/sharp-win32-arm64@0.35.0': - resolution: {integrity: sha512-M5eKxug0dabbaWgFKvPa3odNs2OpaP+81NASfGKkt4GcYXpNhSu7CaeYxWkLNV6vHmUp4hnCxnxrUyhUJhXbKA==} + '@img/sharp-win32-arm64@0.35.4': + resolution: {integrity: sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [win32] - '@img/sharp-win32-ia32@0.35.0': - resolution: {integrity: sha512-z0+pZ03QCDvdVN0Ez9IX/yjWC19ikMlXrmdYMwYNLTh2BLPx3hXWPvyqWfquZ0BTO9O6GVOjIVoTcyyacMnWlQ==} + '@img/sharp-win32-ia32@0.35.4': + resolution: {integrity: sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==} engines: {node: ^20.9.0} cpu: [ia32] os: [win32] - '@img/sharp-win32-x64@0.35.0': - resolution: {integrity: sha512-feNnlz5ZHKr0MY1LPHvZQyJeBkbo4ctsn0D8FvA53VTw5TC63rfEL2UrWbkSBR19htSE7Mw78xYVwdJqoMWVHw==} + '@img/sharp-win32-x64@0.35.4': + resolution: {integrity: sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==} engines: {node: '>=20.9.0'} cpu: [x64] os: [win32] @@ -549,60 +555,60 @@ packages: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 - '@next/env@16.2.12': - resolution: {integrity: sha512-d0Z5Bc13Fa4nR8pFAKx2jay2yhJM16vlfHbTzYnUQAxlNb6B6lmn4hjt69lYNt4kRtyYP6gEM49lPRHNbIyneg==} + '@next/env@16.3.4': + resolution: {integrity: sha512-cjWZnUUa6jZq2kFaNe/ZyJdZonOZ/QoN0Zka2nz/FLOrfx14pQuM9c5RaSVkWMqgdt4ksgPAMWPyHSs/CyV48Q==} - '@next/eslint-plugin-next@16.2.12': - resolution: {integrity: sha512-uF2z/qAK2q7B5/6CpnFcBRX6jOq5iCO+Uqh1UkJhXljX1JwLarLYhhoJadO6dPb6moTprOKewMXheBcbIoSbug==} + '@next/eslint-plugin-next@16.3.4': + resolution: {integrity: sha512-szW9y2Aumu4z88YXfTzcFsgUAg2k64uzbtcO5L9f1AKS4w/GUKJcbFllRflROVyNPgJtGOnvNxiyp3v6b+prIA==} - '@next/swc-darwin-arm64@16.2.12': - resolution: {integrity: sha512-0W1R0teHWJrqKX0FH20IzzIWAOuGtBxPGuObrxy1lE8hQvCFj49KE8a3WUg0D7sq6rn6zkM4c7YGUnhudBS6oA==} + '@next/swc-darwin-arm64@16.3.4': + resolution: {integrity: sha512-iBr3I5LZNk5/bgl5//iTgD2tcym14MX0Xo7fD//u9dYAEgGzza1y9oywluPtf74YnOswVdH1908aK9xVz7zQTw==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@16.2.12': - resolution: {integrity: sha512-Hy5Ls099+aFUmOLmIgPfLqNi6iCwhL3uQCssz5rWk+5Nkc6TUKCE83DY5BbNylfm3+mfwcSFnLRfrZDJhVxdtw==} + '@next/swc-darwin-x64@16.3.4': + resolution: {integrity: sha512-2dpiSyl2Jw/NrBPaU2MAKGSa+2MR82pJIn4Sm5Rjr+gxAeuh0z158Su3Z2O8zn7UNNq+ej4bToed6RcRN/Lydg==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@16.2.12': - resolution: {integrity: sha512-+YqU2h1cQkHsGfvjAsrSmst8UIFBibBGm5x3Xgel8NLMiDQtNOM4sM2GOEMvG5YiOBNeN/Ykk8cQC2S0Xrqljg==} + '@next/swc-linux-arm64-gnu@16.3.4': + resolution: {integrity: sha512-+t+U8HZT+fApePCS5h89CSH3datz29MkzyfCn+6fpsZBG/oiEOhINcb9rtkv6sdpToLGFn2e6146NzaKCXkqrA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [glibc] - '@next/swc-linux-arm64-musl@16.2.12': - resolution: {integrity: sha512-0qjhiYBaKAqF63LA1ZWAAnKTzFUguAaZiRa5etMLGGPj/B6uEVjtIZldIzFEp3wHlB0koK6aTzqPtSdplTCjoA==} + '@next/swc-linux-arm64-musl@16.3.4': + resolution: {integrity: sha512-mx03GNs1ocQA5JQ4FxDMmIsNkdrZh8cuezKCrId28e5/gIPU/l7Kcy2+vmCCzdjnnmXJy+iOAu+7K0QppO6Urg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [musl] - '@next/swc-linux-x64-gnu@16.2.12': - resolution: {integrity: sha512-7A3q26W+h7gnA15uqBToNuDqBEFZZcqh0mW2mn4AJh/G5pdg2RVE3n4slzLEliASZFG3NmsbEzng/x2Sh09mBg==} + '@next/swc-linux-x64-gnu@16.3.4': + resolution: {integrity: sha512-YIhGY6fSMfha52bnVxnzc9zaVBzJg+cqQTOD8tXIBSx4fuv0pVMxQTE0PaS59YhnMOiYiG09IMwxJAf/CFm/Dw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [glibc] - '@next/swc-linux-x64-musl@16.2.12': - resolution: {integrity: sha512-qSjL/uppm+cbh21s72Ss8gkiOhQ4dExWHNGOWy6eZV7STj5WsKehgxT61beSsOj+YYQuTplL376lOCdMQU5T8w==} + '@next/swc-linux-x64-musl@16.3.4': + resolution: {integrity: sha512-+eaaX6axpDb0yF1GCpiERe6njplvdC+nks/fKfcHu3XPGRrald8P3/X7yv7QLdjA51knnxwl9pxdIJsg+w1L+Q==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [musl] - '@next/swc-win32-arm64-msvc@16.2.12': - resolution: {integrity: sha512-X6hzsOUJac/e7AWSbn9gQ9nzHld1xWP5iyjHpYWvud8pufB679O1xg4JDyKr8Xd69Jvd+kM2Der6uftiZCmjYA==} + '@next/swc-win32-arm64-msvc@16.3.4': + resolution: {integrity: sha512-0jcXW7Xs/uzICrmgV3MhDYDeRy++1CqnpDIerlPIqYO4bhzB4WNbX/aRnQclustsAyTkFKB0z6rbcjmNg5tR8A==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@16.2.12': - resolution: {integrity: sha512-F6fakeHuFTLOPt0bslQJdf+xtT+WIP9DVn/m4y1w1mRnVPyh3D/cNvzlRkxM444xfm+IvvYNSOrKiA2CDJ0Uxw==} + '@next/swc-win32-x64-msvc@16.3.4': + resolution: {integrity: sha512-vvBzwu1pYQCp92maZCFCIw/XgOTMR5tur9GjakwIo2cmwRTMKajRZZDS9+e4KsUZWKu1E007WUeAFXRRjZeuzw==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -893,8 +899,8 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@swc/helpers@0.5.15': - resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + '@swc/helpers@0.5.23': + resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} '@tailwindcss/node@4.3.3': resolution: {integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==} @@ -1529,8 +1535,8 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - eslint-config-next@16.2.12: - resolution: {integrity: sha512-iaaf4vvKo5h2LBdGt0JuRv7t0Ysqr9FMCiFxbptDg8LqOE//mIKR80DdpOnSVM7qjLH3jT8P0aFiwXxBEGZRXw==} + eslint-config-next@16.3.4: + resolution: {integrity: sha512-35/8RM10huEL9vlr8hUZMERMENHBrnyHN3ZZkF9efSgzGaqK34jIqry44A956//zriUhUAUW0XSkcolhrryqAA==} peerDependencies: eslint: '>=9.0.0' typescript: '>=3.3.1' @@ -1622,6 +1628,7 @@ packages: eslint@9.39.5: resolution: {integrity: sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true peerDependencies: jiti: '*' @@ -2259,8 +2266,8 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - next@16.2.12: - resolution: {integrity: sha512-iD59eYQWmbFcEbX7v/acG5DRym9iw1DdaPoD0WTA920naWsE25wShzJW4+UvAs8MK9EC2kBfIH6vtto1H1PHGw==} + next@16.3.4: + resolution: {integrity: sha512-/Ztf6CeRH+ejEXUrYtqI4gkS66eFIHuSwqi60RgcpWKodxFZx2/dqVCMKBwILfAHXQ+F1b1vAudgj3mnxqtoIA==} engines: {node: '>=20.9.0'} hasBin: true peerDependencies: @@ -2504,9 +2511,14 @@ packages: resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} engines: {node: '>= 0.4'} - sharp@0.35.0: - resolution: {integrity: sha512-BqvG5XbwPZ4NV0DK90d86leEECMsoa8bO0nqnKWlBDYxri4GJ7c4EDInaF6q20lTh/mATmnDIKWJFfXnoVfH5g==} + sharp@0.35.4: + resolution: {integrity: sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==} engines: {node: '>=20.9.0'} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} @@ -3130,6 +3142,11 @@ snapshots: eslint: 9.39.5(jiti@2.7.0) eslint-visitor-keys: 3.4.3 + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.5(jiti@2.7.0))': + dependencies: + eslint: 9.39.5(jiti@2.7.0) + eslint-visitor-keys: 3.4.3 + '@eslint-community/regexpp@4.12.2': {} '@eslint/config-array@0.21.2': @@ -3209,108 +3226,108 @@ snapshots: '@img/colour@1.1.0': optional: true - '@img/sharp-darwin-arm64@0.35.0': + '@img/sharp-darwin-arm64@0.35.4': optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.3.0 + '@img/sharp-libvips-darwin-arm64': 1.3.3 optional: true - '@img/sharp-darwin-x64@0.35.0': + '@img/sharp-darwin-x64@0.35.4': optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.3.0 + '@img/sharp-libvips-darwin-x64': 1.3.3 optional: true - '@img/sharp-freebsd-wasm32@0.35.0': + '@img/sharp-freebsd-wasm32@0.35.4': dependencies: - '@img/sharp-wasm32': 0.35.0 + '@img/sharp-wasm32': 0.35.4 optional: true - '@img/sharp-libvips-darwin-arm64@1.3.0': + '@img/sharp-libvips-darwin-arm64@1.3.3': optional: true - '@img/sharp-libvips-darwin-x64@1.3.0': + '@img/sharp-libvips-darwin-x64@1.3.3': optional: true - '@img/sharp-libvips-linux-arm64@1.3.0': + '@img/sharp-libvips-linux-arm64@1.3.3': optional: true - '@img/sharp-libvips-linux-arm@1.3.0': + '@img/sharp-libvips-linux-arm@1.3.3': optional: true - '@img/sharp-libvips-linux-ppc64@1.3.0': + '@img/sharp-libvips-linux-ppc64@1.3.3': optional: true - '@img/sharp-libvips-linux-riscv64@1.3.0': + '@img/sharp-libvips-linux-riscv64@1.3.3': optional: true - '@img/sharp-libvips-linux-s390x@1.3.0': + '@img/sharp-libvips-linux-s390x@1.3.3': optional: true - '@img/sharp-libvips-linux-x64@1.3.0': + '@img/sharp-libvips-linux-x64@1.3.3': optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.3.0': + '@img/sharp-libvips-linuxmusl-arm64@1.3.3': optional: true - '@img/sharp-libvips-linuxmusl-x64@1.3.0': + '@img/sharp-libvips-linuxmusl-x64@1.3.3': optional: true - '@img/sharp-linux-arm64@0.35.0': + '@img/sharp-linux-arm64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.3.0 + '@img/sharp-libvips-linux-arm64': 1.3.3 optional: true - '@img/sharp-linux-arm@0.35.0': + '@img/sharp-linux-arm@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.3.0 + '@img/sharp-libvips-linux-arm': 1.3.3 optional: true - '@img/sharp-linux-ppc64@0.35.0': + '@img/sharp-linux-ppc64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.3.0 + '@img/sharp-libvips-linux-ppc64': 1.3.3 optional: true - '@img/sharp-linux-riscv64@0.35.0': + '@img/sharp-linux-riscv64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-riscv64': 1.3.0 + '@img/sharp-libvips-linux-riscv64': 1.3.3 optional: true - '@img/sharp-linux-s390x@0.35.0': + '@img/sharp-linux-s390x@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.3.0 + '@img/sharp-libvips-linux-s390x': 1.3.3 optional: true - '@img/sharp-linux-x64@0.35.0': + '@img/sharp-linux-x64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.3.0 + '@img/sharp-libvips-linux-x64': 1.3.3 optional: true - '@img/sharp-linuxmusl-arm64@0.35.0': + '@img/sharp-linuxmusl-arm64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.3.0 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.3 optional: true - '@img/sharp-linuxmusl-x64@0.35.0': + '@img/sharp-linuxmusl-x64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.3.0 + '@img/sharp-libvips-linuxmusl-x64': 1.3.3 optional: true - '@img/sharp-wasm32@0.35.0': + '@img/sharp-wasm32@0.35.4': dependencies: '@emnapi/runtime': 1.11.3 optional: true - '@img/sharp-webcontainers-wasm32@0.35.0': + '@img/sharp-webcontainers-wasm32@0.35.4': dependencies: - '@img/sharp-wasm32': 0.35.0 + '@img/sharp-wasm32': 0.35.4 optional: true - '@img/sharp-win32-arm64@0.35.0': + '@img/sharp-win32-arm64@0.35.4': optional: true - '@img/sharp-win32-ia32@0.35.0': + '@img/sharp-win32-ia32@0.35.4': optional: true - '@img/sharp-win32-x64@0.35.0': + '@img/sharp-win32-x64@0.35.4': optional: true '@jridgewell/gen-mapping@0.3.13': @@ -3346,34 +3363,37 @@ snapshots: '@tybys/wasm-util': 0.10.3 optional: true - '@next/env@16.2.12': {} + '@next/env@16.3.4': {} - '@next/eslint-plugin-next@16.2.12': + '@next/eslint-plugin-next@16.3.4(eslint@9.39.5(jiti@2.7.0))': dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.5(jiti@2.7.0)) fast-glob: 3.3.1 + transitivePeerDependencies: + - eslint - '@next/swc-darwin-arm64@16.2.12': + '@next/swc-darwin-arm64@16.3.4': optional: true - '@next/swc-darwin-x64@16.2.12': + '@next/swc-darwin-x64@16.3.4': optional: true - '@next/swc-linux-arm64-gnu@16.2.12': + '@next/swc-linux-arm64-gnu@16.3.4': optional: true - '@next/swc-linux-arm64-musl@16.2.12': + '@next/swc-linux-arm64-musl@16.3.4': optional: true - '@next/swc-linux-x64-gnu@16.2.12': + '@next/swc-linux-x64-gnu@16.3.4': optional: true - '@next/swc-linux-x64-musl@16.2.12': + '@next/swc-linux-x64-musl@16.3.4': optional: true - '@next/swc-win32-arm64-msvc@16.2.12': + '@next/swc-win32-arm64-msvc@16.3.4': optional: true - '@next/swc-win32-x64-msvc@16.2.12': + '@next/swc-win32-x64-msvc@16.3.4': optional: true '@nodelib/fs.scandir@2.1.5': @@ -3584,7 +3604,7 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@swc/helpers@0.5.15': + '@swc/helpers@0.5.23': dependencies: tslib: 2.8.1 @@ -4287,9 +4307,9 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-next@16.2.12(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3): + eslint-config-next@16.3.4(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3): dependencies: - '@next/eslint-plugin-next': 16.2.12 + '@next/eslint-plugin-next': 16.3.4(eslint@9.39.5(jiti@2.7.0)) eslint: 9.39.5(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0)))(eslint@9.39.5(jiti@2.7.0)) @@ -5055,10 +5075,10 @@ snapshots: natural-compare@1.4.0: {} - next@16.2.12(@babel/core@7.29.7)(@playwright/test@1.62.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + next@16.3.4(@babel/core@7.29.7)(@playwright/test@1.62.0)(@types/node@26.1.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: - '@next/env': 16.2.12 - '@swc/helpers': 0.5.15 + '@next/env': 16.3.4 + '@swc/helpers': 0.5.23 baseline-browser-mapping: 2.11.5 caniuse-lite: 1.0.30001806 postcss: 8.5.24 @@ -5066,18 +5086,19 @@ snapshots: react-dom: 19.2.8(react@19.2.8) styled-jsx: 5.1.6(@babel/core@7.29.7)(react@19.2.8) optionalDependencies: - '@next/swc-darwin-arm64': 16.2.12 - '@next/swc-darwin-x64': 16.2.12 - '@next/swc-linux-arm64-gnu': 16.2.12 - '@next/swc-linux-arm64-musl': 16.2.12 - '@next/swc-linux-x64-gnu': 16.2.12 - '@next/swc-linux-x64-musl': 16.2.12 - '@next/swc-win32-arm64-msvc': 16.2.12 - '@next/swc-win32-x64-msvc': 16.2.12 + '@next/swc-darwin-arm64': 16.3.4 + '@next/swc-darwin-x64': 16.3.4 + '@next/swc-linux-arm64-gnu': 16.3.4 + '@next/swc-linux-arm64-musl': 16.3.4 + '@next/swc-linux-x64-gnu': 16.3.4 + '@next/swc-linux-x64-musl': 16.3.4 + '@next/swc-win32-arm64-msvc': 16.3.4 + '@next/swc-win32-x64-msvc': 16.3.4 '@playwright/test': 1.62.0 - sharp: 0.35.0 + sharp: 0.35.4(@types/node@26.1.2) transitivePeerDependencies: - '@babel/core' + - '@types/node' - babel-plugin-macros node-exports-info@1.6.2: @@ -5338,37 +5359,38 @@ snapshots: es-errors: 1.3.0 es-object-atoms: 1.1.2 - sharp@0.35.0: + sharp@0.35.4(@types/node@26.1.2): dependencies: '@img/colour': 1.1.0 detect-libc: 2.1.2 semver: 7.8.5 optionalDependencies: - '@img/sharp-darwin-arm64': 0.35.0 - '@img/sharp-darwin-x64': 0.35.0 - '@img/sharp-freebsd-wasm32': 0.35.0 - '@img/sharp-libvips-darwin-arm64': 1.3.0 - '@img/sharp-libvips-darwin-x64': 1.3.0 - '@img/sharp-libvips-linux-arm': 1.3.0 - '@img/sharp-libvips-linux-arm64': 1.3.0 - '@img/sharp-libvips-linux-ppc64': 1.3.0 - '@img/sharp-libvips-linux-riscv64': 1.3.0 - '@img/sharp-libvips-linux-s390x': 1.3.0 - '@img/sharp-libvips-linux-x64': 1.3.0 - '@img/sharp-libvips-linuxmusl-arm64': 1.3.0 - '@img/sharp-libvips-linuxmusl-x64': 1.3.0 - '@img/sharp-linux-arm': 0.35.0 - '@img/sharp-linux-arm64': 0.35.0 - '@img/sharp-linux-ppc64': 0.35.0 - '@img/sharp-linux-riscv64': 0.35.0 - '@img/sharp-linux-s390x': 0.35.0 - '@img/sharp-linux-x64': 0.35.0 - '@img/sharp-linuxmusl-arm64': 0.35.0 - '@img/sharp-linuxmusl-x64': 0.35.0 - '@img/sharp-webcontainers-wasm32': 0.35.0 - '@img/sharp-win32-arm64': 0.35.0 - '@img/sharp-win32-ia32': 0.35.0 - '@img/sharp-win32-x64': 0.35.0 + '@img/sharp-darwin-arm64': 0.35.4 + '@img/sharp-darwin-x64': 0.35.4 + '@img/sharp-freebsd-wasm32': 0.35.4 + '@img/sharp-libvips-darwin-arm64': 1.3.3 + '@img/sharp-libvips-darwin-x64': 1.3.3 + '@img/sharp-libvips-linux-arm': 1.3.3 + '@img/sharp-libvips-linux-arm64': 1.3.3 + '@img/sharp-libvips-linux-ppc64': 1.3.3 + '@img/sharp-libvips-linux-riscv64': 1.3.3 + '@img/sharp-libvips-linux-s390x': 1.3.3 + '@img/sharp-libvips-linux-x64': 1.3.3 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.3 + '@img/sharp-libvips-linuxmusl-x64': 1.3.3 + '@img/sharp-linux-arm': 0.35.4 + '@img/sharp-linux-arm64': 0.35.4 + '@img/sharp-linux-ppc64': 0.35.4 + '@img/sharp-linux-riscv64': 0.35.4 + '@img/sharp-linux-s390x': 0.35.4 + '@img/sharp-linux-x64': 0.35.4 + '@img/sharp-linuxmusl-arm64': 0.35.4 + '@img/sharp-linuxmusl-x64': 0.35.4 + '@img/sharp-webcontainers-wasm32': 0.35.4 + '@img/sharp-win32-arm64': 0.35.4 + '@img/sharp-win32-ia32': 0.35.4 + '@img/sharp-win32-x64': 0.35.4 + '@types/node': 26.1.2 optional: true shebang-command@2.0.0: diff --git a/frontend/pnpm-workspace.yaml b/frontend/pnpm-workspace.yaml index d028031d2..784b361c1 100644 --- a/frontend/pnpm-workspace.yaml +++ b/frontend/pnpm-workspace.yaml @@ -16,7 +16,7 @@ supportedArchitectures: overrides: brace-expansion: "5.0.9" postcss: "8.5.24" - sharp: "0.35.0" + sharp: "0.35.4" undici: 8.9.0 patchedDependencies: From d76f7b450888d42040ca090ed9b9924e08f01881 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 11:56:07 +0900 Subject: [PATCH 02/16] test(security): lock frontend patched dependency floors --- .../test_frontend_framework_security_floor.py | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 backend/tests/test_frontend_framework_security_floor.py diff --git a/backend/tests/test_frontend_framework_security_floor.py b/backend/tests/test_frontend_framework_security_floor.py new file mode 100644 index 000000000..4130e8e7a --- /dev/null +++ b/backend/tests/test_frontend_framework_security_floor.py @@ -0,0 +1,52 @@ +"""Fail closed when frontend framework/image dependencies regress below patched floors.""" + +from __future__ import annotations + +import json +import re +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[2] +FRONTEND_ROOT = REPO_ROOT / "frontend" + + +def _exact_version(value: str) -> tuple[int, int, int]: + """Return a three-part exact version, rejecting ranges and prereleases.""" + + match = re.fullmatch(r"(\d+)\.(\d+)\.(\d+)", value) + assert match is not None, f"expected exact semantic version, got {value!r}" + return tuple(int(part) for part in match.groups()) + + +def test_frontend_framework_and_image_security_floors() -> None: + """Keep Next.js and sharp at releases containing the reviewed security fixes.""" + + package = json.loads((FRONTEND_ROOT / "package.json").read_text(encoding="utf-8")) + next_value = package["dependencies"]["next"] + eslint_next_value = package["devDependencies"]["eslint-config-next"] + + assert _exact_version(next_value) >= (16, 3, 3), ( + "Next.js must include the fixes for CVE-2026-75604 and " + "GHSA-2xp9-vwfh-vxw4" + ) + assert eslint_next_value == next_value, ( + "eslint-config-next must stay on the same reviewed release as Next.js" + ) + + workspace = (FRONTEND_ROOT / "pnpm-workspace.yaml").read_text(encoding="utf-8") + sharp_match = re.search( + r"(?m)^\s{2}sharp:\s*[\"']?(\d+\.\d+\.\d+)[\"']?\s*$", + workspace, + ) + assert sharp_match is not None, "pnpm workspace must keep an explicit sharp override" + sharp_value = sharp_match.group(1) + assert _exact_version(sharp_value) >= (0, 35, 4), ( + "sharp must include the fix for GHSA-rgj7-g3m4-5g8c" + ) + + lock = (FRONTEND_ROOT / "pnpm-lock.yaml").read_text(encoding="utf-8") + assert f"next@{next_value}" in lock, "lockfile must resolve the reviewed Next.js release" + assert f"sharp@{sharp_value}" in lock, "lockfile must resolve the reviewed sharp release" + assert "next@16.3.1" not in lock, "vulnerable Next.js 16.3.1 must not remain locked" + assert "sharp@0.35.0" not in lock, "vulnerable sharp 0.35.0 must not remain locked" From a5abe047d318477b64c62cdbee5b737aad8054f0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 12:43:03 +0900 Subject: [PATCH 03/16] test(deps): reproduce weak frontend lock validation --- .../test_frontend_framework_security_floor.py | 79 ++++++++++++++++--- 1 file changed, 68 insertions(+), 11 deletions(-) diff --git a/backend/tests/test_frontend_framework_security_floor.py b/backend/tests/test_frontend_framework_security_floor.py index 4130e8e7a..19ce8f7f2 100644 --- a/backend/tests/test_frontend_framework_security_floor.py +++ b/backend/tests/test_frontend_framework_security_floor.py @@ -6,6 +6,9 @@ import re from pathlib import Path +import pytest +import yaml + REPO_ROOT = Path(__file__).resolve().parents[2] FRONTEND_ROOT = REPO_ROOT / "frontend" @@ -19,6 +22,25 @@ def _exact_version(value: str) -> tuple[int, int, int]: return tuple(int(part) for part in match.groups()) +def _assert_weak_lock_text_contract( + lock_text: str, next_value: str, sharp_value: str +) -> None: + """Preserve the predecessor lock checks while stronger regressions are RED.""" + + assert f"next@{next_value}" in lock_text, ( + "lockfile must resolve the reviewed Next.js release" + ) + assert f"sharp@{sharp_value}" in lock_text, ( + "lockfile must resolve the reviewed sharp release" + ) + assert "next@16.3.1" not in lock_text, ( + "vulnerable Next.js 16.3.1 must not remain locked" + ) + assert "sharp@0.35.0" not in lock_text, ( + "vulnerable sharp 0.35.0 must not remain locked" + ) + + def test_frontend_framework_and_image_security_floors() -> None: """Keep Next.js and sharp at releases containing the reviewed security fixes.""" @@ -34,19 +56,54 @@ def test_frontend_framework_and_image_security_floors() -> None: "eslint-config-next must stay on the same reviewed release as Next.js" ) - workspace = (FRONTEND_ROOT / "pnpm-workspace.yaml").read_text(encoding="utf-8") - sharp_match = re.search( - r"(?m)^\s{2}sharp:\s*[\"']?(\d+\.\d+\.\d+)[\"']?\s*$", - workspace, + workspace = yaml.safe_load( + (FRONTEND_ROOT / "pnpm-workspace.yaml").read_text(encoding="utf-8") ) - assert sharp_match is not None, "pnpm workspace must keep an explicit sharp override" - sharp_value = sharp_match.group(1) + sharp_value = str(workspace["overrides"]["sharp"]) assert _exact_version(sharp_value) >= (0, 35, 4), ( "sharp must include the fix for GHSA-rgj7-g3m4-5g8c" ) - lock = (FRONTEND_ROOT / "pnpm-lock.yaml").read_text(encoding="utf-8") - assert f"next@{next_value}" in lock, "lockfile must resolve the reviewed Next.js release" - assert f"sharp@{sharp_value}" in lock, "lockfile must resolve the reviewed sharp release" - assert "next@16.3.1" not in lock, "vulnerable Next.js 16.3.1 must not remain locked" - assert "sharp@0.35.0" not in lock, "vulnerable sharp 0.35.0 must not remain locked" + lock_text = (FRONTEND_ROOT / "pnpm-lock.yaml").read_text(encoding="utf-8") + _assert_weak_lock_text_contract(lock_text, next_value, sharp_value) + + +def test_security_floor_rejects_importer_drift() -> None: + """Reject a partially regenerated lock whose root importer drifts below the floor.""" + + package = json.loads((FRONTEND_ROOT / "package.json").read_text(encoding="utf-8")) + next_value = package["dependencies"]["next"] + workspace = yaml.safe_load( + (FRONTEND_ROOT / "pnpm-workspace.yaml").read_text(encoding="utf-8") + ) + sharp_value = str(workspace["overrides"]["sharp"]) + lock = yaml.safe_load( + (FRONTEND_ROOT / "pnpm-lock.yaml").read_text(encoding="utf-8") + ) + lock["importers"]["."]["dependencies"]["next"]["specifier"] = "16.3.2" + + with pytest.raises(AssertionError): + _assert_weak_lock_text_contract( + yaml.safe_dump(lock, sort_keys=False), next_value, sharp_value + ) + + +def test_security_floor_rejects_any_below_floor_lock_entry() -> None: + """Reject stale vulnerable package/snapshot entries, not only two known literals.""" + + package = json.loads((FRONTEND_ROOT / "package.json").read_text(encoding="utf-8")) + next_value = package["dependencies"]["next"] + workspace = yaml.safe_load( + (FRONTEND_ROOT / "pnpm-workspace.yaml").read_text(encoding="utf-8") + ) + sharp_value = str(workspace["overrides"]["sharp"]) + lock = yaml.safe_load( + (FRONTEND_ROOT / "pnpm-lock.yaml").read_text(encoding="utf-8") + ) + lock["packages"]["next@16.3.2"] = {} + lock["snapshots"]["sharp@0.35.3"] = {} + + with pytest.raises(AssertionError): + _assert_weak_lock_text_contract( + yaml.safe_dump(lock, sort_keys=False), next_value, sharp_value + ) From e8a54fc5156ac3ffbb79bc8418c5125d7dcdea60 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 12:43:37 +0900 Subject: [PATCH 04/16] fix(deps): validate frontend lock security floors structurally --- .../test_frontend_framework_security_floor.py | 165 ++++++++++++------ 1 file changed, 111 insertions(+), 54 deletions(-) diff --git a/backend/tests/test_frontend_framework_security_floor.py b/backend/tests/test_frontend_framework_security_floor.py index 19ce8f7f2..bf318b5ad 100644 --- a/backend/tests/test_frontend_framework_security_floor.py +++ b/backend/tests/test_frontend_framework_security_floor.py @@ -5,6 +5,7 @@ import json import re from pathlib import Path +from typing import Any import pytest import yaml @@ -12,6 +13,8 @@ REPO_ROOT = Path(__file__).resolve().parents[2] FRONTEND_ROOT = REPO_ROOT / "frontend" +NEXT_SECURITY_FLOOR = (16, 3, 3) +SHARP_SECURITY_FLOOR = (0, 35, 4) def _exact_version(value: str) -> tuple[int, int, int]: @@ -22,88 +25,142 @@ def _exact_version(value: str) -> tuple[int, int, int]: return tuple(int(part) for part in match.groups()) -def _assert_weak_lock_text_contract( - lock_text: str, next_value: str, sharp_value: str +def _resolved_version(value: str) -> tuple[int, int, int]: + """Return the exact version prefix from a pnpm peer-qualified resolution.""" + + version = value.split("(", 1)[0] + return _exact_version(version) + + +def _package_key_version(package_key: str, package_name: str) -> tuple[int, int, int]: + """Return the version encoded by one pnpm package/snapshot key.""" + + prefix = f"{package_name}@" + assert package_key.startswith(prefix), ( + f"expected {package_name!r} lock key, got {package_key!r}" + ) + return _resolved_version(package_key[len(prefix) :]) + + +def _assert_lock_contract( + lock: dict[str, Any], + next_value: str, + eslint_next_value: str, + sharp_value: str, ) -> None: - """Preserve the predecessor lock checks while stronger regressions are RED.""" + """Validate root resolution identity and every locked Next.js/sharp security floor.""" - assert f"next@{next_value}" in lock_text, ( - "lockfile must resolve the reviewed Next.js release" + importer = lock["importers"]["."] + next_import = importer["dependencies"]["next"] + assert next_import["specifier"] == next_value, ( + "root importer must preserve the package.json Next.js specifier" + ) + assert _resolved_version(str(next_import["version"])) == _exact_version(next_value), ( + "root importer must resolve the reviewed Next.js release" ) - assert f"sharp@{sharp_value}" in lock_text, ( - "lockfile must resolve the reviewed sharp release" + assert f"next@{next_import['version']}" in lock["snapshots"], ( + "root importer Next.js resolution must reference an existing snapshot" ) - assert "next@16.3.1" not in lock_text, ( - "vulnerable Next.js 16.3.1 must not remain locked" + + eslint_next_import = importer["devDependencies"]["eslint-config-next"] + assert eslint_next_import["specifier"] == eslint_next_value, ( + "root importer must preserve the eslint-config-next specifier" ) - assert "sharp@0.35.0" not in lock_text, ( - "vulnerable sharp 0.35.0 must not remain locked" + assert _resolved_version(str(eslint_next_import["version"])) == _exact_version( + eslint_next_value + ), "root importer must resolve the reviewed eslint-config-next release" + assert f"eslint-config-next@{eslint_next_import['version']}" in lock["snapshots"], ( + "root importer eslint-config-next resolution must reference an existing snapshot" ) + assert str(lock["overrides"]["sharp"]) == sharp_value, ( + "lockfile sharp override must match the reviewed workspace override" + ) -def test_frontend_framework_and_image_security_floors() -> None: - """Keep Next.js and sharp at releases containing the reviewed security fixes.""" + expected_next = _exact_version(next_value) + expected_sharp = _exact_version(sharp_value) + for section_name in ("packages", "snapshots"): + section = lock[section_name] + next_keys = [key for key in section if key.startswith("next@")] + sharp_keys = [key for key in section if key.startswith("sharp@")] + + assert next_keys, f"{section_name} must contain a Next.js resolution" + assert sharp_keys, f"{section_name} must contain a sharp resolution" + assert any( + _package_key_version(key, "next") == expected_next for key in next_keys + ), f"{section_name} must contain the reviewed Next.js release" + assert any( + _package_key_version(key, "sharp") == expected_sharp for key in sharp_keys + ), f"{section_name} must contain the reviewed sharp release" + + for package_key in next_keys: + assert _package_key_version(package_key, "next") >= NEXT_SECURITY_FLOOR, ( + f"{section_name} contains Next.js below the reviewed security floor: " + f"{package_key}" + ) + for package_key in sharp_keys: + assert _package_key_version(package_key, "sharp") >= SHARP_SECURITY_FLOOR, ( + f"{section_name} contains sharp below the reviewed security floor: " + f"{package_key}" + ) + + +def _frontend_security_inputs() -> tuple[str, str, str, dict[str, Any]]: + """Load the manifest, workspace override, and generated lock contract.""" package = json.loads((FRONTEND_ROOT / "package.json").read_text(encoding="utf-8")) next_value = package["dependencies"]["next"] eslint_next_value = package["devDependencies"]["eslint-config-next"] + workspace = yaml.safe_load( + (FRONTEND_ROOT / "pnpm-workspace.yaml").read_text(encoding="utf-8") + ) + sharp_value = str(workspace["overrides"]["sharp"]) + lock = yaml.safe_load( + (FRONTEND_ROOT / "pnpm-lock.yaml").read_text(encoding="utf-8") + ) + return next_value, eslint_next_value, sharp_value, lock - assert _exact_version(next_value) >= (16, 3, 3), ( + +def test_frontend_framework_and_image_security_floors() -> None: + """Keep manifests and every generated lock resolution at reviewed patched releases.""" + + next_value, eslint_next_value, sharp_value, lock = _frontend_security_inputs() + + assert _exact_version(next_value) >= NEXT_SECURITY_FLOOR, ( "Next.js must include the fixes for CVE-2026-75604 and " "GHSA-2xp9-vwfh-vxw4" ) assert eslint_next_value == next_value, ( "eslint-config-next must stay on the same reviewed release as Next.js" ) - - workspace = yaml.safe_load( - (FRONTEND_ROOT / "pnpm-workspace.yaml").read_text(encoding="utf-8") - ) - sharp_value = str(workspace["overrides"]["sharp"]) - assert _exact_version(sharp_value) >= (0, 35, 4), ( + assert _exact_version(sharp_value) >= SHARP_SECURITY_FLOOR, ( "sharp must include the fix for GHSA-rgj7-g3m4-5g8c" ) + _assert_lock_contract(lock, next_value, eslint_next_value, sharp_value) - lock_text = (FRONTEND_ROOT / "pnpm-lock.yaml").read_text(encoding="utf-8") - _assert_weak_lock_text_contract(lock_text, next_value, sharp_value) +@pytest.mark.parametrize("field", ["specifier", "version"]) +def test_security_floor_rejects_root_importer_drift(field: str) -> None: + """Reject a partially regenerated lock whose root Next.js importer drifts.""" -def test_security_floor_rejects_importer_drift() -> None: - """Reject a partially regenerated lock whose root importer drifts below the floor.""" - - package = json.loads((FRONTEND_ROOT / "package.json").read_text(encoding="utf-8")) - next_value = package["dependencies"]["next"] - workspace = yaml.safe_load( - (FRONTEND_ROOT / "pnpm-workspace.yaml").read_text(encoding="utf-8") - ) - sharp_value = str(workspace["overrides"]["sharp"]) - lock = yaml.safe_load( - (FRONTEND_ROOT / "pnpm-lock.yaml").read_text(encoding="utf-8") - ) - lock["importers"]["."]["dependencies"]["next"]["specifier"] = "16.3.2" + next_value, eslint_next_value, sharp_value, lock = _frontend_security_inputs() + lock["importers"]["."]["dependencies"]["next"][field] = "16.3.2" with pytest.raises(AssertionError): - _assert_weak_lock_text_contract( - yaml.safe_dump(lock, sort_keys=False), next_value, sharp_value - ) + _assert_lock_contract(lock, next_value, eslint_next_value, sharp_value) -def test_security_floor_rejects_any_below_floor_lock_entry() -> None: - """Reject stale vulnerable package/snapshot entries, not only two known literals.""" +@pytest.mark.parametrize( + ("section_name", "package_key"), + [("packages", "next@16.3.2"), ("snapshots", "sharp@0.35.3")], +) +def test_security_floor_rejects_every_below_floor_lock_entry( + section_name: str, package_key: str +) -> None: + """Reject any stale vulnerable Next.js or sharp package/snapshot entry.""" - package = json.loads((FRONTEND_ROOT / "package.json").read_text(encoding="utf-8")) - next_value = package["dependencies"]["next"] - workspace = yaml.safe_load( - (FRONTEND_ROOT / "pnpm-workspace.yaml").read_text(encoding="utf-8") - ) - sharp_value = str(workspace["overrides"]["sharp"]) - lock = yaml.safe_load( - (FRONTEND_ROOT / "pnpm-lock.yaml").read_text(encoding="utf-8") - ) - lock["packages"]["next@16.3.2"] = {} - lock["snapshots"]["sharp@0.35.3"] = {} + next_value, eslint_next_value, sharp_value, lock = _frontend_security_inputs() + lock[section_name][package_key] = {} with pytest.raises(AssertionError): - _assert_weak_lock_text_contract( - yaml.safe_dump(lock, sort_keys=False), next_value, sharp_value - ) + _assert_lock_contract(lock, next_value, eslint_next_value, sharp_value) From b97f42f16f2dfedccc1e98812c88117132ef8126 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 17:37:41 +0900 Subject: [PATCH 05/16] fix(deps): raise frontend audit security floors --- AGENTS.md | 6 + CLAUDE.md | 3 + .../test_frontend_framework_security_floor.py | 38 +++++ frontend/package.json | 4 +- frontend/pnpm-lock.yaml | 139 +++++++++--------- frontend/pnpm-workspace.yaml | 1 + 6 files changed, 120 insertions(+), 71 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 9104dd1f4..0557febfd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -130,6 +130,12 @@ in this repo. them as source can exhaust model context before security evidence finalizes. - Prefer upgrading or removing vulnerable dependencies over downgrading patched packages unless compatibility evidence is recorded in the PR. +- Dependency-audit results are time-sensitive. Re-run the exact-head audit + immediately before push; a newly published advisory is a source repair, not + a reason to reuse an older clean receipt. After regenerating a pnpm lockfile, + require a warning-free frozen install and `pnpm peers check`. Do not adopt an + unsupported major or silence peer ranges merely to remove a deprecation + notice. - OpenCode Review, Strix Security Scan, and PR Review Merge Scheduler are provided by ContextualWisdomLab central required workflows in `ContextualWisdomLab/.github`; do not reintroduce repo-local copies of diff --git a/CLAUDE.md b/CLAUDE.md index be67bc80c..22227847c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -174,5 +174,8 @@ Next.js frontend ──> FastAPI backend (control plane) ──> Postgres + pgve - Release bumps keep `VERSION`, `CHANGELOG.md`, `frontend/package.json`, and FastAPI app metadata synchronized; the backend reads its version from `VERSION`. +- Treat dependency audits as exact-head, time-sensitive evidence. After pnpm + lock regeneration, verify a warning-free frozen install and peer check; do + not hide unsupported-major peer conflicts to clear a deprecation notice. - When a review uncovers a recurring bug pattern, record the anti-pattern in `AGENTS.md` and update the affected tests/mocks/docs in the same PR. diff --git a/backend/tests/test_frontend_framework_security_floor.py b/backend/tests/test_frontend_framework_security_floor.py index bf318b5ad..5593b2304 100644 --- a/backend/tests/test_frontend_framework_security_floor.py +++ b/backend/tests/test_frontend_framework_security_floor.py @@ -15,6 +15,8 @@ FRONTEND_ROOT = REPO_ROOT / "frontend" NEXT_SECURITY_FLOOR = (16, 3, 3) SHARP_SECURITY_FLOOR = (0, 35, 4) +JS_YAML_SECURITY_FLOOR = (4, 3, 2) +VITEST_SECURITY_FLOOR = (4, 1, 11) def _exact_version(value: str) -> tuple[int, int, int]: @@ -139,6 +141,42 @@ def test_frontend_framework_and_image_security_floors() -> None: _assert_lock_contract(lock, next_value, eslint_next_value, sharp_value) +def test_js_yaml_security_floor_covers_every_lock_resolution() -> None: + """Keep every js-yaml resolution above the reviewed denial-of-service floor.""" + + lock = yaml.safe_load( + (FRONTEND_ROOT / "pnpm-lock.yaml").read_text(encoding="utf-8") + ) + for section_name in ("packages", "snapshots"): + js_yaml_keys = [ + key for key in lock[section_name] if key.startswith("js-yaml@") + ] + for package_key in js_yaml_keys: + assert ( + _package_key_version(package_key, "js-yaml") + >= JS_YAML_SECURITY_FLOOR + ), f"{section_name} contains js-yaml below the reviewed security floor" + + +def test_vitest_security_floor_covers_manifest_and_lock() -> None: + """Keep Vitest and its coverage package above the reviewed traversal floor.""" + + package = json.loads((FRONTEND_ROOT / "package.json").read_text(encoding="utf-8")) + lock = yaml.safe_load( + (FRONTEND_ROOT / "pnpm-lock.yaml").read_text(encoding="utf-8") + ) + for package_name in ("vitest", "@vitest/coverage-v8"): + declared_value = package["devDependencies"][package_name] + assert _exact_version(declared_value) >= VITEST_SECURITY_FLOOR + for section_name in ("packages", "snapshots"): + for package_key in lock[section_name]: + if package_key.startswith(f"{package_name}@"): + assert ( + _package_key_version(package_key, package_name) + >= VITEST_SECURITY_FLOOR + ), f"{section_name} contains {package_name} below the reviewed floor" + + @pytest.mark.parametrize("field", ["specifier", "version"]) def test_security_floor_rejects_root_importer_drift(field: str) -> None: """Reject a partially regenerated lock whose root Next.js importer drifts.""" diff --git a/frontend/package.json b/frontend/package.json index c86342aa1..f902cecd2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -37,14 +37,14 @@ "@types/node": "^26", "@types/react": "^19", "@types/react-dom": "^19", - "@vitest/coverage-v8": "4.1.10", + "@vitest/coverage-v8": "4.1.11", "eslint": "^9", "eslint-config-next": "16.3.4", "fast-check": "^4.9.0", "jsdom": "^30.0.1", "postcss": "8.5.24", "typescript": "^6", - "vitest": "^4.1.10" + "vitest": "4.1.11" }, "overrides": { "brace-expansion": "5.0.9", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index add535314..58377eb5f 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -6,6 +6,7 @@ settings: overrides: brace-expansion: 5.0.9 + js-yaml: 4.3.2 postcss: 8.5.24 sharp: 0.35.4 undici: 8.9.0 @@ -78,8 +79,8 @@ importers: specifier: ^19 version: 19.2.3(@types/react@19.2.17) '@vitest/coverage-v8': - specifier: 4.1.10 - version: 4.1.10(vitest@4.1.10) + specifier: 4.1.11 + version: 4.1.11(vitest@4.1.11) eslint: specifier: ^9 version: 9.39.5(jiti@2.7.0) @@ -99,8 +100,8 @@ importers: specifier: ^6 version: 6.0.3 vitest: - specifier: ^4.1.10 - version: 4.1.10(@types/node@26.1.2)(@vitest/coverage-v8@4.1.10)(jsdom@30.0.1)(vite@8.1.4(@types/node@26.1.2)(jiti@2.7.0)) + specifier: 4.1.11 + version: 4.1.11(@types/node@26.1.2)(@vitest/coverage-v8@4.1.11)(jsdom@30.0.1)(vite@8.1.4(@types/node@26.1.2)(jiti@2.7.0)) packages: @@ -311,8 +312,8 @@ packages: resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/eslintrc@3.3.6': - resolution: {integrity: sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==} + '@eslint/eslintrc@3.3.7': + resolution: {integrity: sha512-F42g89Qd5oAWtp0k0nnSrjziAKza7w8SVT4mStc18LZMaRb4J1HQAHLCalEtDCxrTuksx7NU9qsmeLwpOfPqWw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/js@9.39.5': @@ -1205,20 +1206,20 @@ packages: cpu: [x64] os: [win32] - '@vitest/coverage-v8@4.1.10': - resolution: {integrity: sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==} + '@vitest/coverage-v8@4.1.11': + resolution: {integrity: sha512-8MVGEFnJIcdGjcbfKmeq8z0pZHH0JlVtoVZH9Q/qwUp6wyFnEJUBMrw9DCaj+ra3vShGmhavjalMIhPNxZAUcw==} peerDependencies: - '@vitest/browser': 4.1.10 - vitest: 4.1.10 + '@vitest/browser': 4.1.11 + vitest: 4.1.11 peerDependenciesMeta: '@vitest/browser': optional: true - '@vitest/expect@4.1.10': - resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} + '@vitest/expect@4.1.11': + resolution: {integrity: sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==} - '@vitest/mocker@4.1.10': - resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==} + '@vitest/mocker@4.1.11': + resolution: {integrity: sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==} peerDependencies: msw: ^2.4.9 vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -1228,20 +1229,20 @@ packages: vite: optional: true - '@vitest/pretty-format@4.1.10': - resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} + '@vitest/pretty-format@4.1.11': + resolution: {integrity: sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==} - '@vitest/runner@4.1.10': - resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==} + '@vitest/runner@4.1.11': + resolution: {integrity: sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==} - '@vitest/snapshot@4.1.10': - resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==} + '@vitest/snapshot@4.1.11': + resolution: {integrity: sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==} - '@vitest/spy@4.1.10': - resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} + '@vitest/spy@4.1.11': + resolution: {integrity: sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==} - '@vitest/utils@4.1.10': - resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} + '@vitest/utils@4.1.11': + resolution: {integrity: sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==} acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} @@ -1987,8 +1988,8 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-yaml@4.3.0: - resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} + js-yaml@4.3.2: + resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==} hasBin: true jsdom@30.0.1: @@ -2808,20 +2809,20 @@ packages: yaml: optional: true - vitest@4.1.10: - resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} + vitest@4.1.11: + resolution: {integrity: sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.10 - '@vitest/browser-preview': 4.1.10 - '@vitest/browser-webdriverio': 4.1.10 - '@vitest/coverage-istanbul': 4.1.10 - '@vitest/coverage-v8': 4.1.10 - '@vitest/ui': 4.1.10 + '@vitest/browser-playwright': 4.1.11 + '@vitest/browser-preview': 4.1.11 + '@vitest/browser-webdriverio': 4.1.11 + '@vitest/coverage-istanbul': 4.1.11 + '@vitest/coverage-v8': 4.1.11 + '@vitest/ui': 4.1.11 happy-dom: '*' jsdom: '*' vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -3165,7 +3166,7 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/eslintrc@3.3.6': + '@eslint/eslintrc@3.3.7': dependencies: ajv: 6.15.0 debug: 4.4.3 @@ -3173,7 +3174,7 @@ snapshots: globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.1 - js-yaml: 4.3.0 + js-yaml: 4.3.2 minimatch: 3.1.5(patch_hash=5f38b9c5382c1163b0389810f5e4e867519096f3c11a6df0a51d7cafbdfa93e2) strip-json-comments: 3.1.1 transitivePeerDependencies: @@ -3870,10 +3871,10 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.12.2': optional: true - '@vitest/coverage-v8@4.1.10(vitest@4.1.10)': + '@vitest/coverage-v8@4.1.11(vitest@4.1.11)': dependencies: '@bcoe/v8-coverage': 1.0.2 - '@vitest/utils': 4.1.10 + '@vitest/utils': 4.1.11 ast-v8-to-istanbul: 1.0.4 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 @@ -3882,46 +3883,46 @@ snapshots: obug: 2.1.3 std-env: 4.2.0 tinyrainbow: 3.1.0 - vitest: 4.1.10(@types/node@26.1.2)(@vitest/coverage-v8@4.1.10)(jsdom@30.0.1)(vite@8.1.4(@types/node@26.1.2)(jiti@2.7.0)) + vitest: 4.1.11(@types/node@26.1.2)(@vitest/coverage-v8@4.1.11)(jsdom@30.0.1)(vite@8.1.4(@types/node@26.1.2)(jiti@2.7.0)) - '@vitest/expect@4.1.10': + '@vitest/expect@4.1.11': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@vitest/spy': 4.1.10 - '@vitest/utils': 4.1.10 + '@vitest/spy': 4.1.11 + '@vitest/utils': 4.1.11 chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.10(vite@8.1.4(@types/node@26.1.2)(jiti@2.7.0))': + '@vitest/mocker@4.1.11(vite@8.1.4(@types/node@26.1.2)(jiti@2.7.0))': dependencies: - '@vitest/spy': 4.1.10 + '@vitest/spy': 4.1.11 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: vite: 8.1.4(@types/node@26.1.2)(jiti@2.7.0) - '@vitest/pretty-format@4.1.10': + '@vitest/pretty-format@4.1.11': dependencies: tinyrainbow: 3.1.0 - '@vitest/runner@4.1.10': + '@vitest/runner@4.1.11': dependencies: - '@vitest/utils': 4.1.10 + '@vitest/utils': 4.1.11 pathe: 2.0.3 - '@vitest/snapshot@4.1.10': + '@vitest/snapshot@4.1.11': dependencies: - '@vitest/pretty-format': 4.1.10 - '@vitest/utils': 4.1.10 + '@vitest/pretty-format': 4.1.11 + '@vitest/utils': 4.1.11 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@4.1.10': {} + '@vitest/spy@4.1.11': {} - '@vitest/utils@4.1.10': + '@vitest/utils@4.1.11': dependencies: - '@vitest/pretty-format': 4.1.10 + '@vitest/pretty-format': 4.1.11 convert-source-map: 2.0.0 tinyrainbow: 3.1.0 @@ -4312,7 +4313,7 @@ snapshots: '@next/eslint-plugin-next': 16.3.4(eslint@9.39.5(jiti@2.7.0)) eslint: 9.39.5(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0)))(eslint@9.39.5(jiti@2.7.0)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@2.7.0)) eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.5(jiti@2.7.0)) eslint-plugin-react: 7.37.5(eslint@9.39.5(jiti@2.7.0)) @@ -4335,7 +4336,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0)))(eslint@9.39.5(jiti@2.7.0)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@2.7.0)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -4350,14 +4351,14 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.14.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0)))(eslint@9.39.5(jiti@2.7.0)))(eslint@9.39.5(jiti@2.7.0)): + eslint-module-utils@2.14.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3) eslint: 9.39.5(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0)))(eslint@9.39.5(jiti@2.7.0)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@2.7.0)) transitivePeerDependencies: - supports-color @@ -4372,7 +4373,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.5(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 - eslint-module-utils: 2.14.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0)))(eslint@9.39.5(jiti@2.7.0)))(eslint@9.39.5(jiti@2.7.0)) + eslint-module-utils: 2.14.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)) hasown: 2.0.4 is-core-module: 2.16.2 is-glob: 4.0.3 @@ -4460,7 +4461,7 @@ snapshots: '@eslint/config-array': 0.21.2 '@eslint/config-helpers': 0.4.2 '@eslint/core': 0.17.0 - '@eslint/eslintrc': 3.3.6 + '@eslint/eslintrc': 3.3.7 '@eslint/js': 9.39.5 '@eslint/plugin-kit': 0.4.1 '@humanfs/node': 0.16.8 @@ -4846,7 +4847,7 @@ snapshots: js-tokens@4.0.0: {} - js-yaml@4.3.0: + js-yaml@4.3.2: dependencies: argparse: 2.0.1 @@ -5697,15 +5698,15 @@ snapshots: fsevents: 2.3.3 jiti: 2.7.0 - vitest@4.1.10(@types/node@26.1.2)(@vitest/coverage-v8@4.1.10)(jsdom@30.0.1)(vite@8.1.4(@types/node@26.1.2)(jiti@2.7.0)): + vitest@4.1.11(@types/node@26.1.2)(@vitest/coverage-v8@4.1.11)(jsdom@30.0.1)(vite@8.1.4(@types/node@26.1.2)(jiti@2.7.0)): dependencies: - '@vitest/expect': 4.1.10 - '@vitest/mocker': 4.1.10(vite@8.1.4(@types/node@26.1.2)(jiti@2.7.0)) - '@vitest/pretty-format': 4.1.10 - '@vitest/runner': 4.1.10 - '@vitest/snapshot': 4.1.10 - '@vitest/spy': 4.1.10 - '@vitest/utils': 4.1.10 + '@vitest/expect': 4.1.11 + '@vitest/mocker': 4.1.11(vite@8.1.4(@types/node@26.1.2)(jiti@2.7.0)) + '@vitest/pretty-format': 4.1.11 + '@vitest/runner': 4.1.11 + '@vitest/snapshot': 4.1.11 + '@vitest/spy': 4.1.11 + '@vitest/utils': 4.1.11 es-module-lexer: 2.3.0 expect-type: 1.4.0 magic-string: 0.30.21 @@ -5721,7 +5722,7 @@ snapshots: why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 26.1.2 - '@vitest/coverage-v8': 4.1.10(vitest@4.1.10) + '@vitest/coverage-v8': 4.1.11(vitest@4.1.11) jsdom: 30.0.1 transitivePeerDependencies: - msw diff --git a/frontend/pnpm-workspace.yaml b/frontend/pnpm-workspace.yaml index 784b361c1..70f2c4eea 100644 --- a/frontend/pnpm-workspace.yaml +++ b/frontend/pnpm-workspace.yaml @@ -15,6 +15,7 @@ supportedArchitectures: overrides: brace-expansion: "5.0.9" + js-yaml: "4.3.2" postcss: "8.5.24" sharp: "0.35.4" undici: 8.9.0 From 09cb87a25e59b0b5e737f915f77b404cafe245ab Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 17:44:15 +0900 Subject: [PATCH 06/16] test(deps): bind js-yaml owner succession structurally Preserve the concurrent generated 4.3.2/Vitest repair, remove AGENTS/CLAUDE writes from the dependency lane, and port the older js-yaml owner's useful workspace/lock/ESLint-consumer assertions into the protected-base security stack. This keeps one source owner without dropping the prior contract. --- AGENTS.md | 6 --- CLAUDE.md | 3 -- .../tests/test_js_yaml_dependency_security.py | 50 +++++++++++++++++++ 3 files changed, 50 insertions(+), 9 deletions(-) create mode 100644 backend/tests/test_js_yaml_dependency_security.py diff --git a/AGENTS.md b/AGENTS.md index 0557febfd..9104dd1f4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -130,12 +130,6 @@ in this repo. them as source can exhaust model context before security evidence finalizes. - Prefer upgrading or removing vulnerable dependencies over downgrading patched packages unless compatibility evidence is recorded in the PR. -- Dependency-audit results are time-sensitive. Re-run the exact-head audit - immediately before push; a newly published advisory is a source repair, not - a reason to reuse an older clean receipt. After regenerating a pnpm lockfile, - require a warning-free frozen install and `pnpm peers check`. Do not adopt an - unsupported major or silence peer ranges merely to remove a deprecation - notice. - OpenCode Review, Strix Security Scan, and PR Review Merge Scheduler are provided by ContextualWisdomLab central required workflows in `ContextualWisdomLab/.github`; do not reintroduce repo-local copies of diff --git a/CLAUDE.md b/CLAUDE.md index 22227847c..be67bc80c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -174,8 +174,5 @@ Next.js frontend ──> FastAPI backend (control plane) ──> Postgres + pgve - Release bumps keep `VERSION`, `CHANGELOG.md`, `frontend/package.json`, and FastAPI app metadata synchronized; the backend reads its version from `VERSION`. -- Treat dependency audits as exact-head, time-sensitive evidence. After pnpm - lock regeneration, verify a warning-free frozen install and peer check; do - not hide unsupported-major peer conflicts to clear a deprecation notice. - When a review uncovers a recurring bug pattern, record the anti-pattern in `AGENTS.md` and update the affected tests/mocks/docs in the same PR. diff --git a/backend/tests/test_js_yaml_dependency_security.py b/backend/tests/test_js_yaml_dependency_security.py new file mode 100644 index 000000000..11f1bc3ad --- /dev/null +++ b/backend/tests/test_js_yaml_dependency_security.py @@ -0,0 +1,50 @@ +"""Keep the generated frontend dependency graph on the reviewed js-yaml floor.""" + +from pathlib import Path + +import yaml + + +FRONTEND_ROOT = Path(__file__).resolve().parents[2] / "frontend" +JS_YAML_PATCHED_RELEASE = "4.3.2" + + +def _resolved_version(package_key: str) -> tuple[int, int, int]: + """Return the semantic version from one peer-qualified js-yaml lock key.""" + + prefix = "js-yaml@" + assert package_key.startswith(prefix) + version = package_key[len(prefix) :].split("(", 1)[0] + return tuple(int(part) for part in version.split(".")) + + +def test_js_yaml_override_lock_and_eslint_consumer_share_patched_release() -> None: + """Bind workspace policy, generated lock identity, and the ESLint consumer together.""" + + workspace = yaml.safe_load( + (FRONTEND_ROOT / "pnpm-workspace.yaml").read_text(encoding="utf-8") + ) + lock = yaml.safe_load( + (FRONTEND_ROOT / "pnpm-lock.yaml").read_text(encoding="utf-8") + ) + + assert str(workspace["overrides"]["js-yaml"]) == JS_YAML_PATCHED_RELEASE + assert str(lock["overrides"]["js-yaml"]) == JS_YAML_PATCHED_RELEASE + + floor = (4, 3, 2) + for section_name in ("packages", "snapshots"): + keys = [key for key in lock[section_name] if key.startswith("js-yaml@")] + assert keys, f"{section_name} must contain a js-yaml resolution" + assert {_resolved_version(key) for key in keys} == {floor} + + eslint_snapshots = [ + value + for key, value in lock["snapshots"].items() + if key.startswith("@eslint/eslintrc@") + ] + assert eslint_snapshots, "lock must retain the ESLint configuration snapshot" + assert any( + str(snapshot.get("dependencies", {}).get("js-yaml")) + == JS_YAML_PATCHED_RELEASE + for snapshot in eslint_snapshots + ), "ESLint must consume the reviewed js-yaml release" From a6715c9c845ed5ad48929fd3261bb07ce4b802e5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 04:41:41 +0900 Subject: [PATCH 07/16] test(deps): reproduce missing Vitest lock-entry gap --- .../test_frontend_framework_security_floor.py | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/backend/tests/test_frontend_framework_security_floor.py b/backend/tests/test_frontend_framework_security_floor.py index 5593b2304..f64edfe4a 100644 --- a/backend/tests/test_frontend_framework_security_floor.py +++ b/backend/tests/test_frontend_framework_security_floor.py @@ -177,6 +177,39 @@ def test_vitest_security_floor_covers_manifest_and_lock() -> None: ), f"{section_name} contains {package_name} below the reviewed floor" +@pytest.mark.parametrize("package_name", ["vitest", "@vitest/coverage-v8"]) +@pytest.mark.parametrize("section_name", ["packages", "snapshots"]) +def test_vitest_security_floor_rejects_missing_lock_resolution( + monkeypatch: pytest.MonkeyPatch, + package_name: str, + section_name: str, +) -> None: + """Reject a regenerated lock section that drops an expected Vitest resolution.""" + + package_text = (FRONTEND_ROOT / "package.json").read_text(encoding="utf-8") + lock = yaml.safe_load( + (FRONTEND_ROOT / "pnpm-lock.yaml").read_text(encoding="utf-8") + ) + lock[section_name] = { + key: value + for key, value in lock[section_name].items() + if not key.startswith(f"{package_name}@") + } + lock_text = yaml.safe_dump(lock) + original_read_text = Path.read_text + + def _read_text(path: Path, *args: Any, **kwargs: Any) -> str: + if path == FRONTEND_ROOT / "package.json": + return package_text + if path == FRONTEND_ROOT / "pnpm-lock.yaml": + return lock_text + return original_read_text(path, *args, **kwargs) + + monkeypatch.setattr(Path, "read_text", _read_text) + with pytest.raises(AssertionError): + test_vitest_security_floor_covers_manifest_and_lock() + + @pytest.mark.parametrize("field", ["specifier", "version"]) def test_security_floor_rejects_root_importer_drift(field: str) -> None: """Reject a partially regenerated lock whose root Next.js importer drifts.""" From 21897d8ccd11ee4a2d7a3df5551db12c92c9ec71 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 04:45:28 +0900 Subject: [PATCH 08/16] test(deps): require Vitest lock resolutions --- .../test_frontend_framework_security_floor.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/backend/tests/test_frontend_framework_security_floor.py b/backend/tests/test_frontend_framework_security_floor.py index f64edfe4a..8c4457571 100644 --- a/backend/tests/test_frontend_framework_security_floor.py +++ b/backend/tests/test_frontend_framework_security_floor.py @@ -169,12 +169,19 @@ def test_vitest_security_floor_covers_manifest_and_lock() -> None: declared_value = package["devDependencies"][package_name] assert _exact_version(declared_value) >= VITEST_SECURITY_FLOOR for section_name in ("packages", "snapshots"): - for package_key in lock[section_name]: - if package_key.startswith(f"{package_name}@"): - assert ( - _package_key_version(package_key, package_name) - >= VITEST_SECURITY_FLOOR - ), f"{section_name} contains {package_name} below the reviewed floor" + package_keys = [ + package_key + for package_key in lock[section_name] + if package_key.startswith(f"{package_name}@") + ] + assert package_keys, ( + f"{section_name} must contain a {package_name} resolution" + ) + for package_key in package_keys: + assert ( + _package_key_version(package_key, package_name) + >= VITEST_SECURITY_FLOOR + ), f"{section_name} contains {package_name} below the reviewed floor" @pytest.mark.parametrize("package_name", ["vitest", "@vitest/coverage-v8"]) From 15fecaaeabe6faaedf7e4c3f8991eb3add1dd0c5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 05:15:48 +0900 Subject: [PATCH 09/16] test(security): reproduce Vitest importer drift --- .../test_frontend_framework_security_floor.py | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/backend/tests/test_frontend_framework_security_floor.py b/backend/tests/test_frontend_framework_security_floor.py index 8c4457571..4d352fcb0 100644 --- a/backend/tests/test_frontend_framework_security_floor.py +++ b/backend/tests/test_frontend_framework_security_floor.py @@ -242,3 +242,64 @@ def test_security_floor_rejects_every_below_floor_lock_entry( with pytest.raises(AssertionError): _assert_lock_contract(lock, next_value, eslint_next_value, sharp_value) + + +@pytest.mark.parametrize("package_name", ["vitest", "@vitest/coverage-v8"]) +@pytest.mark.parametrize("field", ["specifier", "version"]) +def test_vitest_security_floor_rejects_root_importer_drift( + monkeypatch: pytest.MonkeyPatch, + package_name: str, + field: str, +) -> None: + """Reject a root Vitest importer that no longer matches the reviewed manifest.""" + + package_text = (FRONTEND_ROOT / "package.json").read_text(encoding="utf-8") + lock = yaml.safe_load( + (FRONTEND_ROOT / "pnpm-lock.yaml").read_text(encoding="utf-8") + ) + lock["importers"]["."]["devDependencies"][package_name][field] = "4.1.12" + lock_text = yaml.safe_dump(lock) + original_read_text = Path.read_text + + def _read_text(path: Path, *args: Any, **kwargs: Any) -> str: + if path == FRONTEND_ROOT / "package.json": + return package_text + if path == FRONTEND_ROOT / "pnpm-lock.yaml": + return lock_text + return original_read_text(path, *args, **kwargs) + + monkeypatch.setattr(Path, "read_text", _read_text) + with pytest.raises(AssertionError): + test_vitest_security_floor_covers_manifest_and_lock() + + +@pytest.mark.parametrize("package_name", ["vitest", "@vitest/coverage-v8"]) +def test_vitest_security_floor_rejects_missing_root_snapshot( + monkeypatch: pytest.MonkeyPatch, + package_name: str, +) -> None: + """Reject a root Vitest resolution whose exact peer-qualified snapshot vanished.""" + + package_text = (FRONTEND_ROOT / "package.json").read_text(encoding="utf-8") + lock = yaml.safe_load( + (FRONTEND_ROOT / "pnpm-lock.yaml").read_text(encoding="utf-8") + ) + resolution = str( + lock["importers"]["."]["devDependencies"][package_name]["version"] + ) + snapshot_key = f"{package_name}@{resolution}" + snapshot = lock["snapshots"].pop(snapshot_key) + lock["snapshots"][f"{package_name}@4.1.12"] = snapshot + lock_text = yaml.safe_dump(lock) + original_read_text = Path.read_text + + def _read_text(path: Path, *args: Any, **kwargs: Any) -> str: + if path == FRONTEND_ROOT / "package.json": + return package_text + if path == FRONTEND_ROOT / "pnpm-lock.yaml": + return lock_text + return original_read_text(path, *args, **kwargs) + + monkeypatch.setattr(Path, "read_text", _read_text) + with pytest.raises(AssertionError): + test_vitest_security_floor_covers_manifest_and_lock() From 17a7618eda2b212b691f08fa936e042b34258fc9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 05:18:45 +0900 Subject: [PATCH 10/16] test(security): bind Vitest importer contract --- .../tests/test_frontend_framework_security_floor.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/backend/tests/test_frontend_framework_security_floor.py b/backend/tests/test_frontend_framework_security_floor.py index 4d352fcb0..c1a7e2b9b 100644 --- a/backend/tests/test_frontend_framework_security_floor.py +++ b/backend/tests/test_frontend_framework_security_floor.py @@ -165,9 +165,20 @@ def test_vitest_security_floor_covers_manifest_and_lock() -> None: lock = yaml.safe_load( (FRONTEND_ROOT / "pnpm-lock.yaml").read_text(encoding="utf-8") ) + importer = lock["importers"]["."]["devDependencies"] for package_name in ("vitest", "@vitest/coverage-v8"): declared_value = package["devDependencies"][package_name] assert _exact_version(declared_value) >= VITEST_SECURITY_FLOOR + importer_entry = importer[package_name] + assert importer_entry["specifier"] == declared_value, ( + f"root importer must preserve the package.json {package_name} specifier" + ) + assert _resolved_version(str(importer_entry["version"])) == _exact_version( + declared_value + ), f"root importer must resolve the reviewed {package_name} release" + assert f"{package_name}@{importer_entry['version']}" in lock["snapshots"], ( + f"root importer {package_name} resolution must reference an existing snapshot" + ) for section_name in ("packages", "snapshots"): package_keys = [ package_key From 54871419193734918e79c89b54c65761f04ae095 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 19:22:52 +0900 Subject: [PATCH 11/16] fix(deps): bind Vitest importer package records Reject a lock that retains a different patched package record while losing the importer-resolved base record. Signed-off-by: Seongho Bae --- AGENTS.md | 4 ++ CLAUDE.md | 5 +++ .../test_frontend_framework_security_floor.py | 37 +++++++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 9104dd1f4..0d0fd6abc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -678,6 +678,10 @@ in this repo. backend/tests/test_release_governance.py backend/tests/test_runtime_config_api.py -q`, `corepack pnpm@11.5.3 --dir frontend test --runInBand` when frontend behavior changes, and a Docker build of the affected image. +- A pnpm importer entry is only valid when both records it names exist: the + base-version key in `packages` and the complete peer-qualified key in + `snapshots`. Security-floor tests must reject a lock that retains another + compliant version while dropping the importer's own base package record. - GHCR publishing evidence for the combined `naruon` image must include the exact image name, tag, local image ID, push result, and registry verification from GitHub Packages or an equivalent manifest/API query. Publish the package diff --git a/CLAUDE.md b/CLAUDE.md index be67bc80c..197a38766 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -55,6 +55,11 @@ pnpm run dev npm run test:e2e -- tests/e2e/dashboard-branding.spec.ts # Playwright (set LIVE_BASE_URL for live stacks) ``` +For pnpm security-floor checks, bind each root importer to its base-version +`packages` entry and its complete peer-qualified `snapshots` entry. A different +patched version elsewhere in the lockfile is not evidence for the importer's +declared resolution. + ### Whole-repo verification ```bash diff --git a/backend/tests/test_frontend_framework_security_floor.py b/backend/tests/test_frontend_framework_security_floor.py index c1a7e2b9b..e200c5ead 100644 --- a/backend/tests/test_frontend_framework_security_floor.py +++ b/backend/tests/test_frontend_framework_security_floor.py @@ -176,6 +176,11 @@ def test_vitest_security_floor_covers_manifest_and_lock() -> None: assert _resolved_version(str(importer_entry["version"])) == _exact_version( declared_value ), f"root importer must resolve the reviewed {package_name} release" + resolved_version = str(importer_entry["version"]) + base_version = resolved_version.split("(", 1)[0] + assert f"{package_name}@{base_version}" in lock["packages"], ( + f"root importer {package_name} resolution must reference an existing package record" + ) assert f"{package_name}@{importer_entry['version']}" in lock["snapshots"], ( f"root importer {package_name} resolution must reference an existing snapshot" ) @@ -314,3 +319,35 @@ def _read_text(path: Path, *args: Any, **kwargs: Any) -> str: monkeypatch.setattr(Path, "read_text", _read_text) with pytest.raises(AssertionError): test_vitest_security_floor_covers_manifest_and_lock() + + +@pytest.mark.parametrize("package_name", ["vitest", "@vitest/coverage-v8"]) +def test_vitest_security_floor_rejects_missing_root_package( + monkeypatch: pytest.MonkeyPatch, + package_name: str, +) -> None: + """Reject a root Vitest resolution whose base package record vanished.""" + + package_text = (FRONTEND_ROOT / "package.json").read_text(encoding="utf-8") + lock = yaml.safe_load( + (FRONTEND_ROOT / "pnpm-lock.yaml").read_text(encoding="utf-8") + ) + resolution = str( + lock["importers"]["."]["devDependencies"][package_name]["version"] + ) + package_key = f"{package_name}@{resolution.split('(', 1)[0]}" + package_record = lock["packages"].pop(package_key) + lock["packages"][f"{package_name}@4.1.12"] = package_record + lock_text = yaml.safe_dump(lock) + original_read_text = Path.read_text + + def _read_text(path: Path, *args: Any, **kwargs: Any) -> str: + if path == FRONTEND_ROOT / "package.json": + return package_text + if path == FRONTEND_ROOT / "pnpm-lock.yaml": + return lock_text + return original_read_text(path, *args, **kwargs) + + monkeypatch.setattr(Path, "read_text", _read_text) + with pytest.raises(AssertionError): + test_vitest_security_floor_covers_manifest_and_lock() From 4e66036b6257a03369f690eb0dba942bc260b284 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 19:27:40 +0900 Subject: [PATCH 12/16] docs(gap): record Vitest lockfile repair evidence Keep the focused local result distinct from hosted checks, review, merge, and deployment evidence. Signed-off-by: Seongho Bae --- docs/product-technical-gap-baseline.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 98bc17d2a..3b4256943 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -158,6 +158,18 @@ Windows-separator traversal protection passed 130 focused parser/import tests. All three have recreated hosted Checks and remain normal protected-merge candidates; no hosted pass or merge is claimed. +**Exact-head maintenance ledger (2026-09-12):** PR #1623 now has exact head +`54871419193734918e79c89b54c65761f04ae095`. A current CodeRabbit P1 finding +showed that the frontend dependency-floor test accepted a lockfile whose +peer-qualified Vitest snapshot existed but whose importer-resolved base +`packages` record was absent. The regression test now removes that exact base +record while retaining another compliant version, so the importer contract +fails closed. The focused suite passed 19 tests in 24.35 seconds from +`backend` after its project-local frozen environment was synchronized. An +earlier missing `pgvector` import came from invoking the test outside that +environment and is not source-test evidence. Hosted Checks, a qualifying +current-head review, protected merge, and deployment remain unverified. + The protected-branch SHA in this header identifies the baseline's observation point. The inventory's `Base-SHA` column is captured independently for each PR at its scan time, so an older `develop` SHA in a row is expected snapshot From d8327d4904f38588b6b6883338aafb575256a19b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 19:34:40 +0900 Subject: [PATCH 13/16] fix(governance): keep dependency PR off canonical gap ledger Restore the protected develop ledger blob on this branch. PR #1602 remains the sole writer for docs/product-technical-gap-baseline.md while the complete dependency and regression-test delta stays in ancestry. --- docs/product-technical-gap-baseline.md | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 3b4256943..98bc17d2a 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -158,18 +158,6 @@ Windows-separator traversal protection passed 130 focused parser/import tests. All three have recreated hosted Checks and remain normal protected-merge candidates; no hosted pass or merge is claimed. -**Exact-head maintenance ledger (2026-09-12):** PR #1623 now has exact head -`54871419193734918e79c89b54c65761f04ae095`. A current CodeRabbit P1 finding -showed that the frontend dependency-floor test accepted a lockfile whose -peer-qualified Vitest snapshot existed but whose importer-resolved base -`packages` record was absent. The regression test now removes that exact base -record while retaining another compliant version, so the importer contract -fails closed. The focused suite passed 19 tests in 24.35 seconds from -`backend` after its project-local frozen environment was synchronized. An -earlier missing `pgvector` import came from invoking the test outside that -environment and is not source-test evidence. Hosted Checks, a qualifying -current-head review, protected merge, and deployment remain unverified. - The protected-branch SHA in this header identifies the baseline's observation point. The inventory's `Base-SHA` column is captured independently for each PR at its scan time, so an older `develop` SHA in a row is expected snapshot From 8175f7f04a8624a681a509eb8b446c7a3ff7bd5d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 22:04:52 +0900 Subject: [PATCH 14/16] test(deps): require patched Nano ID resolution --- backend/tests/test_frontend_nanoid_security.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/tests/test_frontend_nanoid_security.py b/backend/tests/test_frontend_nanoid_security.py index f80b23a01..66bc5b499 100644 --- a/backend/tests/test_frontend_nanoid_security.py +++ b/backend/tests/test_frontend_nanoid_security.py @@ -8,7 +8,7 @@ REPO_ROOT = Path(__file__).resolve().parents[2] FRONTEND_LOCK = REPO_ROOT / "frontend" / "pnpm-lock.yaml" -PATCHED_NANOID_VERSION = "3.3.18" +PATCHED_NANOID_VERSION = "3.3.19" def test_frontend_lock_resolves_only_patched_nanoid_3x() -> None: From 9d6d1e09e1ad52e57a1a3162c9984cceea4b2cb8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 22:05:04 +0900 Subject: [PATCH 15/16] fix(deps): pin patched Nano ID in pnpm owner --- CHANGELOG.md | 2 +- frontend/pnpm-lock.yaml | 9 +++++---- frontend/pnpm-workspace.yaml | 1 + 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ec84c36f..1fb2dc28c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,7 +39,7 @@ - OIDC token endpoint는 운영 환경에서 서버 전용 `OIDC_ALLOWED_HOSTS` 정확 호스트 allowlist를 필수로 적용합니다. hostname의 모든 DNS 결과가 공인 주소인지 검증한 뒤 해당 주소 집합을 native HTTP(S) 연결의 `lookup`에 고정하고, 원래 issuer hostname은 Host/TLS SNI로 유지해 사설 주소 해석과 DNS rebinding 사이의 TOCTOU를 차단합니다. 실패 로그는 입력 URL·token 대신 고정된 configuration/DNS·transport/response/backend-verification reason code만 남깁니다. - Trivy 2026-07-26 DB에서 새로 확인된 Next.js High 4건·Medium 5건(`CVE-2026-64641`–`CVE-2026-64649`)과 PostCSS High 1건(`GHSA-r28c-9q8g-f849`)을 제거하기 위해 Next.js/`eslint-config-next`를 `16.2.11`, PostCSS를 `8.5.18`로 갱신했습니다. 이후 2026-08-04 DB가 `8.5.18`에서 추가 탐지한 PostCSS Medium(`CVE-2026-69153`, 최초 수정 `8.5.23`)도 제거하도록 manifest·workspace override·lock을 `8.5.24`로 동기화했으며 저장소의 release-age 정책을 우회하지 않습니다. - `pnpm audit`가 개발 도구 체인에서 추가 탐지한 `brace-expansion <=5.0.7` High DoS(`GHSA-mh99-v99m-4gvg`)와 이후 `5.0.8`까지 영향을 주는 우회형 High DoS(`GHSA-rgw5-rvv9-x895`)는 `5.0.9` 전역 override로 제거했습니다. CommonJS default export를 기대하는 legacy `minimatch 3.1.5`에는 `expand` named export도 수용하는 최소 pnpm 패치를 적용해 ESLint/glob 동작을 보존합니다. 같은 감사에서 확인된 `undici 7.28.0`의 High 1건·Moderate 4건(`GHSA-4cwx-7wf7-3272` 등)은 `jsdom 30.0.1` 및 release-age 정책을 통과하는 `undici 8.9.0`으로 갱신했습니다. -- PostCSS의 Nano ID 해석을 `3.3.18`로 갱신해 사용자 제공 음수 크기에서 비보안 생성기가 무한 반복될 수 있는 High DoS(`CVE-2026-67214`, `GHSA-28wg-ghj8-5hjv`)를 제거했습니다. lockfile과 release-governance 회귀 테스트가 같은 최초 수정 3.x 버전을 강제합니다. +- PostCSS의 Nano ID 해석을 `3.3.19`로 갱신해 사용자 제공 음수 크기에서 비보안 생성기가 무한 반복될 수 있는 High DoS(`CVE-2026-67214`, `GHSA-28wg-ghj8-5hjv`)와 후속 3.x 보안 floor를 충족합니다. workspace override·lockfile·release-governance 회귀 테스트가 같은 패치 버전을 강제합니다. - root·frontend Docker build의 frozen install 계층이 pnpm manifest와 함께 `frontend/patches`를 먼저 복사하도록 수정해, 이미지 검증에서도 lockfile의 patched dependency를 동일하게 재현합니다. - Scorecard SARIF normalizer는 고정 workspace artifact로 정규화되는 `./scorecard-results.sarif`와 절대 경로를 동일하게 허용하면서 symlink·workspace 이탈은 계속 거부합니다. 도구 실행 실패 API는 CR/LF·제어 문자를 escape하고 500자로 제한하며, 로그에는 raw 도구 코드·예외 text 대신 SHA-256 기반 코드·traceback 상관 식별자만 기록합니다. - 백엔드 origin 보안 경계를 `frontend/src/lib/backend-url.ts`의 단일 생성기로 통합해 API proxy·session·OIDC callback이 같은 검증을 사용합니다. UI smoke의 새 `NARUON_FULL_PRODUCT_SCREENSHOT_PROFILE` 이름은 실제 selector 의미를 드러내며, 기존 `..._SCREENSHOT_DIR`은 호환 alias로 계속 지원합니다. diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 58377eb5f..cea165615 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -7,6 +7,7 @@ settings: overrides: brace-expansion: 5.0.9 js-yaml: 4.3.2 + nanoid: 3.3.19 postcss: 8.5.24 sharp: 0.35.4 undici: 8.9.0 @@ -2254,8 +2255,8 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - nanoid@3.3.18: - resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} + nanoid@3.3.19: + resolution: {integrity: sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -5070,7 +5071,7 @@ snapshots: ms@2.1.3: {} - nanoid@3.3.18: {} + nanoid@3.3.19: {} napi-postinstall@0.3.4: {} @@ -5213,7 +5214,7 @@ snapshots: postcss@8.5.24: dependencies: - nanoid: 3.3.18 + nanoid: 3.3.19 picocolors: 1.1.1 source-map-js: 1.2.1 diff --git a/frontend/pnpm-workspace.yaml b/frontend/pnpm-workspace.yaml index 70f2c4eea..585527fe9 100644 --- a/frontend/pnpm-workspace.yaml +++ b/frontend/pnpm-workspace.yaml @@ -16,6 +16,7 @@ supportedArchitectures: overrides: brace-expansion: "5.0.9" js-yaml: "4.3.2" + nanoid: "3.3.19" postcss: "8.5.24" sharp: "0.35.4" undici: 8.9.0 From ecbeb7a191b981d3e46eb8455e11812ca21de482 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 14:45:21 +0900 Subject: [PATCH 16/16] test(deps): bind PostCSS floor to reviewed manifest --- .../test_container_dependency_pin_contract.py | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/backend/tests/test_container_dependency_pin_contract.py b/backend/tests/test_container_dependency_pin_contract.py index fdd4f6620..5daf15b6e 100644 --- a/backend/tests/test_container_dependency_pin_contract.py +++ b/backend/tests/test_container_dependency_pin_contract.py @@ -19,6 +19,8 @@ REPO_ROOT = Path(__file__).resolve().parents[2] _HASH_PATTERN = re.compile(r"--hash=sha256:([0-9a-f]{64})") _EXACT_PIN_PATTERN = re.compile(r"^([A-Za-z0-9_.-]+)==([^\\\s]+)") +_EXACT_SEMVER_PATTERN = re.compile(r"^(\d+)\.(\d+)\.(\d+)$") +POSTCSS_SECURITY_FLOOR = (8, 5, 24) def read_repo_text(relative_path: str) -> str: @@ -83,6 +85,13 @@ def importer_resolution(importer_section: dict[str, object], group: str, name: s return resolution +def exact_semver(value: str) -> tuple[int, int, int]: + """Return one exact three-part semantic version for security-floor comparison.""" + match = _EXACT_SEMVER_PATTERN.fullmatch(value) + assert match is not None, f"expected exact semantic version, got {value!r}" + return tuple(int(part) for part in match.groups()) + + def test_container_provenance_dependency_pins_match_reviewed_manifests() -> None: """Keep backend, Strix, and frontend dependency floors reviewable together.""" backend_pins = exact_requirement_pins(read_repo_text("backend/requirements.txt")) @@ -94,6 +103,7 @@ def test_container_provenance_dependency_pins_match_reviewed_manifests() -> None read_repo_text("requirements-strix-ci-hashes.txt") ) frontend_package = json.loads(read_repo_text("frontend/package.json")) + frontend_workspace = yaml.safe_load(read_repo_text("frontend/pnpm-workspace.yaml")) frontend_lock = yaml.safe_load(read_repo_text("frontend/pnpm-lock.yaml")) assert backend_pins["cryptography"] == "50.0.0" @@ -116,29 +126,36 @@ def test_container_provenance_dependency_pins_match_reviewed_manifests() -> None for digest in strix_records[pin] ) + reviewed_postcss = frontend_package["devDependencies"]["postcss"] + assert isinstance(reviewed_postcss, str) + assert exact_semver(reviewed_postcss) >= POSTCSS_SECURITY_FLOOR + assert frontend_package["overrides"]["postcss"] == reviewed_postcss + assert frontend_workspace["overrides"]["postcss"] == reviewed_postcss + root_importer = frontend_lock["importers"]["."] postcss_resolution = importer_resolution( root_importer, "devDependencies", "postcss" ) jsdom_resolution = importer_resolution(root_importer, "devDependencies", "jsdom") - assert postcss_resolution == {"specifier": "8.5.24", "version": "8.5.24"} + assert postcss_resolution == { + "specifier": reviewed_postcss, + "version": reviewed_postcss, + } assert jsdom_resolution == {"specifier": "^30.0.1", "version": "30.0.1"} - assert frontend_package["devDependencies"]["postcss"] == "8.5.24" assert frontend_package["devDependencies"]["jsdom"] == "^30.0.1" - assert frontend_package["overrides"]["postcss"] == "8.5.24" assert frontend_package["overrides"]["brace-expansion"] == "5.0.9" assert frontend_package["overrides"]["undici"] == "8.9.0" assert frontend_lock["overrides"] == { **frontend_lock["overrides"], - "postcss": "8.5.24", + "postcss": reviewed_postcss, "brace-expansion": "5.0.9", "undici": "8.9.0", } package_records = frontend_lock["packages"] for exact_lock_entry in ( - "postcss@8.5.24", + f"postcss@{reviewed_postcss}", "jsdom@30.0.1", "brace-expansion@5.0.9", "undici@8.9.0",