Merge main into dev-dependencies bump, resolving the conflicts

`main` moved next 16.2.6 → 16.2.12 (#441) and applied transitive audit
fixes (#442) since this branch was cut, so both manifests conflicted.

Resolved by taking main's manifests and re-applying the group's bumps on
top: prettier ^3.9.6 and prettier-plugin-tailwindcss ^0.8.1 in
package.json, plus tailwindcss and @tailwindcss/postcss 4.3.2 → 4.3.3,
which are lockfile-only here because both are specced as `^4`. Lockfile
regenerated with npm 10 — the version CI's Node 20 ships, so the nested
optional-peer @swc/helpers entry `npm ci` needs is preserved.

Verified next 16.2.12 and #442's transitive versions all hold, and
`npm ci` succeeds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Arnas Donauskas 2026-07-29 09:29:22 +03:00
commit 77d37fb023
4 changed files with 213 additions and 103 deletions

230
package-lock.json generated
View File

@ -21,7 +21,7 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"date-fns": "^4.4.0", "date-fns": "^4.4.0",
"lucide-react": "^1.22.0", "lucide-react": "^1.22.0",
"next": "16.2.6", "next": "16.2.12",
"next-intl": "^4.13.2", "next-intl": "^4.13.2",
"opus-recorder": "^8.0.5", "opus-recorder": "^8.0.5",
"react": "19.2.4", "react": "19.2.4",
@ -38,7 +38,7 @@
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "16.2.6", "eslint-config-next": "16.2.12",
"prettier": "^3.9.6", "prettier": "^3.9.6",
"prettier-plugin-tailwindcss": "^0.8.1", "prettier-plugin-tailwindcss": "^0.8.1",
"tailwindcss": "^4", "tailwindcss": "^4",
@ -1048,12 +1048,12 @@
} }
}, },
"node_modules/@hono/node-server": { "node_modules/@hono/node-server": {
"version": "1.19.14", "version": "2.0.12",
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.0.12.tgz",
"integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", "integrity": "sha512-eWpQYr67tqJLeaSUl0Q+TquuYfUdTibpOJlUMV2FfUP7+KqCC5TufnwnlXL6mobZBJbGAYRd7ZvEBDCbLInjhg==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=18.14.1" "node": ">=20"
}, },
"peerDependencies": { "peerDependencies": {
"hono": "^4" "hono": "^4"
@ -1623,12 +1623,12 @@
} }
}, },
"node_modules/@modelcontextprotocol/sdk": { "node_modules/@modelcontextprotocol/sdk": {
"version": "1.29.0", "version": "1.30.0",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz",
"integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@hono/node-server": "^1.19.9", "@hono/node-server": "^1.19.9 || ^2.0.5",
"ajv": "^8.17.1", "ajv": "^8.17.1",
"ajv-formats": "^3.0.1", "ajv-formats": "^3.0.1",
"content-type": "^1.0.5", "content-type": "^1.0.5",
@ -1698,15 +1698,15 @@
} }
}, },
"node_modules/@next/env": { "node_modules/@next/env": {
"version": "16.2.6", "version": "16.2.12",
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.6.tgz", "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.12.tgz",
"integrity": "sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw==", "integrity": "sha512-d0Z5Bc13Fa4nR8pFAKx2jay2yhJM16vlfHbTzYnUQAxlNb6B6lmn4hjt69lYNt4kRtyYP6gEM49lPRHNbIyneg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@next/eslint-plugin-next": { "node_modules/@next/eslint-plugin-next": {
"version": "16.2.6", "version": "16.2.12",
"resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.2.6.tgz", "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.2.12.tgz",
"integrity": "sha512-Z8l6o4JWKUl755x4R+wogD86KPeU+Ckw4K+SYG4kHeOJtRenDeK+OSbGcqZpDtbwn9DsJVdir2UxmwXuinUbUw==", "integrity": "sha512-uF2z/qAK2q7B5/6CpnFcBRX6jOq5iCO+Uqh1UkJhXljX1JwLarLYhhoJadO6dPb6moTprOKewMXheBcbIoSbug==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -1714,9 +1714,9 @@
} }
}, },
"node_modules/@next/swc-darwin-arm64": { "node_modules/@next/swc-darwin-arm64": {
"version": "16.2.6", "version": "16.2.12",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.6.tgz", "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.12.tgz",
"integrity": "sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg==", "integrity": "sha512-0W1R0teHWJrqKX0FH20IzzIWAOuGtBxPGuObrxy1lE8hQvCFj49KE8a3WUg0D7sq6rn6zkM4c7YGUnhudBS6oA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1730,9 +1730,9 @@
} }
}, },
"node_modules/@next/swc-darwin-x64": { "node_modules/@next/swc-darwin-x64": {
"version": "16.2.6", "version": "16.2.12",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.6.tgz", "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.12.tgz",
"integrity": "sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ==", "integrity": "sha512-Hy5Ls099+aFUmOLmIgPfLqNi6iCwhL3uQCssz5rWk+5Nkc6TUKCE83DY5BbNylfm3+mfwcSFnLRfrZDJhVxdtw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -1746,9 +1746,9 @@
} }
}, },
"node_modules/@next/swc-linux-arm64-gnu": { "node_modules/@next/swc-linux-arm64-gnu": {
"version": "16.2.6", "version": "16.2.12",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.6.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.12.tgz",
"integrity": "sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w==", "integrity": "sha512-+YqU2h1cQkHsGfvjAsrSmst8UIFBibBGm5x3Xgel8NLMiDQtNOM4sM2GOEMvG5YiOBNeN/Ykk8cQC2S0Xrqljg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1762,9 +1762,9 @@
} }
}, },
"node_modules/@next/swc-linux-arm64-musl": { "node_modules/@next/swc-linux-arm64-musl": {
"version": "16.2.6", "version": "16.2.12",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.6.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.12.tgz",
"integrity": "sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==", "integrity": "sha512-0qjhiYBaKAqF63LA1ZWAAnKTzFUguAaZiRa5etMLGGPj/B6uEVjtIZldIzFEp3wHlB0koK6aTzqPtSdplTCjoA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1778,9 +1778,9 @@
} }
}, },
"node_modules/@next/swc-linux-x64-gnu": { "node_modules/@next/swc-linux-x64-gnu": {
"version": "16.2.6", "version": "16.2.12",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.6.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.12.tgz",
"integrity": "sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==", "integrity": "sha512-7A3q26W+h7gnA15uqBToNuDqBEFZZcqh0mW2mn4AJh/G5pdg2RVE3n4slzLEliASZFG3NmsbEzng/x2Sh09mBg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -1794,9 +1794,9 @@
} }
}, },
"node_modules/@next/swc-linux-x64-musl": { "node_modules/@next/swc-linux-x64-musl": {
"version": "16.2.6", "version": "16.2.12",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.6.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.12.tgz",
"integrity": "sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==", "integrity": "sha512-qSjL/uppm+cbh21s72Ss8gkiOhQ4dExWHNGOWy6eZV7STj5WsKehgxT61beSsOj+YYQuTplL376lOCdMQU5T8w==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -1810,9 +1810,9 @@
} }
}, },
"node_modules/@next/swc-win32-arm64-msvc": { "node_modules/@next/swc-win32-arm64-msvc": {
"version": "16.2.6", "version": "16.2.12",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.6.tgz", "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.12.tgz",
"integrity": "sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==", "integrity": "sha512-X6hzsOUJac/e7AWSbn9gQ9nzHld1xWP5iyjHpYWvud8pufB679O1xg4JDyKr8Xd69Jvd+kM2Der6uftiZCmjYA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1826,9 +1826,9 @@
} }
}, },
"node_modules/@next/swc-win32-x64-msvc": { "node_modules/@next/swc-win32-x64-msvc": {
"version": "16.2.6", "version": "16.2.12",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.6.tgz", "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.12.tgz",
"integrity": "sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA==", "integrity": "sha512-F6fakeHuFTLOPt0bslQJdf+xtT+WIP9DVn/m4y1w1mRnVPyh3D/cNvzlRkxM444xfm+IvvYNSOrKiA2CDJ0Uxw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -3277,15 +3277,15 @@
} }
}, },
"node_modules/@ts-morph/common/node_modules/brace-expansion": { "node_modules/@ts-morph/common/node_modules/brace-expansion": {
"version": "5.0.6", "version": "5.0.8",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"balanced-match": "^4.0.2" "balanced-match": "^4.0.2"
}, },
"engines": { "engines": {
"node": "18 || 20 || >=22" "node": "20 || >=22"
} }
}, },
"node_modules/@ts-morph/common/node_modules/fast-glob": { "node_modules/@ts-morph/common/node_modules/fast-glob": {
@ -3719,16 +3719,16 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
"version": "5.0.6", "version": "5.0.8",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"balanced-match": "^4.0.2" "balanced-match": "^4.0.2"
}, },
"engines": { "engines": {
"node": "18 || 20 || >=22" "node": "20 || >=22"
} }
}, },
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
@ -4639,20 +4639,20 @@
} }
}, },
"node_modules/body-parser": { "node_modules/body-parser": {
"version": "2.2.2", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz",
"integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"bytes": "^3.1.2", "bytes": "^3.1.2",
"content-type": "^1.0.5", "content-type": "^2.0.0",
"debug": "^4.4.3", "debug": "^4.4.3",
"http-errors": "^2.0.0", "http-errors": "^2.0.1",
"iconv-lite": "^0.7.0", "iconv-lite": "^0.7.2",
"on-finished": "^2.4.1", "on-finished": "^2.4.1",
"qs": "^6.14.1", "qs": "^6.15.2",
"raw-body": "^3.0.1", "raw-body": "^3.0.2",
"type-is": "^2.0.1" "type-is": "^2.1.0"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
@ -4662,10 +4662,23 @@
"url": "https://opencollective.com/express" "url": "https://opencollective.com/express"
} }
}, },
"node_modules/body-parser/node_modules/content-type": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz",
"integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "1.1.15", "version": "1.1.16",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
"integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -5578,9 +5591,9 @@
} }
}, },
"node_modules/enhanced-resolve": { "node_modules/enhanced-resolve": {
"version": "5.24.3", "version": "5.24.4",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.3.tgz", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.4.tgz",
"integrity": "sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ==", "integrity": "sha512-GVoi+ICHocoOIU7qVVM48wOJziRsqrsyqlI0Ce0LdowRn6v3bcH2zUa9kp85ncx0nwIb9/HOCOLS3fdThDG/XQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -5889,13 +5902,13 @@
} }
}, },
"node_modules/eslint-config-next": { "node_modules/eslint-config-next": {
"version": "16.2.6", "version": "16.2.12",
"resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.2.6.tgz", "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.2.12.tgz",
"integrity": "sha512-z2ELYSkyrrJ6cuunTU8vhsT/RpouPkjaSah06nVW6Rg2Hpg0Vs8s497/e5s8G8qtdp4ccsiovz5P1rv+5VSW2Q==", "integrity": "sha512-iaaf4vvKo5h2LBdGt0JuRv7t0Ysqr9FMCiFxbptDg8LqOE//mIKR80DdpOnSVM7qjLH3jT8P0aFiwXxBEGZRXw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@next/eslint-plugin-next": "16.2.6", "@next/eslint-plugin-next": "16.2.12",
"eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-node": "^0.3.6",
"eslint-import-resolver-typescript": "^3.5.2", "eslint-import-resolver-typescript": "^3.5.2",
"eslint-plugin-import": "^2.32.0", "eslint-plugin-import": "^2.32.0",
@ -6441,9 +6454,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-uri": { "node_modules/fast-uri": {
"version": "3.1.2", "version": "3.1.4",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz",
"integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==",
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@ -7006,9 +7019,9 @@
} }
}, },
"node_modules/hono": { "node_modules/hono": {
"version": "4.12.26", "version": "4.12.32",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.26.tgz", "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.32.tgz",
"integrity": "sha512-uyZtpnYxM9CmQ7QsQknM4zN8EftNqhON1qYeIKM0Se67CCEe2c44xyGURwB0axX2fBDu1dqHrHAc1hmNT8ITkw==", "integrity": "sha512-XcuyW9qE2kJn07PkecMOBd5Vq/hMy7mmGw+idz1yblbg9N17ijJODrvPkn7/dwL3Kulj8LcRJ69DLOWf91dRUg==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=16.9.0" "node": ">=16.9.0"
@ -7818,9 +7831,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/js-yaml": { "node_modules/js-yaml": {
"version": "4.2.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz",
"integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==",
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@ -8481,9 +8494,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.11", "version": "3.3.16",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@ -8531,12 +8544,12 @@
} }
}, },
"node_modules/next": { "node_modules/next": {
"version": "16.2.6", "version": "16.2.12",
"resolved": "https://registry.npmjs.org/next/-/next-16.2.6.tgz", "resolved": "https://registry.npmjs.org/next/-/next-16.2.12.tgz",
"integrity": "sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==", "integrity": "sha512-iD59eYQWmbFcEbX7v/acG5DRym9iw1DdaPoD0WTA920naWsE25wShzJW4+UvAs8MK9EC2kBfIH6vtto1H1PHGw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@next/env": "16.2.6", "@next/env": "16.2.12",
"@swc/helpers": "0.5.15", "@swc/helpers": "0.5.15",
"baseline-browser-mapping": "^2.9.19", "baseline-browser-mapping": "^2.9.19",
"caniuse-lite": "^1.0.30001579", "caniuse-lite": "^1.0.30001579",
@ -8550,14 +8563,14 @@
"node": ">=20.9.0" "node": ">=20.9.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@next/swc-darwin-arm64": "16.2.6", "@next/swc-darwin-arm64": "16.2.12",
"@next/swc-darwin-x64": "16.2.6", "@next/swc-darwin-x64": "16.2.12",
"@next/swc-linux-arm64-gnu": "16.2.6", "@next/swc-linux-arm64-gnu": "16.2.12",
"@next/swc-linux-arm64-musl": "16.2.6", "@next/swc-linux-arm64-musl": "16.2.12",
"@next/swc-linux-x64-gnu": "16.2.6", "@next/swc-linux-x64-gnu": "16.2.12",
"@next/swc-linux-x64-musl": "16.2.6", "@next/swc-linux-x64-musl": "16.2.12",
"@next/swc-win32-arm64-msvc": "16.2.6", "@next/swc-win32-arm64-msvc": "16.2.12",
"@next/swc-win32-x64-msvc": "16.2.6", "@next/swc-win32-x64-msvc": "16.2.12",
"sharp": "^0.34.5" "sharp": "^0.34.5"
}, },
"peerDependencies": { "peerDependencies": {
@ -9208,9 +9221,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.14", "version": "8.5.24",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.24.tgz",
"integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", "integrity": "sha512-8RyVklq0owXUTa4xlpzu4l9AaVKIdQvAcOHZWaMh98HgySsUtxRVf/chRe3dsSLqb6i40BzGRzEUddRaI+9TSw==",
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@ -9227,7 +9240,7 @@
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"nanoid": "^3.3.11", "nanoid": "^3.3.16",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
"source-map-js": "^1.2.1" "source-map-js": "^1.2.1"
}, },
@ -10823,17 +10836,34 @@
} }
}, },
"node_modules/type-is": { "node_modules/type-is": {
"version": "2.0.1", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz",
"integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"content-type": "^1.0.5", "content-type": "^2.0.0",
"media-typer": "^1.1.0", "media-typer": "^1.1.0",
"mime-types": "^3.0.0" "mime-types": "^3.0.0"
}, },
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/type-is/node_modules/content-type": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz",
"integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
} }
}, },
"node_modules/typed-array-buffer": { "node_modules/typed-array-buffer": {

View File

@ -51,7 +51,7 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"date-fns": "^4.4.0", "date-fns": "^4.4.0",
"lucide-react": "^1.22.0", "lucide-react": "^1.22.0",
"next": "16.2.6", "next": "16.2.12",
"next-intl": "^4.13.2", "next-intl": "^4.13.2",
"opus-recorder": "^8.0.5", "opus-recorder": "^8.0.5",
"react": "19.2.4", "react": "19.2.4",
@ -68,7 +68,7 @@
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "16.2.6", "eslint-config-next": "16.2.12",
"prettier": "^3.9.6", "prettier": "^3.9.6",
"prettier-plugin-tailwindcss": "^0.8.1", "prettier-plugin-tailwindcss": "^0.8.1",
"tailwindcss": "^4", "tailwindcss": "^4",

View File

@ -5,8 +5,15 @@ vi.mock('./meta-api', () => ({
uploadResumableMedia: vi.fn(async () => ({ handle: 'HANDLE123' })), uploadResumableMedia: vi.fn(async () => ({ handle: 'HANDLE123' })),
})); }));
// The SSRF guard does a real DNS lookup, so stub it — the fixtures below use
// a `.test` hostname that would never resolve. Each test sets the verdict.
vi.mock('@/lib/webhooks/ssrf', () => ({
isDeliverableUrl: vi.fn(async () => true),
}));
import { ensureImageHeaderHandle } from './template-header-handle'; import { ensureImageHeaderHandle } from './template-header-handle';
import { uploadResumableMedia } from './meta-api'; import { uploadResumableMedia } from './meta-api';
import { isDeliverableUrl } from '@/lib/webhooks/ssrf';
import type { TemplatePayload } from './template-validators'; import type { TemplatePayload } from './template-validators';
function payload(over: Partial<TemplatePayload> = {}): TemplatePayload { function payload(over: Partial<TemplatePayload> = {}): TemplatePayload {
@ -33,6 +40,8 @@ function imgResponse(type = 'image/jpeg', size = 1024, ok = true, status = 200):
describe('ensureImageHeaderHandle', () => { describe('ensureImageHeaderHandle', () => {
beforeEach(() => { beforeEach(() => {
vi.mocked(uploadResumableMedia).mockClear(); vi.mocked(uploadResumableMedia).mockClear();
vi.mocked(isDeliverableUrl).mockClear();
vi.mocked(isDeliverableUrl).mockResolvedValue(true);
}); });
afterEach(() => { afterEach(() => {
vi.unstubAllGlobals(); vi.unstubAllGlobals();
@ -78,4 +87,58 @@ describe('ensureImageHeaderHandle', () => {
vi.stubGlobal('fetch', vi.fn(async () => imgResponse('image/png', 6 * 1024 * 1024))); vi.stubGlobal('fetch', vi.fn(async () => imgResponse('image/png', 6 * 1024 * 1024)));
await expect(ensureImageHeaderHandle(payload(), 'tok')).rejects.toThrow(/5 MB/); await expect(ensureImageHeaderHandle(payload(), 'tok')).rejects.toThrow(/5 MB/);
}); });
// Regression: `header_media_url` is caller-supplied and any authenticated
// member can submit a template, so a non-public destination has to be
// refused *before* the server issues the request — otherwise the status
// and content-type carried back in the thrown error are an SSRF oracle for
// loopback, RFC1918 and cloud-metadata addresses.
it('refuses a non-public header URL without fetching it', async () => {
vi.stubEnv('META_APP_ID', 'app-1');
vi.mocked(isDeliverableUrl).mockResolvedValue(false);
const fetchSpy = vi.fn(async () => imgResponse('application/json'));
vi.stubGlobal('fetch', fetchSpy);
const p = payload({ header_media_url: 'http://169.254.169.254/latest/meta-data/' });
await expect(ensureImageHeaderHandle(p, 'tok')).rejects.toThrow(/publicly reachable/);
expect(isDeliverableUrl).toHaveBeenCalledWith('http://169.254.169.254/latest/meta-data/');
expect(fetchSpy).not.toHaveBeenCalled();
expect(uploadResumableMedia).not.toHaveBeenCalled();
expect(p.header_handle).toBeUndefined();
});
it('reports a blocked URL exactly like an unreachable one', async () => {
vi.stubEnv('META_APP_ID', 'app-1');
vi.mocked(isDeliverableUrl).mockResolvedValue(false);
vi.stubGlobal('fetch', vi.fn(async () => imgResponse()));
const blocked = await ensureImageHeaderHandle(payload(), 'tok').catch(
(e: Error) => e.message,
);
vi.mocked(isDeliverableUrl).mockResolvedValue(true);
vi.stubGlobal(
'fetch',
vi.fn(async () => {
throw new Error('ECONNREFUSED');
}),
);
const unreachable = await ensureImageHeaderHandle(payload(), 'tok').catch(
(e: Error) => e.message,
);
expect(blocked).toBe(unreachable);
});
it('does not follow redirects, so a public URL cannot bounce to an internal one', async () => {
vi.stubEnv('META_APP_ID', 'app-1');
const fetchSpy = vi.fn(async () => imgResponse('image/jpeg', 1024));
vi.stubGlobal('fetch', fetchSpy);
await ensureImageHeaderHandle(payload(), 'tok');
const init = (fetchSpy.mock.calls[0] as unknown[])[1] as RequestInit;
expect(init).toMatchObject({ redirect: 'manual' });
});
}); });

View File

@ -1,5 +1,6 @@
import { uploadResumableMedia } from '@/lib/whatsapp/meta-api' import { uploadResumableMedia } from '@/lib/whatsapp/meta-api'
import type { TemplatePayload } from '@/lib/whatsapp/template-validators' import type { TemplatePayload } from '@/lib/whatsapp/template-validators'
import { isDeliverableUrl } from '@/lib/webhooks/ssrf'
/** /**
* Meta requires an `example.header_handle` (from the Resumable Upload * Meta requires an `example.header_handle` (from the Resumable Upload
@ -33,11 +34,27 @@ export async function ensureImageHeaderHandle(
) )
} }
// SSRF guard: `header_media_url` is caller-supplied (any authenticated
// member can submit a template) and the fetch below happens server-side,
// so refuse any destination that resolves to a private / loopback /
// link-local / reserved address. Same guard, same message as the two
// other outbound-fetch call sites (see lib/webhooks/ssrf.ts) — matching
// the unreachable-host message keeps the failure from being an oracle.
if (!(await isDeliverableUrl(payload.header_media_url))) {
throw new Error('Could not fetch the header image URL. Make sure it is publicly reachable.')
}
// Fetch the sample image bytes (works for our uploaded chat-media URL // Fetch the sample image bytes (works for our uploaded chat-media URL
// and for a manually-pasted public link). // and for a manually-pasted public link).
let res: Response let res: Response
try { try {
res = await fetch(payload.header_media_url) res = await fetch(payload.header_media_url, {
// Do NOT follow redirects — a public URL could 3xx-bounce to an
// internal address, defeating the guard above. Bound the request so
// a hung host can't tie up the template-submit handler.
redirect: 'manual',
signal: AbortSignal.timeout(10_000),
})
} catch { } catch {
throw new Error('Could not fetch the header image URL. Make sure it is publicly reachable.') throw new Error('Could not fetch the header image URL. Make sure it is publicly reachable.')
} }