From a72196cc927ef151d4310684729994b688e5297a Mon Sep 17 00:00:00 2001 From: "vercel[bot]" <35613825+vercel[bot]@users.noreply.github.com> Date: Thu, 11 Dec 2025 16:45:46 -0600 Subject: [PATCH 01/16] Fix React Server Components CVE vulnerabilities (#156) Updated dependencies to fix Next.js and React CVE vulnerabilities. The fix-react2shell-next tool automatically updated the following packages to their secure versions: - next - react-server-dom-webpack - react-server-dom-parcel - react-server-dom-turbopack All package.json files have been scanned and vulnerable versions have been patched to the correct fixed versions based on the official React advisory. Co-authored-by: Vercel --- lib/next-boilerplate/package.json | 2 +- lib/next-boilerplate/pnpm-lock.yaml | 82 ++++++++++----------- package.json | 2 +- pnpm-lock.yaml | 110 ++++++++++++++-------------- 4 files changed, 98 insertions(+), 98 deletions(-) diff --git a/lib/next-boilerplate/package.json b/lib/next-boilerplate/package.json index 950962c66..faf819263 100644 --- a/lib/next-boilerplate/package.json +++ b/lib/next-boilerplate/package.json @@ -9,7 +9,7 @@ "lint": "eslint" }, "dependencies": { - "next": "16.0.7", + "next": "16.0.9", "react": "19.2.0", "react-dom": "19.2.0" }, diff --git a/lib/next-boilerplate/pnpm-lock.yaml b/lib/next-boilerplate/pnpm-lock.yaml index 8d4f62193..dbaf9c7e2 100644 --- a/lib/next-boilerplate/pnpm-lock.yaml +++ b/lib/next-boilerplate/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: next: - specifier: 16.0.7 - version: 16.0.7(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + specifier: 16.0.9 + version: 16.0.9(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) react: specifier: 19.2.0 version: 19.2.0 @@ -335,56 +335,56 @@ packages: '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} - '@next/env@16.0.7': - resolution: {integrity: sha512-gpaNgUh5nftFKRkRQGnVi5dpcYSKGcZZkQffZ172OrG/XkrnS7UBTQ648YY+8ME92cC4IojpI2LqTC8sTDhAaw==} + '@next/env@16.0.9': + resolution: {integrity: sha512-6284pl8c8n9PQidN63qjPVEu1uXXKjnmbmaLebOzIfTrSXdGiAPsIMRi4pk/+v/ezqweE1/B8bFqiAAfC6lMXg==} '@next/eslint-plugin-next@16.0.3': resolution: {integrity: sha512-6sPWmZetzFWMsz7Dhuxsdmbu3fK+/AxKRtj7OB0/3OZAI2MHB/v2FeYh271LZ9abvnM1WIwWc/5umYjx0jo5sQ==} - '@next/swc-darwin-arm64@16.0.7': - resolution: {integrity: sha512-LlDtCYOEj/rfSnEn/Idi+j1QKHxY9BJFmxx7108A6D8K0SB+bNgfYQATPk/4LqOl4C0Wo3LACg2ie6s7xqMpJg==} + '@next/swc-darwin-arm64@16.0.9': + resolution: {integrity: sha512-j06fWg/gPqiWjK+sEpCDsh5gX+Bdy9gnPYjFqMBvBEOIcCFy1/ecF6pY6XAce7WyCJAbBPVb+6GvpmUZKNq0oQ==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@16.0.7': - resolution: {integrity: sha512-rtZ7BhnVvO1ICf3QzfW9H3aPz7GhBrnSIMZyr4Qy6boXF0b5E3QLs+cvJmg3PsTCG2M1PBoC+DANUi4wCOKXpA==} + '@next/swc-darwin-x64@16.0.9': + resolution: {integrity: sha512-FRYYz5GSKUkfvDSjd5hgHME2LgYjfOLBmhRVltbs3oRNQQf9n5UTQMmIu/u5vpkjJFV4L2tqo8duGqDxdQOFwg==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@16.0.7': - resolution: {integrity: sha512-mloD5WcPIeIeeZqAIP5c2kdaTa6StwP4/2EGy1mUw8HiexSHGK/jcM7lFuS3u3i2zn+xH9+wXJs6njO7VrAqww==} + '@next/swc-linux-arm64-gnu@16.0.9': + resolution: {integrity: sha512-EI2klFVL8tOyEIX5J1gXXpm1YuChmDy4R+tHoNjkCHUmBJqXioYErX/O2go4pEhjxkAxHp2i8y5aJcRz2m5NqQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@16.0.7': - resolution: {integrity: sha512-+ksWNrZrthisXuo9gd1XnjHRowCbMtl/YgMpbRvFeDEqEBd523YHPWpBuDjomod88U8Xliw5DHhekBC3EOOd9g==} + '@next/swc-linux-arm64-musl@16.0.9': + resolution: {integrity: sha512-vq/5HeGvowhDPMrpp/KP4GjPVhIXnwNeDPF5D6XK6ta96UIt+C0HwJwuHYlwmn0SWyNANqx1Mp6qSVDXwbFKsw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@16.0.7': - resolution: {integrity: sha512-4WtJU5cRDxpEE44Ana2Xro1284hnyVpBb62lIpU5k85D8xXxatT+rXxBgPkc7C1XwkZMWpK5rXLXTh9PFipWsA==} + '@next/swc-linux-x64-gnu@16.0.9': + resolution: {integrity: sha512-GlUdJwy2leA/HnyRYxJ1ZJLCJH+BxZfqV4E0iYLrJipDKxWejWpPtZUdccPmCfIEY9gNBO7bPfbG6IIgkt0qXg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@16.0.7': - resolution: {integrity: sha512-HYlhqIP6kBPXalW2dbMTSuB4+8fe+j9juyxwfMwCe9kQPPeiyFn7NMjNfoFOfJ2eXkeQsoUGXg+O2SE3m4Qg2w==} + '@next/swc-linux-x64-musl@16.0.9': + resolution: {integrity: sha512-UCtOVx4N8AHF434VPwg4L0KkFLAd7pgJShzlX/hhv9+FDrT7/xCuVdlBsCXH7l9yCA/wHl3OqhMbIkgUluriWA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@16.0.7': - resolution: {integrity: sha512-EviG+43iOoBRZg9deGauXExjRphhuYmIOJ12b9sAPy0eQ6iwcPxfED2asb/s2/yiLYOdm37kPaiZu8uXSYPs0Q==} + '@next/swc-win32-arm64-msvc@16.0.9': + resolution: {integrity: sha512-tQjtDGtv63mV3n/cZ4TH8BgUvKTSFlrF06yT5DyRmgQuj5WEjBUDy0W3myIW5kTRYMPrLn42H3VfCNwBH6YYiA==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@16.0.7': - resolution: {integrity: sha512-gniPjy55zp5Eg0896qSrf3yB1dw4F/3s8VK1ephdsZZ129j2n6e1WqCbE2YgcKhW9hPB9TVZENugquWJD5x0ug==} + '@next/swc-win32-x64-msvc@16.0.9': + resolution: {integrity: sha512-y9AGACHTBwnWFLq5B5Fiv3FEbXBusdPb60pgoerB04CV/pwjY1xQNdoTNxAv7eUhU2k1CKnkN4XWVuiK07uOqA==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -1498,8 +1498,8 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - next@16.0.7: - resolution: {integrity: sha512-3mBRJyPxT4LOxAJI6IsXeFtKfiJUbjCLgvXO02fV8Wy/lIhPvP94Fe7dGhUgHXcQy4sSuYwQNcOLhIfOm0rL0A==} + next@16.0.9: + resolution: {integrity: sha512-Xk5x/wEk6ADIAtQECLo1uyE5OagbQCiZ+gW4XEv24FjQ3O2PdSkvgsn22aaseSXC7xg84oONvQjFbSTX5YsMhQ==} engines: {node: '>=20.9.0'} hasBin: true peerDependencies: @@ -2211,34 +2211,34 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true - '@next/env@16.0.7': {} + '@next/env@16.0.9': {} '@next/eslint-plugin-next@16.0.3': dependencies: fast-glob: 3.3.1 - '@next/swc-darwin-arm64@16.0.7': + '@next/swc-darwin-arm64@16.0.9': optional: true - '@next/swc-darwin-x64@16.0.7': + '@next/swc-darwin-x64@16.0.9': optional: true - '@next/swc-linux-arm64-gnu@16.0.7': + '@next/swc-linux-arm64-gnu@16.0.9': optional: true - '@next/swc-linux-arm64-musl@16.0.7': + '@next/swc-linux-arm64-musl@16.0.9': optional: true - '@next/swc-linux-x64-gnu@16.0.7': + '@next/swc-linux-x64-gnu@16.0.9': optional: true - '@next/swc-linux-x64-musl@16.0.7': + '@next/swc-linux-x64-musl@16.0.9': optional: true - '@next/swc-win32-arm64-msvc@16.0.7': + '@next/swc-win32-arm64-msvc@16.0.9': optional: true - '@next/swc-win32-x64-msvc@16.0.7': + '@next/swc-win32-x64-msvc@16.0.9': optional: true '@nodelib/fs.scandir@2.1.5': @@ -3477,9 +3477,9 @@ snapshots: natural-compare@1.4.0: {} - next@16.0.7(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + next@16.0.9(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: - '@next/env': 16.0.7 + '@next/env': 16.0.9 '@swc/helpers': 0.5.15 caniuse-lite: 1.0.30001756 postcss: 8.4.31 @@ -3487,14 +3487,14 @@ snapshots: react-dom: 19.2.0(react@19.2.0) styled-jsx: 5.1.6(@babel/core@7.28.5)(react@19.2.0) optionalDependencies: - '@next/swc-darwin-arm64': 16.0.7 - '@next/swc-darwin-x64': 16.0.7 - '@next/swc-linux-arm64-gnu': 16.0.7 - '@next/swc-linux-arm64-musl': 16.0.7 - '@next/swc-linux-x64-gnu': 16.0.7 - '@next/swc-linux-x64-musl': 16.0.7 - '@next/swc-win32-arm64-msvc': 16.0.7 - '@next/swc-win32-x64-msvc': 16.0.7 + '@next/swc-darwin-arm64': 16.0.9 + '@next/swc-darwin-x64': 16.0.9 + '@next/swc-linux-arm64-gnu': 16.0.9 + '@next/swc-linux-arm64-musl': 16.0.9 + '@next/swc-linux-x64-gnu': 16.0.9 + '@next/swc-linux-x64-musl': 16.0.9 + '@next/swc-win32-arm64-msvc': 16.0.9 + '@next/swc-win32-x64-msvc': 16.0.9 sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' diff --git a/package.json b/package.json index 3003d7e60..563163c4a 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "lucide-react": "^0.552.0", "motion": "^12.23.24", "nanoid": "^5.1.6", - "next": "16.0.7", + "next": "16.0.9", "next-themes": "^0.4.6", "openai": "^6.8.1", "postgres": "^3.4.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c656cdb9e..64aaecaae 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -34,7 +34,7 @@ importers: version: 7.12.0 '@vercel/analytics': specifier: ^1.5.0 - version: 1.5.0(next@16.0.7(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1) + version: 1.5.0(next@16.0.9(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1) '@vercel/og': specifier: ^0.8.5 version: 0.8.5 @@ -43,7 +43,7 @@ importers: version: 1.17.1 '@vercel/speed-insights': specifier: ^1.2.0 - version: 1.2.0(next@16.0.7(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1) + version: 1.2.0(next@16.0.9(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1) '@xyflow/react': specifier: ^12.9.2 version: 12.9.2(@types/react@19.2.2)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) @@ -52,7 +52,7 @@ importers: version: 5.0.102(zod@4.1.12) better-auth: specifier: ^1.3.34 - version: 1.3.34(next@16.0.7(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + version: 1.3.34(next@16.0.9(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -81,8 +81,8 @@ importers: specifier: ^5.1.6 version: 5.1.6 next: - specifier: 16.0.7 - version: 16.0.7(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + specifier: 16.0.9 + version: 16.0.9(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.2.1(react@19.2.1))(react@19.2.1) @@ -124,7 +124,7 @@ importers: version: 1.1.2(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) workflow: specifier: 4.0.1-beta.17 - version: 4.0.1-beta.17(@aws-sdk/client-sts@3.936.0)(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(next@16.0.7(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typescript@5.9.3) + version: 4.0.1-beta.17(@aws-sdk/client-sts@3.936.0)(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(next@16.0.9(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typescript@5.9.3) zod: specifier: ^4.1.12 version: 4.1.12 @@ -1059,8 +1059,8 @@ packages: '@next/env@15.5.4': resolution: {integrity: sha512-27SQhYp5QryzIT5uO8hq99C69eLQ7qkzkDPsk3N+GuS2XgOgoYEeOav7Pf8Tn4drECOVDsDg8oj+/DVy8qQL2A==} - '@next/env@16.0.7': - resolution: {integrity: sha512-gpaNgUh5nftFKRkRQGnVi5dpcYSKGcZZkQffZ172OrG/XkrnS7UBTQ648YY+8ME92cC4IojpI2LqTC8sTDhAaw==} + '@next/env@16.0.9': + resolution: {integrity: sha512-6284pl8c8n9PQidN63qjPVEu1uXXKjnmbmaLebOzIfTrSXdGiAPsIMRi4pk/+v/ezqweE1/B8bFqiAAfC6lMXg==} '@next/swc-darwin-arm64@15.5.4': resolution: {integrity: sha512-nopqz+Ov6uvorej8ndRX6HlxCYWCO3AHLfKK2TYvxoSB2scETOcfm/HSS3piPqc3A+MUgyHoqE6je4wnkjfrOA==} @@ -1068,8 +1068,8 @@ packages: cpu: [arm64] os: [darwin] - '@next/swc-darwin-arm64@16.0.7': - resolution: {integrity: sha512-LlDtCYOEj/rfSnEn/Idi+j1QKHxY9BJFmxx7108A6D8K0SB+bNgfYQATPk/4LqOl4C0Wo3LACg2ie6s7xqMpJg==} + '@next/swc-darwin-arm64@16.0.9': + resolution: {integrity: sha512-j06fWg/gPqiWjK+sEpCDsh5gX+Bdy9gnPYjFqMBvBEOIcCFy1/ecF6pY6XAce7WyCJAbBPVb+6GvpmUZKNq0oQ==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] @@ -1080,8 +1080,8 @@ packages: cpu: [x64] os: [darwin] - '@next/swc-darwin-x64@16.0.7': - resolution: {integrity: sha512-rtZ7BhnVvO1ICf3QzfW9H3aPz7GhBrnSIMZyr4Qy6boXF0b5E3QLs+cvJmg3PsTCG2M1PBoC+DANUi4wCOKXpA==} + '@next/swc-darwin-x64@16.0.9': + resolution: {integrity: sha512-FRYYz5GSKUkfvDSjd5hgHME2LgYjfOLBmhRVltbs3oRNQQf9n5UTQMmIu/u5vpkjJFV4L2tqo8duGqDxdQOFwg==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] @@ -1092,8 +1092,8 @@ packages: cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-gnu@16.0.7': - resolution: {integrity: sha512-mloD5WcPIeIeeZqAIP5c2kdaTa6StwP4/2EGy1mUw8HiexSHGK/jcM7lFuS3u3i2zn+xH9+wXJs6njO7VrAqww==} + '@next/swc-linux-arm64-gnu@16.0.9': + resolution: {integrity: sha512-EI2klFVL8tOyEIX5J1gXXpm1YuChmDy4R+tHoNjkCHUmBJqXioYErX/O2go4pEhjxkAxHp2i8y5aJcRz2m5NqQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] @@ -1104,8 +1104,8 @@ packages: cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@16.0.7': - resolution: {integrity: sha512-+ksWNrZrthisXuo9gd1XnjHRowCbMtl/YgMpbRvFeDEqEBd523YHPWpBuDjomod88U8Xliw5DHhekBC3EOOd9g==} + '@next/swc-linux-arm64-musl@16.0.9': + resolution: {integrity: sha512-vq/5HeGvowhDPMrpp/KP4GjPVhIXnwNeDPF5D6XK6ta96UIt+C0HwJwuHYlwmn0SWyNANqx1Mp6qSVDXwbFKsw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] @@ -1116,8 +1116,8 @@ packages: cpu: [x64] os: [linux] - '@next/swc-linux-x64-gnu@16.0.7': - resolution: {integrity: sha512-4WtJU5cRDxpEE44Ana2Xro1284hnyVpBb62lIpU5k85D8xXxatT+rXxBgPkc7C1XwkZMWpK5rXLXTh9PFipWsA==} + '@next/swc-linux-x64-gnu@16.0.9': + resolution: {integrity: sha512-GlUdJwy2leA/HnyRYxJ1ZJLCJH+BxZfqV4E0iYLrJipDKxWejWpPtZUdccPmCfIEY9gNBO7bPfbG6IIgkt0qXg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] @@ -1128,8 +1128,8 @@ packages: cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@16.0.7': - resolution: {integrity: sha512-HYlhqIP6kBPXalW2dbMTSuB4+8fe+j9juyxwfMwCe9kQPPeiyFn7NMjNfoFOfJ2eXkeQsoUGXg+O2SE3m4Qg2w==} + '@next/swc-linux-x64-musl@16.0.9': + resolution: {integrity: sha512-UCtOVx4N8AHF434VPwg4L0KkFLAd7pgJShzlX/hhv9+FDrT7/xCuVdlBsCXH7l9yCA/wHl3OqhMbIkgUluriWA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] @@ -1140,8 +1140,8 @@ packages: cpu: [arm64] os: [win32] - '@next/swc-win32-arm64-msvc@16.0.7': - resolution: {integrity: sha512-EviG+43iOoBRZg9deGauXExjRphhuYmIOJ12b9sAPy0eQ6iwcPxfED2asb/s2/yiLYOdm37kPaiZu8uXSYPs0Q==} + '@next/swc-win32-arm64-msvc@16.0.9': + resolution: {integrity: sha512-tQjtDGtv63mV3n/cZ4TH8BgUvKTSFlrF06yT5DyRmgQuj5WEjBUDy0W3myIW5kTRYMPrLn42H3VfCNwBH6YYiA==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] @@ -1152,8 +1152,8 @@ packages: cpu: [x64] os: [win32] - '@next/swc-win32-x64-msvc@16.0.7': - resolution: {integrity: sha512-gniPjy55zp5Eg0896qSrf3yB1dw4F/3s8VK1ephdsZZ129j2n6e1WqCbE2YgcKhW9hPB9TVZENugquWJD5x0ug==} + '@next/swc-win32-x64-msvc@16.0.9': + resolution: {integrity: sha512-y9AGACHTBwnWFLq5B5Fiv3FEbXBusdPb60pgoerB04CV/pwjY1xQNdoTNxAv7eUhU2k1CKnkN4XWVuiK07uOqA==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -3834,8 +3834,8 @@ packages: sass: optional: true - next@16.0.7: - resolution: {integrity: sha512-3mBRJyPxT4LOxAJI6IsXeFtKfiJUbjCLgvXO02fV8Wy/lIhPvP94Fe7dGhUgHXcQy4sSuYwQNcOLhIfOm0rL0A==} + next@16.0.9: + resolution: {integrity: sha512-Xk5x/wEk6ADIAtQECLo1uyE5OagbQCiZ+gW4XEv24FjQ3O2PdSkvgsn22aaseSXC7xg84oONvQjFbSTX5YsMhQ==} engines: {node: '>=20.9.0'} hasBin: true peerDependencies: @@ -5684,54 +5684,54 @@ snapshots: '@next/env@15.5.4': {} - '@next/env@16.0.7': {} + '@next/env@16.0.9': {} '@next/swc-darwin-arm64@15.5.4': optional: true - '@next/swc-darwin-arm64@16.0.7': + '@next/swc-darwin-arm64@16.0.9': optional: true '@next/swc-darwin-x64@15.5.4': optional: true - '@next/swc-darwin-x64@16.0.7': + '@next/swc-darwin-x64@16.0.9': optional: true '@next/swc-linux-arm64-gnu@15.5.4': optional: true - '@next/swc-linux-arm64-gnu@16.0.7': + '@next/swc-linux-arm64-gnu@16.0.9': optional: true '@next/swc-linux-arm64-musl@15.5.4': optional: true - '@next/swc-linux-arm64-musl@16.0.7': + '@next/swc-linux-arm64-musl@16.0.9': optional: true '@next/swc-linux-x64-gnu@15.5.4': optional: true - '@next/swc-linux-x64-gnu@16.0.7': + '@next/swc-linux-x64-gnu@16.0.9': optional: true '@next/swc-linux-x64-musl@15.5.4': optional: true - '@next/swc-linux-x64-musl@16.0.7': + '@next/swc-linux-x64-musl@16.0.9': optional: true '@next/swc-win32-arm64-msvc@15.5.4': optional: true - '@next/swc-win32-arm64-msvc@16.0.7': + '@next/swc-win32-arm64-msvc@16.0.9': optional: true '@next/swc-win32-x64-msvc@15.5.4': optional: true - '@next/swc-win32-x64-msvc@16.0.7': + '@next/swc-win32-x64-msvc@16.0.9': optional: true '@noble/ciphers@2.0.1': {} @@ -7222,9 +7222,9 @@ snapshots: '@types/retry@0.12.0': {} - '@vercel/analytics@1.5.0(next@16.0.7(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)': + '@vercel/analytics@1.5.0(next@16.0.9(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)': optionalDependencies: - next: 16.0.7(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + next: 16.0.9(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) react: 19.2.1 '@vercel/functions@3.3.3(@aws-sdk/credential-provider-web-identity@3.609.0(@aws-sdk/client-sts@3.936.0))': @@ -7249,9 +7249,9 @@ snapshots: dependencies: zod: 4.1.12 - '@vercel/speed-insights@1.2.0(next@16.0.7(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)': + '@vercel/speed-insights@1.2.0(next@16.0.9(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)': optionalDependencies: - next: 16.0.7(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + next: 16.0.9(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) react: 19.2.1 '@workflow/builders@4.0.1-beta.13(@aws-sdk/client-sts@3.936.0)(@opentelemetry/api@1.9.0)': @@ -7347,7 +7347,7 @@ snapshots: '@workflow/utils': 4.0.1-beta.3 ms: 2.1.3 - '@workflow/next@4.0.1-beta.17(@aws-sdk/client-sts@3.936.0)(@opentelemetry/api@1.9.0)(next@16.0.7(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))': + '@workflow/next@4.0.1-beta.17(@aws-sdk/client-sts@3.936.0)(@opentelemetry/api@1.9.0)(next@16.0.9(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))': dependencies: '@swc/core': 1.11.24 '@workflow/builders': 4.0.1-beta.13(@aws-sdk/client-sts@3.936.0)(@opentelemetry/api@1.9.0) @@ -7356,7 +7356,7 @@ snapshots: semver: 7.7.3 watchpack: 2.4.4 optionalDependencies: - next: 16.0.7(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + next: 16.0.9(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) transitivePeerDependencies: - '@aws-sdk/client-sts' - '@opentelemetry/api' @@ -7543,7 +7543,7 @@ snapshots: baseline-browser-mapping@2.8.23: optional: true - better-auth@1.3.34(next@16.0.7(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1): + better-auth@1.3.34(next@16.0.9(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1): dependencies: '@better-auth/core': 1.3.34(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.0.19)(jose@6.1.0)(kysely@0.28.8)(nanostores@1.0.1) '@better-auth/telemetry': 1.3.34(better-call@1.0.19)(jose@6.1.0)(kysely@0.28.8)(nanostores@1.0.1) @@ -7560,7 +7560,7 @@ snapshots: nanostores: 1.0.1 zod: 4.1.12 optionalDependencies: - next: 16.0.7(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + next: 16.0.9(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) react: 19.2.1 react-dom: 19.2.1(react@19.2.1) @@ -8518,9 +8518,9 @@ snapshots: - '@babel/core' - babel-plugin-macros - next@16.0.7(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1): + next@16.0.9(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1): dependencies: - '@next/env': 16.0.7 + '@next/env': 16.0.9 '@swc/helpers': 0.5.15 caniuse-lite: 1.0.30001753 postcss: 8.4.31 @@ -8528,14 +8528,14 @@ snapshots: react-dom: 19.2.1(react@19.2.1) styled-jsx: 5.1.6(@babel/core@7.28.5)(react@19.2.1) optionalDependencies: - '@next/swc-darwin-arm64': 16.0.7 - '@next/swc-darwin-x64': 16.0.7 - '@next/swc-linux-arm64-gnu': 16.0.7 - '@next/swc-linux-arm64-musl': 16.0.7 - '@next/swc-linux-x64-gnu': 16.0.7 - '@next/swc-linux-x64-musl': 16.0.7 - '@next/swc-win32-arm64-msvc': 16.0.7 - '@next/swc-win32-x64-msvc': 16.0.7 + '@next/swc-darwin-arm64': 16.0.9 + '@next/swc-darwin-x64': 16.0.9 + '@next/swc-linux-arm64-gnu': 16.0.9 + '@next/swc-linux-arm64-musl': 16.0.9 + '@next/swc-linux-x64-gnu': 16.0.9 + '@next/swc-linux-x64-musl': 16.0.9 + '@next/swc-win32-arm64-msvc': 16.0.9 + '@next/swc-win32-x64-msvc': 16.0.9 '@opentelemetry/api': 1.9.0 '@playwright/test': 1.57.0 sharp: 0.34.4 @@ -9270,12 +9270,12 @@ snapshots: wordwrap@1.0.0: {} - workflow@4.0.1-beta.17(@aws-sdk/client-sts@3.936.0)(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(next@16.0.7(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typescript@5.9.3): + workflow@4.0.1-beta.17(@aws-sdk/client-sts@3.936.0)(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(next@16.0.9(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typescript@5.9.3): dependencies: '@workflow/cli': 4.0.1-beta.17(@aws-sdk/client-sts@3.936.0)(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typescript@5.9.3) '@workflow/core': 4.0.1-beta.15(@aws-sdk/client-sts@3.936.0)(@opentelemetry/api@1.9.0) '@workflow/errors': 4.0.1-beta.5 - '@workflow/next': 4.0.1-beta.17(@aws-sdk/client-sts@3.936.0)(@opentelemetry/api@1.9.0)(next@16.0.7(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)) + '@workflow/next': 4.0.1-beta.17(@aws-sdk/client-sts@3.936.0)(@opentelemetry/api@1.9.0)(next@16.0.9(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)) '@workflow/nitro': 4.0.1-beta.17(@aws-sdk/client-sts@3.936.0)(@opentelemetry/api@1.9.0) '@workflow/nuxt': 4.0.1-beta.6(@aws-sdk/client-sts@3.936.0)(@opentelemetry/api@1.9.0) '@workflow/sveltekit': 4.0.0-beta.11(@aws-sdk/client-sts@3.936.0)(@opentelemetry/api@1.9.0) From 21b614238291b1f3dd580652f78927872a8abc7a Mon Sep 17 00:00:00 2001 From: "vercel[bot]" <35613825+vercel[bot]@users.noreply.github.com> Date: Thu, 11 Dec 2025 22:31:47 -0600 Subject: [PATCH 02/16] Fix React Server Components CVE vulnerabilities (#157) Updated dependencies to fix Next.js and React CVE vulnerabilities. The fix-react2shell-next tool automatically updated the following packages to their secure versions: - next - react-server-dom-webpack - react-server-dom-parcel - react-server-dom-turbopack All package.json files have been scanned and vulnerable versions have been patched to the correct fixed versions based on the official React advisory. Co-authored-by: Vercel --- lib/next-boilerplate/package.json | 2 +- lib/next-boilerplate/pnpm-lock.yaml | 82 ++++++++++----------- package.json | 2 +- pnpm-lock.yaml | 110 ++++++++++++++-------------- 4 files changed, 98 insertions(+), 98 deletions(-) diff --git a/lib/next-boilerplate/package.json b/lib/next-boilerplate/package.json index faf819263..124e01fad 100644 --- a/lib/next-boilerplate/package.json +++ b/lib/next-boilerplate/package.json @@ -9,7 +9,7 @@ "lint": "eslint" }, "dependencies": { - "next": "16.0.9", + "next": "16.0.10", "react": "19.2.0", "react-dom": "19.2.0" }, diff --git a/lib/next-boilerplate/pnpm-lock.yaml b/lib/next-boilerplate/pnpm-lock.yaml index dbaf9c7e2..72515cd85 100644 --- a/lib/next-boilerplate/pnpm-lock.yaml +++ b/lib/next-boilerplate/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: next: - specifier: 16.0.9 - version: 16.0.9(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + specifier: 16.0.10 + version: 16.0.10(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) react: specifier: 19.2.0 version: 19.2.0 @@ -335,56 +335,56 @@ packages: '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} - '@next/env@16.0.9': - resolution: {integrity: sha512-6284pl8c8n9PQidN63qjPVEu1uXXKjnmbmaLebOzIfTrSXdGiAPsIMRi4pk/+v/ezqweE1/B8bFqiAAfC6lMXg==} + '@next/env@16.0.10': + resolution: {integrity: sha512-8tuaQkyDVgeONQ1MeT9Mkk8pQmZapMKFh5B+OrFUlG3rVmYTXcXlBetBgTurKXGaIZvkoqRT9JL5K3phXcgang==} '@next/eslint-plugin-next@16.0.3': resolution: {integrity: sha512-6sPWmZetzFWMsz7Dhuxsdmbu3fK+/AxKRtj7OB0/3OZAI2MHB/v2FeYh271LZ9abvnM1WIwWc/5umYjx0jo5sQ==} - '@next/swc-darwin-arm64@16.0.9': - resolution: {integrity: sha512-j06fWg/gPqiWjK+sEpCDsh5gX+Bdy9gnPYjFqMBvBEOIcCFy1/ecF6pY6XAce7WyCJAbBPVb+6GvpmUZKNq0oQ==} + '@next/swc-darwin-arm64@16.0.10': + resolution: {integrity: sha512-4XgdKtdVsaflErz+B5XeG0T5PeXKDdruDf3CRpnhN+8UebNa5N2H58+3GDgpn/9GBurrQ1uWW768FfscwYkJRg==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@16.0.9': - resolution: {integrity: sha512-FRYYz5GSKUkfvDSjd5hgHME2LgYjfOLBmhRVltbs3oRNQQf9n5UTQMmIu/u5vpkjJFV4L2tqo8duGqDxdQOFwg==} + '@next/swc-darwin-x64@16.0.10': + resolution: {integrity: sha512-spbEObMvRKkQ3CkYVOME+ocPDFo5UqHb8EMTS78/0mQ+O1nqE8toHJVioZo4TvebATxgA8XMTHHrScPrn68OGw==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@16.0.9': - resolution: {integrity: sha512-EI2klFVL8tOyEIX5J1gXXpm1YuChmDy4R+tHoNjkCHUmBJqXioYErX/O2go4pEhjxkAxHp2i8y5aJcRz2m5NqQ==} + '@next/swc-linux-arm64-gnu@16.0.10': + resolution: {integrity: sha512-uQtWE3X0iGB8apTIskOMi2w/MKONrPOUCi5yLO+v3O8Mb5c7K4Q5KD1jvTpTF5gJKa3VH/ijKjKUq9O9UhwOYw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@16.0.9': - resolution: {integrity: sha512-vq/5HeGvowhDPMrpp/KP4GjPVhIXnwNeDPF5D6XK6ta96UIt+C0HwJwuHYlwmn0SWyNANqx1Mp6qSVDXwbFKsw==} + '@next/swc-linux-arm64-musl@16.0.10': + resolution: {integrity: sha512-llA+hiDTrYvyWI21Z0L1GiXwjQaanPVQQwru5peOgtooeJ8qx3tlqRV2P7uH2pKQaUfHxI/WVarvI5oYgGxaTw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@16.0.9': - resolution: {integrity: sha512-GlUdJwy2leA/HnyRYxJ1ZJLCJH+BxZfqV4E0iYLrJipDKxWejWpPtZUdccPmCfIEY9gNBO7bPfbG6IIgkt0qXg==} + '@next/swc-linux-x64-gnu@16.0.10': + resolution: {integrity: sha512-AK2q5H0+a9nsXbeZ3FZdMtbtu9jxW4R/NgzZ6+lrTm3d6Zb7jYrWcgjcpM1k8uuqlSy4xIyPR2YiuUr+wXsavA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@16.0.9': - resolution: {integrity: sha512-UCtOVx4N8AHF434VPwg4L0KkFLAd7pgJShzlX/hhv9+FDrT7/xCuVdlBsCXH7l9yCA/wHl3OqhMbIkgUluriWA==} + '@next/swc-linux-x64-musl@16.0.10': + resolution: {integrity: sha512-1TDG9PDKivNw5550S111gsO4RGennLVl9cipPhtkXIFVwo31YZ73nEbLjNC8qG3SgTz/QZyYyaFYMeY4BKZR/g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@16.0.9': - resolution: {integrity: sha512-tQjtDGtv63mV3n/cZ4TH8BgUvKTSFlrF06yT5DyRmgQuj5WEjBUDy0W3myIW5kTRYMPrLn42H3VfCNwBH6YYiA==} + '@next/swc-win32-arm64-msvc@16.0.10': + resolution: {integrity: sha512-aEZIS4Hh32xdJQbHz121pyuVZniSNoqDVx1yIr2hy+ZwJGipeqnMZBJHyMxv2tiuAXGx6/xpTcQJ6btIiBjgmg==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@16.0.9': - resolution: {integrity: sha512-y9AGACHTBwnWFLq5B5Fiv3FEbXBusdPb60pgoerB04CV/pwjY1xQNdoTNxAv7eUhU2k1CKnkN4XWVuiK07uOqA==} + '@next/swc-win32-x64-msvc@16.0.10': + resolution: {integrity: sha512-E+njfCoFLb01RAFEnGZn6ERoOqhK1Gl3Lfz1Kjnj0Ulfu7oJbuMyvBKNj/bw8XZnenHDASlygTjZICQW+rYW1Q==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -1498,8 +1498,8 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - next@16.0.9: - resolution: {integrity: sha512-Xk5x/wEk6ADIAtQECLo1uyE5OagbQCiZ+gW4XEv24FjQ3O2PdSkvgsn22aaseSXC7xg84oONvQjFbSTX5YsMhQ==} + next@16.0.10: + resolution: {integrity: sha512-RtWh5PUgI+vxlV3HdR+IfWA1UUHu0+Ram/JBO4vWB54cVPentCD0e+lxyAYEsDTqGGMg7qpjhKh6dc6aW7W/sA==} engines: {node: '>=20.9.0'} hasBin: true peerDependencies: @@ -2211,34 +2211,34 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true - '@next/env@16.0.9': {} + '@next/env@16.0.10': {} '@next/eslint-plugin-next@16.0.3': dependencies: fast-glob: 3.3.1 - '@next/swc-darwin-arm64@16.0.9': + '@next/swc-darwin-arm64@16.0.10': optional: true - '@next/swc-darwin-x64@16.0.9': + '@next/swc-darwin-x64@16.0.10': optional: true - '@next/swc-linux-arm64-gnu@16.0.9': + '@next/swc-linux-arm64-gnu@16.0.10': optional: true - '@next/swc-linux-arm64-musl@16.0.9': + '@next/swc-linux-arm64-musl@16.0.10': optional: true - '@next/swc-linux-x64-gnu@16.0.9': + '@next/swc-linux-x64-gnu@16.0.10': optional: true - '@next/swc-linux-x64-musl@16.0.9': + '@next/swc-linux-x64-musl@16.0.10': optional: true - '@next/swc-win32-arm64-msvc@16.0.9': + '@next/swc-win32-arm64-msvc@16.0.10': optional: true - '@next/swc-win32-x64-msvc@16.0.9': + '@next/swc-win32-x64-msvc@16.0.10': optional: true '@nodelib/fs.scandir@2.1.5': @@ -3477,9 +3477,9 @@ snapshots: natural-compare@1.4.0: {} - next@16.0.9(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + next@16.0.10(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: - '@next/env': 16.0.9 + '@next/env': 16.0.10 '@swc/helpers': 0.5.15 caniuse-lite: 1.0.30001756 postcss: 8.4.31 @@ -3487,14 +3487,14 @@ snapshots: react-dom: 19.2.0(react@19.2.0) styled-jsx: 5.1.6(@babel/core@7.28.5)(react@19.2.0) optionalDependencies: - '@next/swc-darwin-arm64': 16.0.9 - '@next/swc-darwin-x64': 16.0.9 - '@next/swc-linux-arm64-gnu': 16.0.9 - '@next/swc-linux-arm64-musl': 16.0.9 - '@next/swc-linux-x64-gnu': 16.0.9 - '@next/swc-linux-x64-musl': 16.0.9 - '@next/swc-win32-arm64-msvc': 16.0.9 - '@next/swc-win32-x64-msvc': 16.0.9 + '@next/swc-darwin-arm64': 16.0.10 + '@next/swc-darwin-x64': 16.0.10 + '@next/swc-linux-arm64-gnu': 16.0.10 + '@next/swc-linux-arm64-musl': 16.0.10 + '@next/swc-linux-x64-gnu': 16.0.10 + '@next/swc-linux-x64-musl': 16.0.10 + '@next/swc-win32-arm64-msvc': 16.0.10 + '@next/swc-win32-x64-msvc': 16.0.10 sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' diff --git a/package.json b/package.json index 563163c4a..54b593b89 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "lucide-react": "^0.552.0", "motion": "^12.23.24", "nanoid": "^5.1.6", - "next": "16.0.9", + "next": "16.0.10", "next-themes": "^0.4.6", "openai": "^6.8.1", "postgres": "^3.4.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 64aaecaae..818c9f09b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -34,7 +34,7 @@ importers: version: 7.12.0 '@vercel/analytics': specifier: ^1.5.0 - version: 1.5.0(next@16.0.9(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1) + version: 1.5.0(next@16.0.10(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1) '@vercel/og': specifier: ^0.8.5 version: 0.8.5 @@ -43,7 +43,7 @@ importers: version: 1.17.1 '@vercel/speed-insights': specifier: ^1.2.0 - version: 1.2.0(next@16.0.9(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1) + version: 1.2.0(next@16.0.10(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1) '@xyflow/react': specifier: ^12.9.2 version: 12.9.2(@types/react@19.2.2)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) @@ -52,7 +52,7 @@ importers: version: 5.0.102(zod@4.1.12) better-auth: specifier: ^1.3.34 - version: 1.3.34(next@16.0.9(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + version: 1.3.34(next@16.0.10(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -81,8 +81,8 @@ importers: specifier: ^5.1.6 version: 5.1.6 next: - specifier: 16.0.9 - version: 16.0.9(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + specifier: 16.0.10 + version: 16.0.10(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.2.1(react@19.2.1))(react@19.2.1) @@ -124,7 +124,7 @@ importers: version: 1.1.2(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) workflow: specifier: 4.0.1-beta.17 - version: 4.0.1-beta.17(@aws-sdk/client-sts@3.936.0)(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(next@16.0.9(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typescript@5.9.3) + version: 4.0.1-beta.17(@aws-sdk/client-sts@3.936.0)(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(next@16.0.10(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typescript@5.9.3) zod: specifier: ^4.1.12 version: 4.1.12 @@ -1059,8 +1059,8 @@ packages: '@next/env@15.5.4': resolution: {integrity: sha512-27SQhYp5QryzIT5uO8hq99C69eLQ7qkzkDPsk3N+GuS2XgOgoYEeOav7Pf8Tn4drECOVDsDg8oj+/DVy8qQL2A==} - '@next/env@16.0.9': - resolution: {integrity: sha512-6284pl8c8n9PQidN63qjPVEu1uXXKjnmbmaLebOzIfTrSXdGiAPsIMRi4pk/+v/ezqweE1/B8bFqiAAfC6lMXg==} + '@next/env@16.0.10': + resolution: {integrity: sha512-8tuaQkyDVgeONQ1MeT9Mkk8pQmZapMKFh5B+OrFUlG3rVmYTXcXlBetBgTurKXGaIZvkoqRT9JL5K3phXcgang==} '@next/swc-darwin-arm64@15.5.4': resolution: {integrity: sha512-nopqz+Ov6uvorej8ndRX6HlxCYWCO3AHLfKK2TYvxoSB2scETOcfm/HSS3piPqc3A+MUgyHoqE6je4wnkjfrOA==} @@ -1068,8 +1068,8 @@ packages: cpu: [arm64] os: [darwin] - '@next/swc-darwin-arm64@16.0.9': - resolution: {integrity: sha512-j06fWg/gPqiWjK+sEpCDsh5gX+Bdy9gnPYjFqMBvBEOIcCFy1/ecF6pY6XAce7WyCJAbBPVb+6GvpmUZKNq0oQ==} + '@next/swc-darwin-arm64@16.0.10': + resolution: {integrity: sha512-4XgdKtdVsaflErz+B5XeG0T5PeXKDdruDf3CRpnhN+8UebNa5N2H58+3GDgpn/9GBurrQ1uWW768FfscwYkJRg==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] @@ -1080,8 +1080,8 @@ packages: cpu: [x64] os: [darwin] - '@next/swc-darwin-x64@16.0.9': - resolution: {integrity: sha512-FRYYz5GSKUkfvDSjd5hgHME2LgYjfOLBmhRVltbs3oRNQQf9n5UTQMmIu/u5vpkjJFV4L2tqo8duGqDxdQOFwg==} + '@next/swc-darwin-x64@16.0.10': + resolution: {integrity: sha512-spbEObMvRKkQ3CkYVOME+ocPDFo5UqHb8EMTS78/0mQ+O1nqE8toHJVioZo4TvebATxgA8XMTHHrScPrn68OGw==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] @@ -1092,8 +1092,8 @@ packages: cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-gnu@16.0.9': - resolution: {integrity: sha512-EI2klFVL8tOyEIX5J1gXXpm1YuChmDy4R+tHoNjkCHUmBJqXioYErX/O2go4pEhjxkAxHp2i8y5aJcRz2m5NqQ==} + '@next/swc-linux-arm64-gnu@16.0.10': + resolution: {integrity: sha512-uQtWE3X0iGB8apTIskOMi2w/MKONrPOUCi5yLO+v3O8Mb5c7K4Q5KD1jvTpTF5gJKa3VH/ijKjKUq9O9UhwOYw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] @@ -1104,8 +1104,8 @@ packages: cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@16.0.9': - resolution: {integrity: sha512-vq/5HeGvowhDPMrpp/KP4GjPVhIXnwNeDPF5D6XK6ta96UIt+C0HwJwuHYlwmn0SWyNANqx1Mp6qSVDXwbFKsw==} + '@next/swc-linux-arm64-musl@16.0.10': + resolution: {integrity: sha512-llA+hiDTrYvyWI21Z0L1GiXwjQaanPVQQwru5peOgtooeJ8qx3tlqRV2P7uH2pKQaUfHxI/WVarvI5oYgGxaTw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] @@ -1116,8 +1116,8 @@ packages: cpu: [x64] os: [linux] - '@next/swc-linux-x64-gnu@16.0.9': - resolution: {integrity: sha512-GlUdJwy2leA/HnyRYxJ1ZJLCJH+BxZfqV4E0iYLrJipDKxWejWpPtZUdccPmCfIEY9gNBO7bPfbG6IIgkt0qXg==} + '@next/swc-linux-x64-gnu@16.0.10': + resolution: {integrity: sha512-AK2q5H0+a9nsXbeZ3FZdMtbtu9jxW4R/NgzZ6+lrTm3d6Zb7jYrWcgjcpM1k8uuqlSy4xIyPR2YiuUr+wXsavA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] @@ -1128,8 +1128,8 @@ packages: cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@16.0.9': - resolution: {integrity: sha512-UCtOVx4N8AHF434VPwg4L0KkFLAd7pgJShzlX/hhv9+FDrT7/xCuVdlBsCXH7l9yCA/wHl3OqhMbIkgUluriWA==} + '@next/swc-linux-x64-musl@16.0.10': + resolution: {integrity: sha512-1TDG9PDKivNw5550S111gsO4RGennLVl9cipPhtkXIFVwo31YZ73nEbLjNC8qG3SgTz/QZyYyaFYMeY4BKZR/g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] @@ -1140,8 +1140,8 @@ packages: cpu: [arm64] os: [win32] - '@next/swc-win32-arm64-msvc@16.0.9': - resolution: {integrity: sha512-tQjtDGtv63mV3n/cZ4TH8BgUvKTSFlrF06yT5DyRmgQuj5WEjBUDy0W3myIW5kTRYMPrLn42H3VfCNwBH6YYiA==} + '@next/swc-win32-arm64-msvc@16.0.10': + resolution: {integrity: sha512-aEZIS4Hh32xdJQbHz121pyuVZniSNoqDVx1yIr2hy+ZwJGipeqnMZBJHyMxv2tiuAXGx6/xpTcQJ6btIiBjgmg==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] @@ -1152,8 +1152,8 @@ packages: cpu: [x64] os: [win32] - '@next/swc-win32-x64-msvc@16.0.9': - resolution: {integrity: sha512-y9AGACHTBwnWFLq5B5Fiv3FEbXBusdPb60pgoerB04CV/pwjY1xQNdoTNxAv7eUhU2k1CKnkN4XWVuiK07uOqA==} + '@next/swc-win32-x64-msvc@16.0.10': + resolution: {integrity: sha512-E+njfCoFLb01RAFEnGZn6ERoOqhK1Gl3Lfz1Kjnj0Ulfu7oJbuMyvBKNj/bw8XZnenHDASlygTjZICQW+rYW1Q==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -3834,8 +3834,8 @@ packages: sass: optional: true - next@16.0.9: - resolution: {integrity: sha512-Xk5x/wEk6ADIAtQECLo1uyE5OagbQCiZ+gW4XEv24FjQ3O2PdSkvgsn22aaseSXC7xg84oONvQjFbSTX5YsMhQ==} + next@16.0.10: + resolution: {integrity: sha512-RtWh5PUgI+vxlV3HdR+IfWA1UUHu0+Ram/JBO4vWB54cVPentCD0e+lxyAYEsDTqGGMg7qpjhKh6dc6aW7W/sA==} engines: {node: '>=20.9.0'} hasBin: true peerDependencies: @@ -5684,54 +5684,54 @@ snapshots: '@next/env@15.5.4': {} - '@next/env@16.0.9': {} + '@next/env@16.0.10': {} '@next/swc-darwin-arm64@15.5.4': optional: true - '@next/swc-darwin-arm64@16.0.9': + '@next/swc-darwin-arm64@16.0.10': optional: true '@next/swc-darwin-x64@15.5.4': optional: true - '@next/swc-darwin-x64@16.0.9': + '@next/swc-darwin-x64@16.0.10': optional: true '@next/swc-linux-arm64-gnu@15.5.4': optional: true - '@next/swc-linux-arm64-gnu@16.0.9': + '@next/swc-linux-arm64-gnu@16.0.10': optional: true '@next/swc-linux-arm64-musl@15.5.4': optional: true - '@next/swc-linux-arm64-musl@16.0.9': + '@next/swc-linux-arm64-musl@16.0.10': optional: true '@next/swc-linux-x64-gnu@15.5.4': optional: true - '@next/swc-linux-x64-gnu@16.0.9': + '@next/swc-linux-x64-gnu@16.0.10': optional: true '@next/swc-linux-x64-musl@15.5.4': optional: true - '@next/swc-linux-x64-musl@16.0.9': + '@next/swc-linux-x64-musl@16.0.10': optional: true '@next/swc-win32-arm64-msvc@15.5.4': optional: true - '@next/swc-win32-arm64-msvc@16.0.9': + '@next/swc-win32-arm64-msvc@16.0.10': optional: true '@next/swc-win32-x64-msvc@15.5.4': optional: true - '@next/swc-win32-x64-msvc@16.0.9': + '@next/swc-win32-x64-msvc@16.0.10': optional: true '@noble/ciphers@2.0.1': {} @@ -7222,9 +7222,9 @@ snapshots: '@types/retry@0.12.0': {} - '@vercel/analytics@1.5.0(next@16.0.9(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)': + '@vercel/analytics@1.5.0(next@16.0.10(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)': optionalDependencies: - next: 16.0.9(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + next: 16.0.10(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) react: 19.2.1 '@vercel/functions@3.3.3(@aws-sdk/credential-provider-web-identity@3.609.0(@aws-sdk/client-sts@3.936.0))': @@ -7249,9 +7249,9 @@ snapshots: dependencies: zod: 4.1.12 - '@vercel/speed-insights@1.2.0(next@16.0.9(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)': + '@vercel/speed-insights@1.2.0(next@16.0.10(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)': optionalDependencies: - next: 16.0.9(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + next: 16.0.10(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) react: 19.2.1 '@workflow/builders@4.0.1-beta.13(@aws-sdk/client-sts@3.936.0)(@opentelemetry/api@1.9.0)': @@ -7347,7 +7347,7 @@ snapshots: '@workflow/utils': 4.0.1-beta.3 ms: 2.1.3 - '@workflow/next@4.0.1-beta.17(@aws-sdk/client-sts@3.936.0)(@opentelemetry/api@1.9.0)(next@16.0.9(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))': + '@workflow/next@4.0.1-beta.17(@aws-sdk/client-sts@3.936.0)(@opentelemetry/api@1.9.0)(next@16.0.10(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))': dependencies: '@swc/core': 1.11.24 '@workflow/builders': 4.0.1-beta.13(@aws-sdk/client-sts@3.936.0)(@opentelemetry/api@1.9.0) @@ -7356,7 +7356,7 @@ snapshots: semver: 7.7.3 watchpack: 2.4.4 optionalDependencies: - next: 16.0.9(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + next: 16.0.10(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) transitivePeerDependencies: - '@aws-sdk/client-sts' - '@opentelemetry/api' @@ -7543,7 +7543,7 @@ snapshots: baseline-browser-mapping@2.8.23: optional: true - better-auth@1.3.34(next@16.0.9(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1): + better-auth@1.3.34(next@16.0.10(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1): dependencies: '@better-auth/core': 1.3.34(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.0.19)(jose@6.1.0)(kysely@0.28.8)(nanostores@1.0.1) '@better-auth/telemetry': 1.3.34(better-call@1.0.19)(jose@6.1.0)(kysely@0.28.8)(nanostores@1.0.1) @@ -7560,7 +7560,7 @@ snapshots: nanostores: 1.0.1 zod: 4.1.12 optionalDependencies: - next: 16.0.9(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + next: 16.0.10(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) react: 19.2.1 react-dom: 19.2.1(react@19.2.1) @@ -8518,9 +8518,9 @@ snapshots: - '@babel/core' - babel-plugin-macros - next@16.0.9(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1): + next@16.0.10(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1): dependencies: - '@next/env': 16.0.9 + '@next/env': 16.0.10 '@swc/helpers': 0.5.15 caniuse-lite: 1.0.30001753 postcss: 8.4.31 @@ -8528,14 +8528,14 @@ snapshots: react-dom: 19.2.1(react@19.2.1) styled-jsx: 5.1.6(@babel/core@7.28.5)(react@19.2.1) optionalDependencies: - '@next/swc-darwin-arm64': 16.0.9 - '@next/swc-darwin-x64': 16.0.9 - '@next/swc-linux-arm64-gnu': 16.0.9 - '@next/swc-linux-arm64-musl': 16.0.9 - '@next/swc-linux-x64-gnu': 16.0.9 - '@next/swc-linux-x64-musl': 16.0.9 - '@next/swc-win32-arm64-msvc': 16.0.9 - '@next/swc-win32-x64-msvc': 16.0.9 + '@next/swc-darwin-arm64': 16.0.10 + '@next/swc-darwin-x64': 16.0.10 + '@next/swc-linux-arm64-gnu': 16.0.10 + '@next/swc-linux-arm64-musl': 16.0.10 + '@next/swc-linux-x64-gnu': 16.0.10 + '@next/swc-linux-x64-musl': 16.0.10 + '@next/swc-win32-arm64-msvc': 16.0.10 + '@next/swc-win32-x64-msvc': 16.0.10 '@opentelemetry/api': 1.9.0 '@playwright/test': 1.57.0 sharp: 0.34.4 @@ -9270,12 +9270,12 @@ snapshots: wordwrap@1.0.0: {} - workflow@4.0.1-beta.17(@aws-sdk/client-sts@3.936.0)(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(next@16.0.9(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typescript@5.9.3): + workflow@4.0.1-beta.17(@aws-sdk/client-sts@3.936.0)(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(next@16.0.10(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typescript@5.9.3): dependencies: '@workflow/cli': 4.0.1-beta.17(@aws-sdk/client-sts@3.936.0)(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typescript@5.9.3) '@workflow/core': 4.0.1-beta.15(@aws-sdk/client-sts@3.936.0)(@opentelemetry/api@1.9.0) '@workflow/errors': 4.0.1-beta.5 - '@workflow/next': 4.0.1-beta.17(@aws-sdk/client-sts@3.936.0)(@opentelemetry/api@1.9.0)(next@16.0.9(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)) + '@workflow/next': 4.0.1-beta.17(@aws-sdk/client-sts@3.936.0)(@opentelemetry/api@1.9.0)(next@16.0.10(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)) '@workflow/nitro': 4.0.1-beta.17(@aws-sdk/client-sts@3.936.0)(@opentelemetry/api@1.9.0) '@workflow/nuxt': 4.0.1-beta.6(@aws-sdk/client-sts@3.936.0)(@opentelemetry/api@1.9.0) '@workflow/sveltekit': 4.0.0-beta.11(@aws-sdk/client-sts@3.936.0)(@opentelemetry/api@1.9.0) From d32b3cca209ad95a3caeb4d834732648a73ada4d Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 12 Dec 2025 00:05:02 -0600 Subject: [PATCH 03/16] better ux for connections (#158) * better connections dx * better ux * improvements * fixes * consistent dialogs * fixes * auto-select * fixes * fixes loop * fixes * fixes --- app/api/integrations/route.ts | 8 +- app/api/integrations/test/route.ts | 128 ++++ components/settings/api-keys-dialog.tsx | 35 +- components/settings/index.tsx | 23 +- .../settings/integration-form-dialog.tsx | 652 ++++++++++++++---- components/settings/integrations-dialog.tsx | 9 +- components/settings/integrations-manager.tsx | 168 ++--- components/ui/integration-selector.tsx | 283 +++++--- components/ui/tooltip.tsx | 61 ++ .../config/action-config-renderer.tsx | 1 + components/workflow/config/action-config.tsx | 86 ++- components/workflow/config/action-grid.tsx | 88 ++- components/workflow/node-config-panel.tsx | 225 +++--- components/workflow/workflow-toolbar.tsx | 239 ++++--- components/workflows/user-menu.tsx | 2 +- lib/api-client.ts | 15 +- package.json | 1 + pnpm-lock.yaml | 3 + 18 files changed, 1396 insertions(+), 631 deletions(-) create mode 100644 app/api/integrations/test/route.ts create mode 100644 components/ui/tooltip.tsx diff --git a/app/api/integrations/route.ts b/app/api/integrations/route.ts index f159dd809..6d81d56c0 100644 --- a/app/api/integrations/route.ts +++ b/app/api/integrations/route.ts @@ -16,7 +16,7 @@ export type GetIntegrationsResponse = { }[]; export type CreateIntegrationRequest = { - name: string; + name?: string; type: IntegrationType; config: IntegrationConfig; }; @@ -92,16 +92,16 @@ export async function POST(request: Request) { const body: CreateIntegrationRequest = await request.json(); - if (!(body.name && body.type && body.config)) { + if (!(body.type && body.config)) { return NextResponse.json( - { error: "Name, type, and config are required" }, + { error: "Type and config are required" }, { status: 400 } ); } const integration = await createIntegration( session.user.id, - body.name, + body.name || "", body.type, body.config ); diff --git a/app/api/integrations/test/route.ts b/app/api/integrations/test/route.ts new file mode 100644 index 000000000..fc7838e5c --- /dev/null +++ b/app/api/integrations/test/route.ts @@ -0,0 +1,128 @@ +import { NextResponse } from "next/server"; +import postgres from "postgres"; +import { auth } from "@/lib/auth"; +import type { + IntegrationConfig, + IntegrationType, +} from "@/lib/types/integration"; +import { + getCredentialMapping, + getIntegration as getPluginFromRegistry, +} from "@/plugins"; + +export type TestConnectionRequest = { + type: IntegrationType; + config: IntegrationConfig; +}; + +export type TestConnectionResult = { + status: "success" | "error"; + message: string; +}; + +/** + * POST /api/integrations/test + * Test connection credentials without saving + */ +export async function POST(request: Request) { + try { + const session = await auth.api.getSession({ + headers: request.headers, + }); + + if (!session?.user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const body: TestConnectionRequest = await request.json(); + + if (!(body.type && body.config)) { + return NextResponse.json( + { error: "Type and config are required" }, + { status: 400 } + ); + } + + if (body.type === "database") { + const result = await testDatabaseConnection(body.config.url); + return NextResponse.json(result); + } + + const plugin = getPluginFromRegistry(body.type); + + if (!plugin) { + return NextResponse.json( + { error: "Invalid integration type" }, + { status: 400 } + ); + } + + if (!plugin.testConfig) { + return NextResponse.json( + { error: "Integration does not support testing" }, + { status: 400 } + ); + } + + const credentials = getCredentialMapping(plugin, body.config); + + const testFn = await plugin.testConfig.getTestFunction(); + const testResult = await testFn(credentials); + + const result: TestConnectionResult = { + status: testResult.success ? "success" : "error", + message: testResult.success + ? "Connection successful" + : testResult.error || "Connection failed", + }; + + return NextResponse.json(result); + } catch (error) { + console.error("Failed to test connection:", error); + return NextResponse.json( + { + status: "error", + message: + error instanceof Error ? error.message : "Failed to test connection", + }, + { status: 500 } + ); + } +} + +async function testDatabaseConnection( + databaseUrl?: string +): Promise { + let connection: postgres.Sql | null = null; + + try { + if (!databaseUrl) { + return { + status: "error", + message: "Connection failed", + }; + } + + connection = postgres(databaseUrl, { + max: 1, + idle_timeout: 5, + connect_timeout: 5, + }); + + await connection`SELECT 1`; + + return { + status: "success", + message: "Connection successful", + }; + } catch { + return { + status: "error", + message: "Connection failed", + }; + } finally { + if (connection) { + await connection.end(); + } + } +} diff --git a/components/settings/api-keys-dialog.tsx b/components/settings/api-keys-dialog.tsx index d36339631..f40053b5a 100644 --- a/components/settings/api-keys-dialog.tsx +++ b/components/settings/api-keys-dialog.tsx @@ -253,25 +253,38 @@ export function ApiKeysDialog({ open, onOpenChange }: ApiKeysDialogProps) { Create a new API key for webhook authentication -
-
- - setNewKeyName(e.target.value)} - placeholder="e.g., Production, Testing" - value={newKeyName} - /> +
{ + e.preventDefault(); + handleCreate(); + }} + > +
+
+ + setNewKeyName(e.target.value)} + placeholder="e.g., Production, Testing" + value={newKeyName} + /> +
-
+ - diff --git a/components/settings/index.tsx b/components/settings/index.tsx index 5e411465a..df9d9d313 100644 --- a/components/settings/index.tsx +++ b/components/settings/index.tsx @@ -83,21 +83,36 @@ export function SettingsDialog({ open, onOpenChange }: SettingsDialogProps) {
) : ( -
+
{ + e.preventDefault(); + saveAccount(); + }} + > -
+ )} - - diff --git a/components/settings/integration-form-dialog.tsx b/components/settings/integration-form-dialog.tsx index 14be45ddf..5cf37e18b 100644 --- a/components/settings/integration-form-dialog.tsx +++ b/components/settings/integration-form-dialog.tsx @@ -1,8 +1,28 @@ "use client"; -import { ArrowLeft } from "lucide-react"; -import { useEffect, useState } from "react"; +import { + ArrowLeft, + Check, + CheckCircle2, + Pencil, + Search, + Trash2, + X, + XCircle, + Zap, +} from "lucide-react"; +import { useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -18,7 +38,6 @@ import { Label } from "@/components/ui/label"; import { Spinner } from "@/components/ui/spinner"; import { api, type Integration } from "@/lib/api-client"; import type { IntegrationType } from "@/lib/types/integration"; -import { cn } from "@/lib/utils"; import { getIntegration, getIntegrationLabels, @@ -29,6 +48,7 @@ type IntegrationFormDialogProps = { open: boolean; onClose: () => void; onSuccess?: (integrationId: string) => void; + onDelete?: () => void; integration?: Integration | null; mode: "create" | "edit"; preselectedType?: IntegrationType; @@ -56,27 +76,400 @@ const getIntegrationTypes = (): IntegrationType[] => [ const getLabel = (type: IntegrationType): string => getIntegrationLabels()[type] || SYSTEM_INTEGRATION_LABELS[type] || type; +function SecretField({ + fieldId, + label, + configKey, + placeholder, + helpText, + helpLink, + value, + onChange, + isEditMode, +}: { + fieldId: string; + label: string; + configKey: string; + placeholder?: string; + helpText?: string; + helpLink?: { url: string; text: string }; + value: string; + onChange: (key: string, value: string) => void; + isEditMode: boolean; +}) { + const [isEditing, setIsEditing] = useState(!isEditMode); + const hasNewValue = value.length > 0; + + // In edit mode, start with "configured" state + // User can click to change, or clear after entering a new value + if (isEditMode && !isEditing && !hasNewValue) { + return ( +
+ +
+
+ + Configured +
+ +
+
+ ); + } + + return ( +
+ +
+ onChange(configKey, e.target.value)} + placeholder={placeholder} + type="password" + value={value} + /> + {isEditMode && (isEditing || hasNewValue) && ( + + )} +
+ {(helpText || helpLink) && ( +

+ {helpText} + {helpLink && ( + + {helpLink.text} + + )} +

+ )} +
+ ); +} + +function ConfigFields({ + formData, + updateConfig, + isEditMode, +}: { + formData: IntegrationFormData; + updateConfig: (key: string, value: string) => void; + isEditMode: boolean; +}) { + if (!formData.type) { + return null; + } + + // Handle system integrations with hardcoded fields + if (formData.type === "database") { + return ( + + ); + } + + // Get plugin form fields from registry + const plugin = getIntegration(formData.type); + if (!plugin?.formFields) { + return null; + } + + return plugin.formFields.map((field) => { + const isSecretField = field.type === "password"; + + if (isSecretField) { + return ( + + ); + } + + return ( +
+ + updateConfig(field.configKey, e.target.value)} + placeholder={field.placeholder} + type={field.type} + value={formData.config[field.configKey] || ""} + /> + {(field.helpText || field.helpLink) && ( +

+ {field.helpText} + {field.helpLink && ( + + {field.helpLink.text} + + )} +

+ )} +
+ ); + }); +} + +function FormFooterActions({ + step, + mode, + preselectedType, + saving, + deleting, + testing, + testResult, + onBack, + onDelete, + onTestConnection, + onClose, +}: { + step: "select" | "configure"; + mode: "create" | "edit"; + preselectedType?: IntegrationType; + saving: boolean; + deleting: boolean; + testing: boolean; + testResult: { status: "success" | "error"; message: string } | null; + onBack: () => void; + onDelete: () => void; + onTestConnection: () => void; + onClose: () => void; +}) { + if (step === "select") { + return ( + + ); + } + + return ( + <> +
+ {mode === "create" && !preselectedType && ( + + )} + {mode === "edit" && ( + + )} + +
+
+ + +
+ + ); +} + +function TestConnectionIcon({ + testing, + testResult, +}: { + testing: boolean; + testResult: { status: "success" | "error"; message: string } | null; +}) { + if (testing) { + return ; + } + if (testResult?.status === "success") { + return ; + } + if (testResult?.status === "error") { + return ; + } + return ; +} + +function DeleteConfirmDialog({ + open, + onOpenChange, + deleting, + onDelete, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; + deleting: boolean; + onDelete: () => void; +}) { + return ( + + + + Delete Connection + + Are you sure you want to delete this connection? Workflows using it + will fail until a new one is configured. + + + + Cancel + + {deleting ? : null} + Delete + + + + + ); +} + +function TypeSelector({ + searchQuery, + onSearchChange, + filteredTypes, + onSelectType, +}: { + searchQuery: string; + onSearchChange: (value: string) => void; + filteredTypes: IntegrationType[]; + onSelectType: (type: IntegrationType) => void; +}) { + return ( +
+
+ + onSearchChange(e.target.value)} + placeholder="Search services..." + value={searchQuery} + /> +
+
+ {filteredTypes.length === 0 ? ( +

+ No services found +

+ ) : ( + filteredTypes.map((type) => ( + + )) + )} +
+
+ ); +} + export function IntegrationFormDialog({ open, onClose, onSuccess, + onDelete, integration, mode, preselectedType, }: IntegrationFormDialogProps) { const [saving, setSaving] = useState(false); + const [deleting, setDeleting] = useState(false); + const [testing, setTesting] = useState(false); + const [testResult, setTestResult] = useState<{ + status: "success" | "error"; + message: string; + } | null>(null); + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [searchQuery, setSearchQuery] = useState(""); const [formData, setFormData] = useState({ name: "", type: preselectedType || null, config: {}, }); - // Step: "select" for type selection grid, "configure" for form + // Step: "select" for type selection list, "configure" for form const [step, setStep] = useState<"select" | "configure">( preselectedType || mode === "edit" ? "configure" : "select" ); useEffect(() => { + setTestResult(null); if (integration) { setFormData({ name: integration.name, @@ -105,6 +498,7 @@ export function IntegrationFormDialog({ const handleBack = () => { setStep("select"); + setSearchQuery(""); setFormData({ name: "", type: null, @@ -120,16 +514,18 @@ export function IntegrationFormDialog({ try { setSaving(true); - // Generate a default name if none provided - const integrationName = - formData.name.trim() || `${getLabel(formData.type)} Integration`; + const integrationName = formData.name.trim(); if (mode === "edit" && integration) { + // Only include config if there are actual new values entered + const hasNewConfig = Object.values(formData.config).some( + (v) => v && v.length > 0 + ); await api.integration.update(integration.id, { name: integrationName, - config: formData.config, + ...(hasNewConfig ? { config: formData.config } : {}), }); - toast.success("Integration updated"); + toast.success("Connection updated"); onSuccess?.(integration.id); } else { const newIntegration = await api.integration.create({ @@ -148,178 +544,176 @@ export function IntegrationFormDialog({ } }; - const updateConfig = (key: string, value: string) => { - setFormData({ - ...formData, - config: { ...formData.config, [key]: value }, - }); + const handleDelete = async () => { + if (!integration) { + return; + } + + try { + setDeleting(true); + await api.integration.delete(integration.id); + toast.success("Connection deleted"); + onDelete?.(); + onClose(); + } catch (error) { + console.error("Failed to delete integration:", error); + toast.error("Failed to delete connection"); + } finally { + setDeleting(false); + setShowDeleteConfirm(false); + } }; - const renderConfigFields = () => { + const handleTestConnection = async () => { if (!formData.type) { - return null; + return; } - // Handle system integrations with hardcoded fields - if (formData.type === "database") { - return ( -
- - updateConfig("url", e.target.value)} - placeholder="postgresql://..." - type="password" - value={formData.config.url || ""} - /> -

- Connection string in the format: - postgresql://user:password@host:port/database -

-
- ); + // Check if we have any config values to test + const hasConfig = Object.values(formData.config).some( + (v) => v && v.length > 0 + ); + if (!hasConfig && mode === "create") { + toast.error("Please enter credentials first"); + return; } - // Get plugin form fields from registry - const plugin = getIntegration(formData.type); - if (!plugin?.formFields) { - return null; + try { + setTesting(true); + setTestResult(null); + + let result: { status: "success" | "error"; message: string }; + + if (mode === "edit" && integration && !hasConfig) { + // Test existing integration (no new config entered) + result = await api.integration.testConnection(integration.id); + } else { + // Test with new credentials + result = await api.integration.testCredentials({ + type: formData.type, + config: formData.config, + }); + } + + setTestResult(result); + } catch (error) { + console.error("Failed to test connection:", error); + const message = + error instanceof Error ? error.message : "Failed to test connection"; + setTestResult({ status: "error", message }); + } finally { + setTesting(false); } + }; - return plugin.formFields.map((field) => ( -
- - updateConfig(field.configKey, e.target.value)} - placeholder={field.placeholder} - type={field.type} - value={formData.config[field.configKey] || ""} - /> - {(field.helpText || field.helpLink) && ( -

- {field.helpText} - {field.helpLink && ( - - {field.helpLink.text} - - )} -

- )} -
- )); + const updateConfig = (key: string, value: string) => { + setFormData({ + ...formData, + config: { ...formData.config, [key]: value }, + }); }; const integrationTypes = getIntegrationTypes(); + const filteredIntegrationTypes = useMemo(() => { + if (!searchQuery.trim()) { + return integrationTypes; + } + const query = searchQuery.toLowerCase(); + return integrationTypes.filter((type) => + getLabel(type).toLowerCase().includes(query) + ); + }, [integrationTypes, searchQuery]); + const getDialogTitle = () => { if (mode === "edit") { - return "Edit Integration"; + return "Edit Connection"; } if (step === "select") { - return "Choose Integration"; + return "Add Connection"; } - return `Add ${formData.type ? getLabel(formData.type) : ""} Integration`; + return `Add ${formData.type ? getLabel(formData.type) : ""} Connection`; }; const getDialogDescription = () => { if (mode === "edit") { - return "Update integration configuration"; + return "Update your connection credentials"; } if (step === "select") { - return "Select an integration type to configure"; + return "Select a service to connect"; } - return "Configure your integration"; + return "Enter your credentials"; }; return ( !isOpen && onClose()} open={open}> - + {getDialogTitle()} {getDialogDescription()} {step === "select" ? ( -
- {integrationTypes.map((type) => ( - - ))} -
+ ) : ( -
- {renderConfigFields()} +
{ + e.preventDefault(); + handleSave(); + }} + > +
- + setFormData({ ...formData, name: e.target.value }) } - placeholder={ - formData.type - ? `${getLabel(formData.type)} Integration` - : "Integration" - } + placeholder="e.g. Production, Personal, Work" value={formData.name} />
-
+ )} - {step === "configure" && mode === "create" && !preselectedType && ( - - )} - {step === "select" ? ( - - ) : ( -
- - -
- )} + setShowDeleteConfirm(true)} + onTestConnection={handleTestConnection} + preselectedType={preselectedType} + saving={saving} + step={step} + testing={testing} + testResult={testResult} + />
+ +
); } diff --git a/components/settings/integrations-dialog.tsx b/components/settings/integrations-dialog.tsx index 443a080a2..f992f4f7c 100644 --- a/components/settings/integrations-dialog.tsx +++ b/components/settings/integrations-dialog.tsx @@ -73,9 +73,9 @@ export function IntegrationsDialog({ showCloseButton={false} > - Integrations + Connections - Manage your integrations that can be used across workflows + Manage API keys and credentials used by your workflows @@ -86,16 +86,17 @@ export function IntegrationsDialog({ ) : (
setShowCreateDialog(false)} onIntegrationChange={handleIntegrationChange} showCreateDialog={showCreateDialog} />
)} - + diff --git a/components/settings/integrations-manager.tsx b/components/settings/integrations-manager.tsx index 821e623ba..395106765 100644 --- a/components/settings/integrations-manager.tsx +++ b/components/settings/integrations-manager.tsx @@ -1,6 +1,6 @@ "use client"; -import { ChevronRight, Pencil, Trash2 } from "lucide-react"; +import { Pencil, Trash2 } from "lucide-react"; import { useCallback, useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; import { @@ -14,15 +14,9 @@ import { AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from "@/components/ui/collapsible"; import { IntegrationIcon } from "@/components/ui/integration-icon"; import { Spinner } from "@/components/ui/spinner"; import { api, type Integration } from "@/lib/api-client"; -import { cn } from "@/lib/utils"; import { getIntegrationLabels } from "@/plugins"; import { IntegrationFormDialog } from "./integration-form-dialog"; @@ -33,11 +27,13 @@ const SYSTEM_INTEGRATION_LABELS: Record = { type IntegrationsManagerProps = { showCreateDialog: boolean; + onCreateDialogClose?: () => void; onIntegrationChange?: () => void; }; export function IntegrationsManager({ showCreateDialog: externalShowCreateDialog, + onCreateDialogClose, onIntegrationChange, }: IntegrationsManagerProps) { const [integrations, setIntegrations] = useState([]); @@ -47,7 +43,6 @@ export function IntegrationsManager({ const [showCreateDialog, setShowCreateDialog] = useState(false); const [deletingId, setDeletingId] = useState(null); const [testingId, setTestingId] = useState(null); - const [expandedGroups, setExpandedGroups] = useState>(new Set()); // Sync external dialog state useEffect(() => { @@ -71,27 +66,25 @@ export function IntegrationsManager({ loadIntegrations(); }, [loadIntegrations]); - // Group integrations by type - const groupedIntegrations = useMemo(() => { - const groups = new Map(); + // Get integrations with their labels, sorted by label then name + const integrationsWithLabels = useMemo(() => { const labels = getIntegrationLabels() as Record; - for (const integration of integrations) { - const type = integration.type; - if (!groups.has(type)) { - groups.set(type, []); - } - groups.get(type)?.push(integration); - } - - // Sort groups by label - return Array.from(groups.entries()) - .map(([type, items]) => ({ - type, - label: labels[type] || SYSTEM_INTEGRATION_LABELS[type] || type, - items, + return integrations + .map((integration) => ({ + ...integration, + label: + labels[integration.type] || + SYSTEM_INTEGRATION_LABELS[integration.type] || + integration.type, })) - .sort((a, b) => a.label.localeCompare(b.label)); + .sort((a, b) => { + const labelCompare = a.label.localeCompare(b.label); + if (labelCompare !== 0) { + return labelCompare; + } + return a.name.localeCompare(b.name); + }); }, [integrations]); const handleDelete = async (id: string) => { @@ -130,6 +123,7 @@ export function IntegrationsManager({ const handleDialogClose = () => { setShowCreateDialog(false); setEditingIntegration(null); + onCreateDialogClose?.(); }; const handleDialogSuccess = async () => { @@ -137,18 +131,6 @@ export function IntegrationsManager({ onIntegrationChange?.(); }; - const toggleGroup = (type: string) => { - setExpandedGroups((prev) => { - const next = new Set(prev); - if (next.has(type)) { - next.delete(type); - } else { - next.add(type); - } - return next; - }); - }; - if (loading) { return (
@@ -162,76 +144,62 @@ export function IntegrationsManager({ {integrations.length === 0 ? (

- No integrations configured yet + No connections configured yet

) : (
- {groupedIntegrations.map((group) => ( - toggleGroup(group.type)} - open={expandedGroups.has(group.type)} + {integrationsWithLabels.map((integration) => ( +
- +
- {group.label} - {integration.label} + + {integration.name} + +
+
+ - - -
-
- ))} -
- - + + + +
+ ))} )} @@ -256,10 +224,10 @@ export function IntegrationsManager({ > - Delete Integration + Delete Connection - Are you sure you want to delete this integration? Workflows using - this integration will fail until a new one is selected. + Are you sure you want to delete this connection? Workflows using + it will fail until a new one is configured. diff --git a/components/ui/integration-selector.tsx b/components/ui/integration-selector.tsx index 4d5323edf..eb5c954e7 100644 --- a/components/ui/integration-selector.tsx +++ b/components/ui/integration-selector.tsx @@ -1,22 +1,17 @@ "use client"; -import { useAtomValue, useSetAtom } from "jotai"; -import { AlertTriangle } from "lucide-react"; -import { useEffect, useState } from "react"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { Separator } from "@/components/ui/separator"; +import { useAtom, useAtomValue, useSetAtom } from "jotai"; +import { AlertTriangle, Check, Circle, Pencil, Plus, Settings } from "lucide-react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { Button } from "@/components/ui/button"; import { api, type Integration } from "@/lib/api-client"; import { integrationsAtom, integrationsVersionAtom, } from "@/lib/integrations-store"; import type { IntegrationType } from "@/lib/types/integration"; +import { cn } from "@/lib/utils"; +import { getIntegration } from "@/plugins"; import { IntegrationFormDialog } from "@/components/settings/integration-form-dialog"; type IntegrationSelectorProps = { @@ -24,8 +19,8 @@ type IntegrationSelectorProps = { value?: string; onChange: (integrationId: string) => void; onOpenSettings?: () => void; - label?: string; disabled?: boolean; + onAddConnection?: () => void; }; export function IntegrationSelector({ @@ -33,87 +28,113 @@ export function IntegrationSelector({ value, onChange, onOpenSettings, - label, disabled, + onAddConnection, }: IntegrationSelectorProps) { - const [integrations, setIntegrations] = useState([]); - const [loading, setLoading] = useState(true); const [showNewDialog, setShowNewDialog] = useState(false); + const [editingIntegration, setEditingIntegration] = + useState(null); + const [globalIntegrations, setGlobalIntegrations] = useAtom(integrationsAtom); const integrationsVersion = useAtomValue(integrationsVersionAtom); - const setGlobalIntegrations = useSetAtom(integrationsAtom); const setIntegrationsVersion = useSetAtom(integrationsVersionAtom); + const lastVersionRef = useRef(integrationsVersion); + const [hasFetched, setHasFetched] = useState(false); + + // Filter integrations from global cache + const integrations = useMemo( + () => globalIntegrations.filter((i) => i.type === integrationType), + [globalIntegrations, integrationType] + ); - const loadIntegrations = async () => { + // Check if we have cached data + const hasCachedData = globalIntegrations.length > 0; + + const loadIntegrations = useCallback(async () => { try { - setLoading(true); const all = await api.integration.getAll(); // Update global store so other components can access it setGlobalIntegrations(all); - const filtered = all.filter((i) => i.type === integrationType); - setIntegrations(filtered); - - // Auto-select if only one option and nothing selected yet - if (filtered.length === 1 && !value) { - onChange(filtered[0].id); - } + setHasFetched(true); } catch (error) { console.error("Failed to load integrations:", error); - } finally { - setLoading(false); } - }; + }, [setGlobalIntegrations]); useEffect(() => { loadIntegrations(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [integrationType, integrationsVersion]); + }, [loadIntegrations, integrationType]); - const handleValueChange = (newValue: string) => { - if (newValue === "__new__") { - setShowNewDialog(true); - } else if (newValue === "__manage__") { - onOpenSettings?.(); - } else { - onChange(newValue); + // Listen for version changes (from other components creating/editing integrations) + useEffect(() => { + // Skip initial render - only react to actual version changes + if (integrationsVersion !== lastVersionRef.current) { + lastVersionRef.current = integrationsVersion; + loadIntegrations(); } - }; + }, [integrationsVersion, loadIntegrations]); + + // Auto-select single integration from cached data + useEffect(() => { + if (integrations.length === 1 && !value && !disabled) { + onChange(integrations[0].id); + } + }, [integrations, value, disabled, onChange]); const handleNewIntegrationCreated = async (integrationId: string) => { await loadIntegrations(); onChange(integrationId); setShowNewDialog(false); - // Increment version to trigger auto-fix for other nodes that need this integration type + // Increment version to trigger re-fetch in other selectors setIntegrationsVersion((v) => v + 1); }; - if (loading) { + const handleEditSuccess = async () => { + await loadIntegrations(); + setEditingIntegration(null); + setIntegrationsVersion((v) => v + 1); + }; + + const handleAddConnection = () => { + if (onAddConnection) { + onAddConnection(); + } else { + setShowNewDialog(true); + } + }; + + // Only show loading skeleton if we have no cached data and haven't fetched yet + if (!hasCachedData && !hasFetched) { return ( - +
+
+
+
+
+
+
); } + const plugin = getIntegration(integrationType); + const integrationLabel = plugin?.label || integrationType; + + // No integrations - show error button to add one if (integrations.length === 0) { return ( -
- - + <> + + setShowNewDialog(false)} @@ -121,29 +142,114 @@ export function IntegrationSelector({ open={showNewDialog} preselectedType={integrationType} /> -
+ ); } + // Single integration - show as outlined field (not radio-style) + if (integrations.length === 1) { + const integration = integrations[0]; + const displayName = integration.name || `${integrationLabel} API Key`; + + return ( + <> +
+ + {displayName} + +
+ + {editingIntegration && ( + setEditingIntegration(null)} + onDelete={async () => { + await loadIntegrations(); + setEditingIntegration(null); + setIntegrationsVersion((v) => v + 1); + }} + onSuccess={handleEditSuccess} + open + /> + )} + + ); + } + + // Multiple integrations - show radio-style selection list return ( -
- {label && {label}} - - + <> +
+ {integrations.map((integration) => { + const isSelected = value === integration.id; + const displayName = + integration.name || `${integrationLabel} API Key`; + return ( +
+ + +
+ ); + })} + {onOpenSettings && ( + + )} +
+ setShowNewDialog(false)} @@ -151,7 +257,22 @@ export function IntegrationSelector({ open={showNewDialog} preselectedType={integrationType} /> -
+ + {editingIntegration && ( + setEditingIntegration(null)} + onDelete={async () => { + await loadIntegrations(); + setEditingIntegration(null); + setIntegrationsVersion((v) => v + 1); + }} + onSuccess={handleEditSuccess} + open + /> + )} + ); } diff --git a/components/ui/tooltip.tsx b/components/ui/tooltip.tsx new file mode 100644 index 000000000..a4e90d4e9 --- /dev/null +++ b/components/ui/tooltip.tsx @@ -0,0 +1,61 @@ +"use client" + +import * as React from "react" +import * as TooltipPrimitive from "@radix-ui/react-tooltip" + +import { cn } from "@/lib/utils" + +function TooltipProvider({ + delayDuration = 0, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function Tooltip({ + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function TooltipTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function TooltipContent({ + className, + sideOffset = 0, + children, + ...props +}: React.ComponentProps) { + return ( + + + {children} + + + + ) +} + +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } diff --git a/components/workflow/config/action-config-renderer.tsx b/components/workflow/config/action-config-renderer.tsx index 030c20baa..7705763fa 100644 --- a/components/workflow/config/action-config-renderer.tsx +++ b/components/workflow/config/action-config-renderer.tsx @@ -151,6 +151,7 @@ function renderField(
= [ const SYSTEM_ACTION_IDS = SYSTEM_ACTIONS.map((a) => a.id); +// System actions that need integrations (not in plugin registry) +const SYSTEM_ACTION_INTEGRATIONS: Record = { + "Database Query": "database", +}; + // Build category mapping dynamically from plugins + System function useCategoryData() { return useMemo(() => { @@ -303,12 +324,28 @@ export function ActionConfig({ // Get dynamic config fields for plugin actions const pluginAction = actionType ? findActionById(actionType) : null; + // Determine the integration type for the current action + const integrationType: IntegrationType | undefined = useMemo(() => { + if (!actionType) { + return; + } + + // Check system actions first + if (SYSTEM_ACTION_INTEGRATIONS[actionType]) { + return SYSTEM_ACTION_INTEGRATIONS[actionType]; + } + + // Check plugin actions + const action = findActionById(actionType); + return action?.integration as IntegrationType | undefined; + }, [actionType]); + return ( <>
setFilter(e.target.value)} - placeholder="Search actions..." - ref={inputRef} - value={filter} - /> -
+
+
+ + setFilter(e.target.value)} + placeholder="Search actions..." + ref={inputRef} + value={filter} + />
-
- {filteredActions.map((action) => ( - - ))} +
+ {filteredActions.length === 0 ? ( +

+ No actions found +

+ ) : ( + filteredActions.map((action) => ( + + )) + )}
- - {filteredActions.length === 0 && ( -

- No actions found -

- )}
); } diff --git a/components/workflow/node-config-panel.tsx b/components/workflow/node-config-panel.tsx index 8942683ec..c635b0bff 100644 --- a/components/workflow/node-config-panel.tsx +++ b/components/workflow/node-config-panel.tsx @@ -53,7 +53,6 @@ import { findActionById } from "@/plugins"; import { Panel } from "../ai-elements/panel"; import { IntegrationsDialog } from "../settings/integrations-dialog"; import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer"; -import { IntegrationSelector } from "../ui/integration-selector"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs"; import { ActionConfig } from "./config/action-config"; import { ActionGrid } from "./config/action-grid"; @@ -775,19 +774,11 @@ export const PanelInner = () => { className="flex flex-col overflow-hidden" value="properties" > -
- {selectedNode.data.type === "trigger" && ( - - )} - - {selectedNode.data.type === "action" && - !selectedNode.data.config?.actionType && - isOwner && ( + {/* Action selection - full height flex layout */} + {selectedNode.data.type === "action" && + !selectedNode.data.config?.actionType && + isOwner && ( +
{ } }} /> +
+ )} + + {/* Other content - scrollable */} + {!( + selectedNode.data.type === "action" && + !selectedNode.data.config?.actionType && + isOwner + ) && ( +
+ {selectedNode.data.type === "trigger" && ( + )} - {selectedNode.data.type === "action" && - !selectedNode.data.config?.actionType && - !isOwner && ( + {selectedNode.data.type === "action" && + !selectedNode.data.config?.actionType && + !isOwner && ( +
+

+ No action configured for this step. +

+
+ )} + + {selectedNode.data.type === "action" && + selectedNode.data.config?.actionType ? ( + + ) : null} + + {selectedNode.data.type !== "action" || + selectedNode.data.config?.actionType ? ( + <> +
+ + handleUpdateLabel(e.target.value)} + value={selectedNode.data.label} + /> +
+ +
+ + handleUpdateDescription(e.target.value)} + placeholder="Optional description" + value={selectedNode.data.description || ""} + /> +
+ + ) : null} + + {!isOwner && (

- No action configured for this step. + You are viewing a public workflow. Duplicate it to make + changes.

)} - - {selectedNode.data.type === "action" && - selectedNode.data.config?.actionType ? ( - - ) : null} - - {selectedNode.data.type !== "action" || - selectedNode.data.config?.actionType ? ( - <> -
- - handleUpdateLabel(e.target.value)} - value={selectedNode.data.label} - /> -
- -
- - handleUpdateDescription(e.target.value)} - placeholder="Optional description" - value={selectedNode.data.description || ""} - /> -
- - ) : null} - - {!isOwner && ( -
-

- You are viewing a public workflow. Duplicate it to make - changes. -

-
- )} -
+
+ )} {selectedNode.data.type === "action" && isOwner && ( -
-
- - -
- - {(() => { - const actionType = selectedNode.data.config - ?.actionType as string; - - // Database Query is special - has integration but no plugin - const SYSTEM_INTEGRATION_MAP: Record = { - "Database Query": "database", - }; - - // Get integration type dynamically - let integrationType: string | undefined; - if (actionType) { - if (SYSTEM_INTEGRATION_MAP[actionType]) { - integrationType = SYSTEM_INTEGRATION_MAP[actionType]; - } else { - // Look up from plugin registry - const action = findActionById(actionType); - integrationType = action?.integration; - } +
+ +
)} {selectedNode.data.type === "trigger" && isOwner && ( diff --git a/components/workflow/workflow-toolbar.tsx b/components/workflow/workflow-toolbar.tsx index 227eefb6a..6443a8728 100644 --- a/components/workflow/workflow-toolbar.tsx +++ b/components/workflow/workflow-toolbar.tsx @@ -1573,10 +1573,20 @@ function WorkflowIssuesDialog({ const { brokenReferences, missingRequiredFields, missingIntegrations } = actions.workflowIssues; - const handleGoToStep = (nodeId: string) => { + const handleGoToStep = (nodeId: string, fieldKey?: string) => { actions.setShowWorkflowIssuesDialog(false); state.setSelectedNodeId(nodeId); state.setActiveTab("properties"); + // Focus on the specific field after a short delay to allow the panel to render + if (fieldKey) { + setTimeout(() => { + const element = document.getElementById(fieldKey); + if (element) { + element.focus(); + element.scrollIntoView({ behavior: "smooth", block: "center" }); + } + }, 100); + } }; const handleAddIntegration = (integrationType: IntegrationType) => { @@ -1608,147 +1618,128 @@ function WorkflowIssuesDialog({ -
- {/* Broken References Section */} - {brokenReferences.length > 0 && ( -
-

- - Broken References ({brokenReferences.length}) +
+ {/* Missing Connections Section */} + {missingIntegrations.length > 0 && ( +
+

+ Missing Connections

-
- {brokenReferences.map((broken) => ( -
( +
+ +

+ + {missing.integrationLabel} + + + {" — "} + {missing.nodeNames.length > 3 + ? `${missing.nodeNames.slice(0, 3).join(", ")} +${missing.nodeNames.length - 3} more` + : missing.nodeNames.join(", ")} + +

+ -
- ))} -
+ Add + +
+ ))}
)} - {/* Missing Required Fields Section */} - {missingRequiredFields.length > 0 && ( + {/* Broken References Section */} + {brokenReferences.length > 0 && (
-

- - Missing Required Fields ({missingRequiredFields.length}) +

+ Broken References

-
- {missingRequiredFields.map((node) => ( -
-
-

- {node.nodeLabel} -

-
- {node.missingFields.map((field) => ( -

- Missing:{" "} - - {field.fieldLabel} - -

- ))} + {brokenReferences.map((broken) => ( +
+

{broken.nodeLabel}

+
+ {broken.brokenReferences.map((ref, idx) => ( +
+

+ {ref.displayText} + {" in "} + {ref.fieldLabel} +

+
-
- + ))}
- ))} -
+
+ ))}
)} - {/* Missing Integrations Section */} - {missingIntegrations.length > 0 && ( + {/* Missing Required Fields Section */} + {missingRequiredFields.length > 0 && (
-

- - Missing Integrations ({missingIntegrations.length}) +

+ Missing Required Fields

-
- {missingIntegrations.map((missing) => ( -
- -
-

- {missing.integrationLabel} -

-

- Used by:{" "} - {missing.nodeNames.length > 3 - ? `${missing.nodeNames.slice(0, 3).join(", ")} and ${missing.nodeNames.length - 3} more` - : missing.nodeNames.join(", ")} -

-
- + {missingRequiredFields.map((node) => ( +
+

{node.nodeLabel}

+
+ {node.missingFields.map((field) => ( +
+

+ {field.fieldLabel} +

+ +
+ ))}
- ))} -
+
+ ))}
)}

- - Cancel + + Cancel @@ -1920,12 +1911,14 @@ function WorkflowDialogsComponent({ the workflow? - - Cancel + - +
+ Cancel + +
diff --git a/components/workflows/user-menu.tsx b/components/workflows/user-menu.tsx index 87f6af08b..43013b132 100644 --- a/components/workflows/user-menu.tsx +++ b/components/workflows/user-menu.tsx @@ -141,7 +141,7 @@ export const UserMenu = () => { )} setIntegrationsOpen(true)}> - Integrations + Connections setApiKeysOpen(true)}> diff --git a/lib/api-client.ts b/lib/api-client.ts index a512d9665..583e372ca 100644 --- a/lib/api-client.ts +++ b/lib/api-client.ts @@ -362,7 +362,7 @@ export const integrationApi = { method: "DELETE", }), - // Test connection + // Test existing integration connection testConnection: (integrationId: string) => apiCall<{ status: "success" | "error"; message: string }>( `/api/integrations/${integrationId}/test`, @@ -370,6 +370,19 @@ export const integrationApi = { method: "POST", } ), + + // Test credentials without saving + testCredentials: (data: { + type: IntegrationType; + config: IntegrationConfig; + }) => + apiCall<{ status: "success" | "error"; message: string }>( + "/api/integrations/test", + { + method: "POST", + body: JSON.stringify(data), + } + ), }; // User API diff --git a/package.json b/package.json index 54b593b89..e700a06d5 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-context-menu": "^2.2.16", "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-tooltip": "^1.2.8", "@slack/web-api": "^7.12.0", "@vercel/analytics": "^1.5.0", "@vercel/og": "^0.8.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 818c9f09b..6163a35eb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,9 @@ importers: '@radix-ui/react-dialog': specifier: ^1.1.15 version: 1.1.15(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-tooltip': + specifier: ^1.2.8 + version: 1.2.8(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) '@slack/web-api': specifier: ^7.12.0 version: 7.12.0 From 8b2f07593fc0579a458dac6c17d6cf55eef18fca Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 12 Dec 2025 12:05:43 -0600 Subject: [PATCH 04/16] make "new step" more robust (#160) * better actions * robust new step --- .../settings/integration-form-dialog.tsx | 49 ++- components/settings/integrations-dialog.tsx | 35 +- components/settings/integrations-manager.tsx | 147 ++++--- components/workflow/config/action-grid.tsx | 384 ++++++++++++++++-- plugins/registry.ts | 11 + 5 files changed, 500 insertions(+), 126 deletions(-) diff --git a/components/settings/integration-form-dialog.tsx b/components/settings/integration-form-dialog.tsx index 5cf37e18b..8757af097 100644 --- a/components/settings/integration-form-dialog.tsx +++ b/components/settings/integration-form-dialog.tsx @@ -43,6 +43,7 @@ import { getIntegrationLabels, getSortedIntegrationTypes, } from "@/plugins"; +import { getIntegrationDescriptions } from "@/plugins/registry"; type IntegrationFormDialogProps = { open: boolean; @@ -65,6 +66,9 @@ const SYSTEM_INTEGRATION_TYPES: IntegrationType[] = ["database"]; const SYSTEM_INTEGRATION_LABELS: Record = { database: "Database", }; +const SYSTEM_INTEGRATION_DESCRIPTIONS: Record = { + database: "Connect to PostgreSQL databases", +}; // Get all integration types (plugins + system) const getIntegrationTypes = (): IntegrationType[] => [ @@ -76,6 +80,12 @@ const getIntegrationTypes = (): IntegrationType[] => [ const getLabel = (type: IntegrationType): string => getIntegrationLabels()[type] || SYSTEM_INTEGRATION_LABELS[type] || type; +// Get description for any integration type +const getDescription = (type: IntegrationType): string => + getIntegrationDescriptions()[type] || + SYSTEM_INTEGRATION_DESCRIPTIONS[type] || + ""; + function SecretField({ fieldId, label, @@ -419,20 +429,31 @@ function TypeSelector({ No services found

) : ( - filteredTypes.map((type) => ( - - )) + filteredTypes.map((type) => { + const description = getDescription(type); + return ( + + ); + }) )}

diff --git a/components/settings/integrations-dialog.tsx b/components/settings/integrations-dialog.tsx index f992f4f7c..1104b4d73 100644 --- a/components/settings/integrations-dialog.tsx +++ b/components/settings/integrations-dialog.tsx @@ -1,7 +1,7 @@ "use client"; import { useSetAtom } from "jotai"; -import { Plus } from "lucide-react"; +import { Plus, Search } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; import { Button } from "@/components/ui/button"; import { @@ -12,6 +12,7 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; import { Spinner } from "@/components/ui/spinner"; import { integrationsVersionAtom } from "@/lib/integrations-store"; import { IntegrationsManager } from "./integrations-manager"; @@ -27,6 +28,7 @@ export function IntegrationsDialog({ }: IntegrationsDialogProps) { const [loading, setLoading] = useState(true); const [showCreateDialog, setShowCreateDialog] = useState(false); + const [filter, setFilter] = useState(""); const setIntegrationsVersion = useSetAtom(integrationsVersionAtom); // Track if any changes were made during this dialog session const hasChangesRef = useRef(false); @@ -48,6 +50,8 @@ export function IntegrationsDialog({ hasChangesRef.current = false; // Reset create dialog state when opening setShowCreateDialog(false); + // Reset filter when opening + setFilter(""); } }, [open, loadAll]); @@ -68,10 +72,7 @@ export function IntegrationsDialog({ return ( - + Connections @@ -84,12 +85,24 @@ export function IntegrationsDialog({
) : ( -
- setShowCreateDialog(false)} - onIntegrationChange={handleIntegrationChange} - showCreateDialog={showCreateDialog} - /> +
+
+ + setFilter(e.target.value)} + placeholder="Filter connections..." + value={filter} + /> +
+
+ setShowCreateDialog(false)} + onIntegrationChange={handleIntegrationChange} + showCreateDialog={showCreateDialog} + /> +
)} diff --git a/components/settings/integrations-manager.tsx b/components/settings/integrations-manager.tsx index 395106765..f6beaca9d 100644 --- a/components/settings/integrations-manager.tsx +++ b/components/settings/integrations-manager.tsx @@ -29,12 +29,14 @@ type IntegrationsManagerProps = { showCreateDialog: boolean; onCreateDialogClose?: () => void; onIntegrationChange?: () => void; + filter?: string; }; export function IntegrationsManager({ showCreateDialog: externalShowCreateDialog, onCreateDialogClose, onIntegrationChange, + filter = "", }: IntegrationsManagerProps) { const [integrations, setIntegrations] = useState([]); const [loading, setLoading] = useState(true); @@ -69,6 +71,7 @@ export function IntegrationsManager({ // Get integrations with their labels, sorted by label then name const integrationsWithLabels = useMemo(() => { const labels = getIntegrationLabels() as Record; + const filterLower = filter.toLowerCase(); return integrations .map((integration) => ({ @@ -78,6 +81,14 @@ export function IntegrationsManager({ SYSTEM_INTEGRATION_LABELS[integration.type] || integration.type, })) + .filter((integration) => { + if (!filter) return true; + return ( + integration.label.toLowerCase().includes(filterLower) || + integration.name.toLowerCase().includes(filterLower) || + integration.type.toLowerCase().includes(filterLower) + ); + }) .sort((a, b) => { const labelCompare = a.label.localeCompare(b.label); if (labelCompare !== 0) { @@ -85,7 +96,7 @@ export function IntegrationsManager({ } return a.name.localeCompare(b.name); }); - }, [integrations]); + }, [integrations, filter]); const handleDelete = async (id: string) => { try { @@ -139,70 +150,88 @@ export function IntegrationsManager({ ); } - return ( -
- {integrations.length === 0 ? ( + const renderIntegrationsList = () => { + if (integrations.length === 0) { + return (

No connections configured yet

- ) : ( -
- {integrationsWithLabels.map((integration) => ( -
-
- - {integration.label} - - {integration.name} - -
-
- - - -
-
- ))} + ); + } + + if (integrationsWithLabels.length === 0) { + return ( +
+

+ No connections match your filter +

- )} + ); + } + + return ( +
+ {integrationsWithLabels.map((integration) => ( +
+
+ + {integration.label} + + {integration.name} + +
+
+ + + +
+
+ ))} +
+ ); + }; + + return ( +
+ {renderIntegrationsList()} {(showCreateDialog || editingIntegration) && ( ; integration?: string; }; @@ -23,21 +45,18 @@ const SYSTEM_ACTIONS: ActionType[] = [ label: "HTTP Request", description: "Make an HTTP request to any API", category: "System", - icon: Zap, }, { id: "Database Query", label: "Database Query", description: "Query your database", category: "System", - icon: Database, }, { id: "Condition", label: "Condition", description: "Branch based on a condition", category: "System", - icon: Settings, }, ]; @@ -65,16 +84,70 @@ type ActionGridProps = { isNewlyCreated?: boolean; }; -function ActionIcon({ action }: { action: ActionType }) { +function GroupIcon({ + group, +}: { + group: { category: string; actions: ActionType[] }; +}) { + // For plugin categories, use the integration icon from the first action + const firstAction = group.actions[0]; + if (firstAction?.integration) { + return ( + + ); + } + // For System category + if (group.category === "System") { + return ; + } + return ; +} + +function ActionIcon({ + action, + className, +}: { + action: ActionType; + className?: string; +}) { if (action.integration) { return ( - + ); } - if (action.icon) { - return ; + if (action.category === "System") { + return ; + } + return ; +} + +// Local storage keys +const HIDDEN_GROUPS_KEY = "workflow-action-grid-hidden-groups"; +const VIEW_MODE_KEY = "workflow-action-grid-view-mode"; + +type ViewMode = "list" | "grid"; + +function getInitialHiddenGroups(): Set { + if (typeof window === "undefined") return new Set(); + try { + const stored = localStorage.getItem(HIDDEN_GROUPS_KEY); + return stored ? new Set(JSON.parse(stored)) : new Set(); + } catch { + return new Set(); + } +} + +function getInitialViewMode(): ViewMode { + if (typeof window === "undefined") return "list"; + try { + const stored = localStorage.getItem(VIEW_MODE_KEY); + return stored === "grid" ? "grid" : "list"; + } catch { + return "list"; } - return ; } export function ActionGrid({ @@ -83,9 +156,49 @@ export function ActionGrid({ isNewlyCreated, }: ActionGridProps) { const [filter, setFilter] = useState(""); + const [collapsedGroups, setCollapsedGroups] = useState>( + new Set() + ); + const [hiddenGroups, setHiddenGroups] = useState>( + getInitialHiddenGroups + ); + const [showHidden, setShowHidden] = useState(false); + const [viewMode, setViewMode] = useState(getInitialViewMode); const actions = useAllActions(); const inputRef = useRef(null); + const toggleViewMode = () => { + const newMode = viewMode === "list" ? "grid" : "list"; + setViewMode(newMode); + localStorage.setItem(VIEW_MODE_KEY, newMode); + }; + + const toggleGroup = (category: string) => { + setCollapsedGroups((prev) => { + const next = new Set(prev); + if (next.has(category)) { + next.delete(category); + } else { + next.add(category); + } + return next; + }); + }; + + const toggleHideGroup = (category: string) => { + setHiddenGroups((prev) => { + const next = new Set(prev); + if (next.has(category)) { + next.delete(category); + } else { + next.add(category); + } + // Persist to localStorage + localStorage.setItem(HIDDEN_GROUPS_KEY, JSON.stringify([...next])); + return next; + }); + }; + useEffect(() => { if (isNewlyCreated && inputRef.current) { inputRef.current.focus(); @@ -101,49 +214,236 @@ export function ActionGrid({ ); }); + // Group actions by category + const groupedActions = useMemo(() => { + const groups: Record = {}; + + for (const action of filteredActions) { + const category = action.category; + if (!groups[category]) { + groups[category] = []; + } + groups[category].push(action); + } + + // Sort categories: System first, then alphabetically + const sortedCategories = Object.keys(groups).sort((a, b) => { + if (a === "System") return -1; + if (b === "System") return 1; + return a.localeCompare(b); + }); + + return sortedCategories.map((category) => ({ + category, + actions: groups[category], + })); + }, [filteredActions]); + + // Filter groups based on hidden state + const visibleGroups = useMemo(() => { + if (showHidden) return groupedActions; + return groupedActions.filter((g) => !hiddenGroups.has(g.category)); + }, [groupedActions, hiddenGroups, showHidden]); + + const hiddenCount = hiddenGroups.size; + return (
-
- - setFilter(e.target.value)} - placeholder="Search actions..." - ref={inputRef} - value={filter} - /> +
+
+ + setFilter(e.target.value)} + placeholder="Search actions..." + ref={inputRef} + value={filter} + /> +
+ + + + + + + {viewMode === "list" ? "Grid view" : "List view"} + + + + {hiddenCount > 0 && ( + + + + + + + {showHidden + ? "Hide hidden groups" + : `Show ${hiddenCount} hidden group${hiddenCount > 1 ? "s" : ""}`} + + + + )}
- {filteredActions.length === 0 ? ( + {filteredActions.length === 0 && (

No actions found

- ) : ( - filteredActions.map((action) => ( - - )) )} + {filteredActions.length > 0 && visibleGroups.length === 0 && ( +

+ All groups are hidden +

+ )} + + {/* Grid View */} + {viewMode === "grid" && visibleGroups.length > 0 && ( +
+ {filteredActions + .filter( + (action) => showHidden || !hiddenGroups.has(action.category) + ) + .map((action) => ( + + ))} +
+ )} + + {/* List View */} + {viewMode === "list" && + visibleGroups.length > 0 && + visibleGroups.map((group, groupIndex) => { + const isCollapsed = collapsedGroups.has(group.category); + const isHidden = hiddenGroups.has(group.category); + return ( +
+ {groupIndex > 0 &&
} +
+ + + + + + + toggleHideGroup(group.category)} + > + {isHidden ? ( + <> + + Show group + + ) : ( + <> + + Hide group + + )} + + + +
+ {!isCollapsed && + group.actions.map((action) => ( + + ))} +
+ ); + })}
); diff --git a/plugins/registry.ts b/plugins/registry.ts index 9e73c407c..ef16bea60 100644 --- a/plugins/registry.ts +++ b/plugins/registry.ts @@ -372,6 +372,17 @@ export function getIntegrationLabels(): Record { return labels as Record; } +/** + * Get integration descriptions map + */ +export function getIntegrationDescriptions(): Record { + const descriptions: Record = {}; + for (const plugin of integrationRegistry.values()) { + descriptions[plugin.type] = plugin.description; + } + return descriptions as Record; +} + /** * Get sorted integration types for dropdowns */ From 4afb1949afa7fe80de13843d184a1236d61ad780 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 15 Dec 2025 17:22:20 -0600 Subject: [PATCH 05/16] ai gateway managed keys (#164) * ai gateway managed keys * remove debugging * stronger encryption * fix new * re-fetch teams * simplify * prefetch * fix avatar * better avatars * fixes * improvements * fixes --- app/api/ai-gateway/consent/route.ts | 286 +++++++ app/api/ai-gateway/status/route.ts | 60 ++ app/api/ai-gateway/teams/route.ts | 119 +++ app/api/integrations/route.ts | 2 + app/layout.tsx | 2 + app/workflows/[workflowId]/page.tsx | 12 +- components/ai-gateway-consent-modal.tsx | 234 ++++++ components/global-modals.tsx | 10 + .../settings/integration-form-dialog.tsx | 230 +++++- components/settings/integrations-dialog.tsx | 2 +- components/ui/checkbox.tsx | 32 + components/ui/integration-selector.tsx | 231 +++++- components/workflow/config/action-config.tsx | 87 ++- components/workflow/config/action-grid.tsx | 2 +- components/workflow/node-config-panel.tsx | 1 + components/workflow/workflow-runs.tsx | 2 +- components/workflows/user-menu.tsx | 2 +- drizzle/0004_real_wither.sql | 1 + drizzle/meta/0004_snapshot.json | 731 ++++++++++++++++++ drizzle/meta/_journal.json | 7 + lib/ai-gateway/config.ts | 27 + lib/ai-gateway/state.ts | 65 ++ lib/api-client.ts | 52 ++ lib/auth.ts | 7 +- lib/db/index.ts | 21 +- lib/db/integrations.ts | 1 + lib/db/schema.ts | 2 + package.json | 2 + pnpm-lock.yaml | 26 +- 29 files changed, 2175 insertions(+), 81 deletions(-) create mode 100644 app/api/ai-gateway/consent/route.ts create mode 100644 app/api/ai-gateway/status/route.ts create mode 100644 app/api/ai-gateway/teams/route.ts create mode 100644 components/ai-gateway-consent-modal.tsx create mode 100644 components/global-modals.tsx create mode 100644 components/ui/checkbox.tsx create mode 100644 drizzle/0004_real_wither.sql create mode 100644 drizzle/meta/0004_snapshot.json create mode 100644 lib/ai-gateway/config.ts create mode 100644 lib/ai-gateway/state.ts diff --git a/app/api/ai-gateway/consent/route.ts b/app/api/ai-gateway/consent/route.ts new file mode 100644 index 000000000..700b95a4f --- /dev/null +++ b/app/api/ai-gateway/consent/route.ts @@ -0,0 +1,286 @@ +import { and, eq } from "drizzle-orm"; +import { isAiGatewayManagedKeysEnabled } from "@/lib/ai-gateway/config"; +import { auth } from "@/lib/auth"; +import { db } from "@/lib/db"; +import { decrypt, encrypt } from "@/lib/db/integrations"; +import { accounts, integrations } from "@/lib/db/schema"; +import { generateId } from "@/lib/utils/id"; + +const API_KEY_PURPOSE = "ai-gateway"; +const API_KEY_NAME = "Workflow Builder Gateway Key"; + +/** + * Get team ID from Vercel API + * First tries /v2/teams, then falls back to userinfo endpoint + */ +async function getTeamId(accessToken: string): Promise { + // First, try to get teams the user has granted access to + const teamsResponse = await fetch("https://api.vercel.com/v2/teams", { + headers: { Authorization: `Bearer ${accessToken}` }, + }); + + if (teamsResponse.ok) { + const teamsData = await teamsResponse.json(); + // biome-ignore lint/suspicious/noExplicitAny: API response type + const accessibleTeam = teamsData.teams?.find((t: any) => !t.limited); + if (accessibleTeam) { + return accessibleTeam.id; + } + } + + // Fallback: get user ID from userinfo endpoint + const userinfoResponse = await fetch( + "https://api.vercel.com/login/oauth/userinfo", + { headers: { Authorization: `Bearer ${accessToken}` } } + ); + + if (!userinfoResponse.ok) { + return null; + } + + const userinfo = await userinfoResponse.json(); + return userinfo.sub; +} + +/** + * Create or exchange API key on Vercel + */ +async function createVercelApiKey( + accessToken: string, + teamId: string +): Promise<{ token: string; id: string } | null> { + const response = await fetch( + `https://api.vercel.com/v1/api-keys?teamId=${teamId}`, + { + method: "POST", + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + purpose: API_KEY_PURPOSE, + name: API_KEY_NAME, + exchange: true, + }), + } + ); + + if (!response.ok) { + console.error( + "[ai-gateway] Failed to create API key:", + await response.text() + ); + return null; + } + + const newKey = await response.json(); + if (!newKey.apiKeyString) { + return null; + } + + return { token: newKey.apiKeyString, id: newKey.apiKey?.id }; +} + +type SaveIntegrationParams = { + userId: string; + apiKey: string; + apiKeyId: string; + teamId: string; + teamName: string; +}; + +/** + * Save managed integration in database + * Each team gets its own managed integration - always creates a new one + * The apiKeyId and teamId are stored in config for later deletion + */ +async function saveIntegration(params: SaveIntegrationParams): Promise { + const { userId, apiKey, apiKeyId, teamId, teamName } = params; + + // Config contains the API key plus metadata for managing the key + const configData = { apiKey, managedKeyId: apiKeyId, teamId }; + // Encrypt the entire config for storage (consistent with other integrations) + const encryptedConfig = encrypt(JSON.stringify(configData)); + + // Always create a new integration - users can have multiple managed keys for different teams + const integrationId = generateId(); + await db.insert(integrations).values({ + id: integrationId, + userId, + name: teamName, + type: "ai-gateway", + config: encryptedConfig, + isManaged: true, + }); + return integrationId; +} + +/** + * Delete API key from Vercel + */ +async function deleteVercelApiKey( + accessToken: string, + apiKeyId: string, + teamId: string +): Promise { + await fetch( + `https://api.vercel.com/v1/api-keys/${apiKeyId}?teamId=${teamId}`, + { + method: "DELETE", + headers: { Authorization: `Bearer ${accessToken}` }, + } + ); +} + +/** + * POST /api/ai-gateway/consent + * Record consent and create API key on user's Vercel account + */ +export async function POST(request: Request) { + if (!isAiGatewayManagedKeysEnabled()) { + return Response.json({ error: "Feature not enabled" }, { status: 403 }); + } + + const session = await auth.api.getSession({ headers: request.headers }); + if (!session?.user?.id) { + return Response.json({ error: "Not authenticated" }, { status: 401 }); + } + + const account = await db.query.accounts.findFirst({ + where: eq(accounts.userId, session.user.id), + }); + + if (!account?.accessToken || account.providerId !== "vercel") { + return Response.json( + { error: "No Vercel account linked" }, + { status: 400 } + ); + } + + // Get teamId and teamName from request body + let teamId: string | null = null; + let teamName: string | null = null; + try { + const body = await request.json(); + teamId = body.teamId; + teamName = body.teamName; + } catch { + // If no body, try to auto-detect + } + + // If no teamId provided, try to auto-detect + if (!teamId) { + teamId = await getTeamId(account.accessToken); + } + + if (!teamId) { + return Response.json( + { error: "Could not determine user's team" }, + { status: 500 } + ); + } + + try { + const vercelApiKey = await createVercelApiKey(account.accessToken, teamId); + if (!vercelApiKey) { + return Response.json( + { error: "Failed to create API key" }, + { status: 500 } + ); + } + + const integrationId = await saveIntegration({ + userId: session.user.id, + apiKey: vercelApiKey.token, + apiKeyId: vercelApiKey.id, + teamId, + teamName: teamName || "AI Gateway", + }); + + return Response.json({ + success: true, + hasManagedKey: true, + managedIntegrationId: integrationId, + }); + } catch (e) { + console.error("[ai-gateway] Error creating API key:", e); + return Response.json( + { error: "Failed to create API key" }, + { status: 500 } + ); + } +} + +/** + * DELETE /api/ai-gateway/consent?integrationId=xxx + * Revoke consent and delete the API key + * Requires integrationId query parameter to specify which integration to delete + */ +export async function DELETE(request: Request) { + if (!isAiGatewayManagedKeysEnabled()) { + return Response.json({ error: "Feature not enabled" }, { status: 403 }); + } + + const session = await auth.api.getSession({ headers: request.headers }); + if (!session?.user?.id) { + return Response.json({ error: "Not authenticated" }, { status: 401 }); + } + + const { searchParams } = new URL(request.url); + const integrationId = searchParams.get("integrationId"); + + if (!integrationId) { + return Response.json( + { error: "integrationId query parameter is required" }, + { status: 400 } + ); + } + + const managedIntegration = await db.query.integrations.findFirst({ + where: and( + eq(integrations.id, integrationId), + eq(integrations.userId, session.user.id), + eq(integrations.type, "ai-gateway"), + eq(integrations.isManaged, true) + ), + }); + + if (!managedIntegration) { + return Response.json({ error: "Integration not found" }, { status: 404 }); + } + + // Get managedKeyId and teamId from config (decrypt it first since it's stored encrypted) + let config: { managedKeyId?: string; teamId?: string } | null = null; + if (managedIntegration?.config) { + try { + const decrypted = decrypt(managedIntegration.config as string); + config = JSON.parse(decrypted); + } catch (e) { + console.error("[ai-gateway] Failed to decrypt config:", e); + } + } + + if (config?.managedKeyId && config?.teamId) { + const account = await db.query.accounts.findFirst({ + where: eq(accounts.userId, session.user.id), + }); + + if (account?.accessToken) { + try { + await deleteVercelApiKey( + account.accessToken, + config.managedKeyId, + config.teamId + ); + } catch (e) { + console.error("[ai-gateway] Failed to delete API key from Vercel:", e); + } + } + } + + await db + .delete(integrations) + .where(eq(integrations.id, managedIntegration.id)); + + return Response.json({ success: true, hasManagedKey: false }); +} diff --git a/app/api/ai-gateway/status/route.ts b/app/api/ai-gateway/status/route.ts new file mode 100644 index 000000000..1ae0aac30 --- /dev/null +++ b/app/api/ai-gateway/status/route.ts @@ -0,0 +1,60 @@ +import { and, eq } from "drizzle-orm"; +import { isAiGatewayManagedKeysEnabled } from "@/lib/ai-gateway/config"; +import { auth } from "@/lib/auth"; +import { db } from "@/lib/db"; +import { accounts, integrations } from "@/lib/db/schema"; + +/** + * GET /api/ai-gateway/status + * Returns user's AI Gateway status including whether they can use managed keys + */ +export async function GET(request: Request) { + const enabled = isAiGatewayManagedKeysEnabled(); + + // If feature is not enabled, return minimal response + if (!enabled) { + return Response.json({ + enabled: false, + signedIn: false, + isVercelUser: false, + hasManagedKey: false, + }); + } + + const session = await auth.api.getSession({ + headers: request.headers, + }); + + if (!session?.user?.id) { + return Response.json({ + enabled: true, + signedIn: false, + isVercelUser: false, + hasManagedKey: false, + }); + } + + // Check if user signed in with Vercel + const account = await db.query.accounts.findFirst({ + where: eq(accounts.userId, session.user.id), + }); + + const isVercelUser = account?.providerId === "vercel"; + + // Check if user has a managed AI Gateway integration + const managedIntegration = await db.query.integrations.findFirst({ + where: and( + eq(integrations.userId, session.user.id), + eq(integrations.type, "ai-gateway"), + eq(integrations.isManaged, true) + ), + }); + + return Response.json({ + enabled: true, + signedIn: true, + isVercelUser, + hasManagedKey: !!managedIntegration, + managedIntegrationId: managedIntegration?.id, + }); +} diff --git a/app/api/ai-gateway/teams/route.ts b/app/api/ai-gateway/teams/route.ts new file mode 100644 index 000000000..3f6ef5f53 --- /dev/null +++ b/app/api/ai-gateway/teams/route.ts @@ -0,0 +1,119 @@ +import { eq } from "drizzle-orm"; +import { isAiGatewayManagedKeysEnabled } from "@/lib/ai-gateway/config"; +import { auth } from "@/lib/auth"; +import { db } from "@/lib/db"; +import { accounts } from "@/lib/db/schema"; + +export type VercelTeam = { + id: string; + name: string; + slug: string; + avatar?: string; + isPersonal: boolean; +}; + +type VercelTeamApiResponse = { + id: string; + name: string; + slug: string; + avatar?: string; + limited?: boolean; +}; + +type VercelUserResponse = { + defaultTeamId: string | null; +}; + +/** + * Fetch user's default team ID from Vercel API + */ +async function fetchDefaultTeamId(accessToken: string): Promise { + const response = await fetch("https://api.vercel.com/v2/user", { + headers: { Authorization: `Bearer ${accessToken}` }, + }); + + if (!response.ok) return null; + + const data = (await response.json()) as { user?: VercelUserResponse }; + return data.user?.defaultTeamId ?? null; +} + +/** + * Fetch teams from Vercel API and transform to our format + */ +async function fetchTeams(accessToken: string): Promise { + const response = await fetch("https://api.vercel.com/v2/teams", { + headers: { Authorization: `Bearer ${accessToken}` }, + }); + + if (!response.ok) return []; + + const data = (await response.json()) as { teams?: VercelTeamApiResponse[] }; + const teams: VercelTeam[] = []; + + for (const team of data.teams || []) { + if (team.limited) continue; + teams.push({ + id: team.id, + name: team.name, + slug: team.slug, + // Team avatar URL uses teamId + avatar: `https://vercel.com/api/www/avatar?teamId=${team.id}&s=64`, + isPersonal: false, + }); + } + + return teams; +} + +/** + * GET /api/ai-gateway/teams + * Fetch Vercel teams for the authenticated user + */ +export async function GET(request: Request) { + if (!isAiGatewayManagedKeysEnabled()) { + return Response.json({ error: "Feature not enabled" }, { status: 403 }); + } + + const session = await auth.api.getSession({ headers: request.headers }); + if (!session?.user?.id) { + return Response.json({ error: "Not authenticated" }, { status: 401 }); + } + + const account = await db.query.accounts.findFirst({ + where: eq(accounts.userId, session.user.id), + }); + + if (!account?.accessToken || account.providerId !== "vercel") { + return Response.json( + { error: "No Vercel account linked" }, + { status: 400 } + ); + } + + try { + // Fetch default team ID and teams in parallel + const [defaultTeamId, teams] = await Promise.all([ + fetchDefaultTeamId(account.accessToken), + fetchTeams(account.accessToken), + ]); + + // Mark the user's default team as personal + const teamsWithPersonal = teams.map((team) => ({ + ...team, + isPersonal: team.id === defaultTeamId, + })); + + // Sort: personal/default team first, then alphabetically by name + const sortedTeams = teamsWithPersonal.sort((a, b) => { + if (a.isPersonal) return -1; + if (b.isPersonal) return 1; + return a.name.localeCompare(b.name); + }); + + return Response.json({ teams: sortedTeams }); + } catch (e) { + console.error("[ai-gateway] Error fetching teams:", e); + return Response.json({ error: "Failed to fetch teams" }, { status: 500 }); + } +} diff --git a/app/api/integrations/route.ts b/app/api/integrations/route.ts index 6d81d56c0..ea9643e06 100644 --- a/app/api/integrations/route.ts +++ b/app/api/integrations/route.ts @@ -10,6 +10,7 @@ export type GetIntegrationsResponse = { id: string; name: string; type: IntegrationType; + isManaged?: boolean; createdAt: string; updatedAt: string; // Config is intentionally excluded for security @@ -58,6 +59,7 @@ export async function GET(request: Request) { id: integration.id, name: integration.name, type: integration.type, + isManaged: integration.isManaged ?? false, createdAt: integration.createdAt.toISOString(), updatedAt: integration.updatedAt.toISOString(), }) diff --git a/app/layout.tsx b/app/layout.tsx index d46652c81..3f3f11dfa 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -8,6 +8,7 @@ import { type ReactNode, Suspense } from "react"; import { AuthProvider } from "@/components/auth/provider"; import { GitHubStarsLoader } from "@/components/github-stars-loader"; import { GitHubStarsProvider } from "@/components/github-stars-provider"; +import { GlobalModals } from "@/components/global-modals"; import { ThemeProvider } from "@/components/theme-provider"; import { Toaster } from "@/components/ui/sonner"; import { PersistentCanvas } from "@/components/workflow/persistent-canvas"; @@ -65,6 +66,7 @@ const RootLayout = ({ children }: RootLayoutProps) => ( + diff --git a/app/workflows/[workflowId]/page.tsx b/app/workflows/[workflowId]/page.tsx index c760640dd..1c03c860d 100644 --- a/app/workflows/[workflowId]/page.tsx +++ b/app/workflows/[workflowId]/page.tsx @@ -129,7 +129,7 @@ const WorkflowEditor = ({ params }: WorkflowPageProps) => { const setCurrentWorkflowVisibility = useSetAtom( currentWorkflowVisibilityAtom ); - const setIsWorkflowOwner = useSetAtom(isWorkflowOwnerAtom); + const [isOwner, setIsWorkflowOwner] = useAtom(isWorkflowOwnerAtom); const setGlobalIntegrations = useSetAtom(integrationsAtom); const setIntegrationsLoaded = useSetAtom(integrationsLoadedAtom); const integrationsVersion = useAtomValue(integrationsVersionAtom); @@ -427,6 +427,11 @@ const WorkflowEditor = ({ params }: WorkflowPageProps) => { return; } + // Skip for non-owners (they can't modify the workflow and may not be authenticated) + if (!isOwner) { + return; + } + // Skip if already checked for this workflow+version combination const lastFix = lastAutoFixRef.current; if ( @@ -480,6 +485,7 @@ const WorkflowEditor = ({ params }: WorkflowPageProps) => { nodes, currentWorkflowId, integrationsVersion, + isOwner, updateNodeData, setGlobalIntegrations, setIntegrationsLoaded, @@ -677,7 +683,7 @@ const WorkflowEditor = ({ params }: WorkflowPageProps) => { {/* Expand button when panel is collapsed */} {!isMobile && panelCollapsed && ( + )} +
+ + +
+ + + + ); +} diff --git a/components/global-modals.tsx b/components/global-modals.tsx new file mode 100644 index 000000000..592d4ff4f --- /dev/null +++ b/components/global-modals.tsx @@ -0,0 +1,10 @@ +"use client"; + +import { AiGatewayConsentModal } from "@/components/ai-gateway-consent-modal"; + +/** + * Global modals that need to be rendered once at app level + */ +export function GlobalModals() { + return ; +} diff --git a/components/settings/integration-form-dialog.tsx b/components/settings/integration-form-dialog.tsx index 8757af097..ad8926555 100644 --- a/components/settings/integration-form-dialog.tsx +++ b/components/settings/integration-form-dialog.tsx @@ -1,5 +1,6 @@ "use client"; +import { useAtomValue, useSetAtom } from "jotai"; import { ArrowLeft, Check, @@ -11,7 +12,7 @@ import { XCircle, Zap, } from "lucide-react"; -import { useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; import { AlertDialog, @@ -24,6 +25,7 @@ import { AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; import { Dialog, DialogContent, @@ -36,6 +38,12 @@ import { Input } from "@/components/ui/input"; import { IntegrationIcon } from "@/components/ui/integration-icon"; import { Label } from "@/components/ui/label"; import { Spinner } from "@/components/ui/spinner"; +import { + aiGatewayStatusAtom, + aiGatewayTeamsAtom, + aiGatewayTeamsLoadingAtom, + openAiGatewayConsentModalAtom, +} from "@/lib/ai-gateway/state"; import { api, type Integration } from "@/lib/api-client"; import type { IntegrationType } from "@/lib/types/integration"; import { @@ -372,12 +380,16 @@ function DeleteConfirmDialog({ onOpenChange, deleting, onDelete, + isManaged, }: { open: boolean; onOpenChange: (open: boolean) => void; deleting: boolean; - onDelete: () => void; + onDelete: (revokeKey: boolean) => void; + isManaged?: boolean; }) { + const [revokeKey, setRevokeKey] = useState(true); + return ( @@ -388,9 +400,24 @@ function DeleteConfirmDialog({ will fail until a new one is configured. + {isManaged && ( +
+ setRevokeKey(checked)} + /> + +
+ )} Cancel - + onDelete(isManaged ? revokeKey : false)} + > {deleting ? : null} Delete @@ -400,6 +427,46 @@ function DeleteConfirmDialog({ ); } +function TestFailedConfirmDialog({ + open, + onOpenChange, + message, + onProceed, + saving, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; + message: string; + onProceed: () => void; + saving: boolean; +}) { + return ( + + + + Connection Test Failed + + The connection test failed with the following error: + + {message} + + + Do you want to save the connection anyway? + + + + + Cancel + + {saving ? : null} + Save Anyway + + + + + ); +} + function TypeSelector({ searchQuery, onSearchChange, @@ -414,7 +481,7 @@ function TypeSelector({ return (
- + (null); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [showTestFailedConfirm, setShowTestFailedConfirm] = useState(false); + const [testFailedMessage, setTestFailedMessage] = useState(""); const [searchQuery, setSearchQuery] = useState(""); const [formData, setFormData] = useState({ name: "", @@ -484,7 +553,15 @@ export function IntegrationFormDialog({ config: {}, }); - // Step: "select" for type selection list, "configure" for form + // AI Gateway managed keys state + const aiGatewayStatus = useAtomValue(aiGatewayStatusAtom); + const openConsentModal = useSetAtom(openAiGatewayConsentModalAtom); + + // Check if AI Gateway managed keys should be offered + const shouldUseManagedKeys = + aiGatewayStatus?.enabled && aiGatewayStatus?.isVercelUser; + + // Step: "select" for type selection, "configure" for form const [step, setStep] = useState<"select" | "configure">( preselectedType || mode === "edit" ? "configure" : "select" ); @@ -508,7 +585,72 @@ export function IntegrationFormDialog({ } }, [integration, preselectedType]); + // AI Gateway atoms for fetching status and teams + const setAiGatewayStatus = useSetAtom(aiGatewayStatusAtom); + const setTeams = useSetAtom(aiGatewayTeamsAtom); + const setTeamsLoading = useSetAtom(aiGatewayTeamsLoadingAtom); + + // Helper to open consent modal with callbacks + const showConsentModalWithCallbacks = useCallback(() => { + onClose(); + openConsentModal({ + onConsent: (integrationId: string) => { + onSuccess?.(integrationId); + }, + }); + }, [onClose, openConsentModal, onSuccess]); + + // Handle preselected AI Gateway - fetch status/teams and show consent modal if managed keys available + useEffect(() => { + if (!open || preselectedType !== "ai-gateway" || mode !== "create") { + return; + } + + // If we already have status and managed keys are available, show consent modal + if (shouldUseManagedKeys) { + showConsentModalWithCallbacks(); + return; + } + + // If status is null (not fetched yet), fetch it and teams + if (aiGatewayStatus === null) { + api.aiGateway.getStatus().then((status) => { + setAiGatewayStatus(status); + // Check if managed keys should be used after fetching + if (status?.enabled && status?.isVercelUser) { + // Also fetch teams before showing consent modal + setTeamsLoading(true); + api.aiGateway + .getTeams() + .then((response) => { + setTeams(response.teams); + }) + .finally(() => { + setTeamsLoading(false); + showConsentModalWithCallbacks(); + }); + } + }); + } + }, [ + open, + preselectedType, + mode, + aiGatewayStatus, + shouldUseManagedKeys, + showConsentModalWithCallbacks, + setAiGatewayStatus, + setTeams, + setTeamsLoading, + ]); + const handleSelectType = (type: IntegrationType) => { + // If selecting AI Gateway and managed keys are available, show consent modal + if (type === "ai-gateway" && shouldUseManagedKeys) { + showConsentModalWithCallbacks(); + return; + } + setFormData({ name: "", type, @@ -527,7 +669,7 @@ export function IntegrationFormDialog({ }); }; - const handleSave = async () => { + const doSave = async () => { if (!formData.type) { return; } @@ -565,14 +707,68 @@ export function IntegrationFormDialog({ } }; - const handleDelete = async () => { + const handleSave = async () => { + if (!formData.type) { + return; + } + + // Check if we have config values to test + const hasConfig = Object.values(formData.config).some( + (v) => v && v.length > 0 + ); + + // In edit mode without new config, skip testing + if (mode === "edit" && !hasConfig) { + await doSave(); + return; + } + + // Test the connection before saving + try { + setSaving(true); + setTestResult(null); + + const result = await api.integration.testCredentials({ + type: formData.type, + config: formData.config, + }); + + if (result.status === "error") { + // Test failed - ask user if they want to proceed + setTestFailedMessage(result.message); + setShowTestFailedConfirm(true); + setSaving(false); + return; + } + + // Test passed - proceed with save + setSaving(false); + await doSave(); + } catch (error) { + console.error("Failed to test connection:", error); + const message = + error instanceof Error ? error.message : "Failed to test connection"; + setTestFailedMessage(message); + setShowTestFailedConfirm(true); + setSaving(false); + } + }; + + const handleDelete = async (revokeKey: boolean) => { if (!integration) { return; } try { setDeleting(true); - await api.integration.delete(integration.id); + + // If this is a managed connection and user wants to revoke the key + if (integration.isManaged && revokeKey) { + await api.aiGateway.revokeConsent(); + } else { + await api.integration.delete(integration.id); + } + toast.success("Connection deleted"); onDelete?.(); onClose(); @@ -674,14 +870,16 @@ export function IntegrationFormDialog({ {getDialogDescription()} - {step === "select" ? ( + {step === "select" && ( - ) : ( + )} + + {step === "configure" && (
+ + { + setShowTestFailedConfirm(false); + doSave(); + }} + open={showTestFailedConfirm} + saving={saving} + /> ); } diff --git a/components/settings/integrations-dialog.tsx b/components/settings/integrations-dialog.tsx index 1104b4d73..280147caf 100644 --- a/components/settings/integrations-dialog.tsx +++ b/components/settings/integrations-dialog.tsx @@ -87,7 +87,7 @@ export function IntegrationsDialog({ ) : (
- + setFilter(e.target.value)} diff --git a/components/ui/checkbox.tsx b/components/ui/checkbox.tsx new file mode 100644 index 000000000..cb0b07b46 --- /dev/null +++ b/components/ui/checkbox.tsx @@ -0,0 +1,32 @@ +"use client" + +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { CheckIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Checkbox({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + + ) +} + +export { Checkbox } diff --git a/components/ui/integration-selector.tsx b/components/ui/integration-selector.tsx index eb5c954e7..41af1e69d 100644 --- a/components/ui/integration-selector.tsx +++ b/components/ui/integration-selector.tsx @@ -1,9 +1,24 @@ "use client"; import { useAtom, useAtomValue, useSetAtom } from "jotai"; -import { AlertTriangle, Check, Circle, Pencil, Plus, Settings } from "lucide-react"; +import { + AlertTriangle, + Check, + Circle, + Pencil, + Plus, + Settings, +} from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { IntegrationFormDialog } from "@/components/settings/integration-form-dialog"; import { Button } from "@/components/ui/button"; +import { + aiGatewayStatusAtom, + aiGatewayTeamsAtom, + aiGatewayTeamsFetchedAtom, + aiGatewayTeamsLoadingAtom, + openAiGatewayConsentModalAtom, +} from "@/lib/ai-gateway/state"; import { api, type Integration } from "@/lib/api-client"; import { integrationsAtom, @@ -12,7 +27,6 @@ import { import type { IntegrationType } from "@/lib/types/integration"; import { cn } from "@/lib/utils"; import { getIntegration } from "@/plugins"; -import { IntegrationFormDialog } from "@/components/settings/integration-form-dialog"; type IntegrationSelectorProps = { integrationType: IntegrationType; @@ -40,6 +54,16 @@ export function IntegrationSelector({ const lastVersionRef = useRef(integrationsVersion); const [hasFetched, setHasFetched] = useState(false); + // AI Gateway user keys state + const [aiGatewayStatus, setAiGatewayStatus] = useAtom(aiGatewayStatusAtom); + const [aiGatewayStatusFetched, setAiGatewayStatusFetched] = useState(false); + const openConsentModal = useSetAtom(openAiGatewayConsentModalAtom); + + // AI Gateway teams state (pre-loaded for consent modal) + const [teams, setTeams] = useAtom(aiGatewayTeamsAtom); + const [teamsFetched, setTeamsFetched] = useAtom(aiGatewayTeamsFetchedAtom); + const setTeamsLoading = useSetAtom(aiGatewayTeamsLoadingAtom); + // Filter integrations from global cache const integrations = useMemo( () => globalIntegrations.filter((i) => i.type === integrationType), @@ -60,6 +84,79 @@ export function IntegrationSelector({ } }, [setGlobalIntegrations]); + // Load AI Gateway status for ai-gateway type + useEffect(() => { + if (integrationType === "ai-gateway" && !aiGatewayStatusFetched) { + api.aiGateway + .getStatus() + .then((status) => { + setAiGatewayStatus(status); + setAiGatewayStatusFetched(true); + }) + .catch(() => { + setAiGatewayStatusFetched(true); + }); + } + }, [integrationType, aiGatewayStatusFetched, setAiGatewayStatus]); + + // Load AI Gateway teams when status indicates user can use managed keys + useEffect(() => { + if ( + integrationType === "ai-gateway" && + aiGatewayStatus?.enabled && + aiGatewayStatus?.isVercelUser && + !teamsFetched + ) { + setTeamsLoading(true); + api.aiGateway + .getTeams() + .then((response) => { + setTeams(response.teams); + // Only mark as fetched if we got teams - empty might mean expired token + if (response.teams.length > 0) { + setTeamsFetched(true); + } + }) + .catch(() => { + // Don't mark as fetched on error - allow retry + }) + .finally(() => { + setTeamsLoading(false); + }); + } + }, [ + integrationType, + aiGatewayStatus, + teamsFetched, + setTeams, + setTeamsFetched, + setTeamsLoading, + ]); + + // Refresh teams in background (always try if we should use managed keys) + useEffect(() => { + if ( + integrationType === "ai-gateway" && + aiGatewayStatus?.enabled && + aiGatewayStatus?.isVercelUser + ) { + // Always try to refresh teams - handles token refresh after re-auth + api.aiGateway + .getTeams() + .then((response) => { + if (response.teams.length > 0) { + setTeams(response.teams); + setTeamsFetched(true); + } + }) + .catch(() => { + // Silently fail background refresh + }); + } + // Only run on mount and when status changes + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [integrationType, aiGatewayStatus?.enabled, aiGatewayStatus?.isVercelUser]); + useEffect(() => { loadIntegrations(); }, [loadIntegrations, integrationType]); @@ -73,10 +170,16 @@ export function IntegrationSelector({ } }, [integrationsVersion, loadIntegrations]); - // Auto-select single integration from cached data + // Auto-select first integration when none is selected or current selection is invalid useEffect(() => { - if (integrations.length === 1 && !value && !disabled) { - onChange(integrations[0].id); + if (integrations.length > 0 && !disabled) { + // Check if current value exists in available integrations + const currentExists = value && integrations.some((i) => i.id === value); + if (!currentExists) { + // Prefer managed integrations, fall back to first available + const managed = integrations.find((i) => i.isManaged); + onChange(managed?.id || integrations[0].id); + } } }, [integrations, value, disabled, onChange]); @@ -94,13 +197,48 @@ export function IntegrationSelector({ setIntegrationsVersion((v) => v + 1); }; - const handleAddConnection = () => { + const handleDelete = async () => { + await loadIntegrations(); + setEditingIntegration(null); + setIntegrationsVersion((v) => v + 1); + // Refresh AI Gateway status if this is an AI Gateway integration + if (integrationType === "ai-gateway") { + const status = await api.aiGateway.getStatus(); + setAiGatewayStatus(status); + } + }; + + // Check if AI Gateway managed keys should be used + const shouldUseManagedKeys = + integrationType === "ai-gateway" && + aiGatewayStatus?.enabled && + aiGatewayStatus?.isVercelUser && + !aiGatewayStatus?.hasManagedKey; + + const handleConsentSuccess = useCallback(async (integrationId: string) => { + await loadIntegrations(); + onChange(integrationId); + setIntegrationsVersion((v) => v + 1); + // Refetch AI Gateway status + const status = await api.aiGateway.getStatus(); + setAiGatewayStatus(status); + }, [loadIntegrations, onChange, setIntegrationsVersion, setAiGatewayStatus]); + + const handleAddConnection = useCallback(() => { if (onAddConnection) { onAddConnection(); + } else if (shouldUseManagedKeys) { + // For AI Gateway with managed keys enabled, show consent modal + openConsentModal({ + onConsent: handleConsentSuccess, + onManualEntry: () => { + setShowNewDialog(true); + }, + }); } else { setShowNewDialog(true); } - }; + }, [onAddConnection, shouldUseManagedKeys, openConsentModal, handleConsentSuccess]); // Only show loading skeleton if we have no cached data and haven't fetched yet if (!hasCachedData && !hasFetched) { @@ -118,7 +256,11 @@ export function IntegrationSelector({ const plugin = getIntegration(integrationType); const integrationLabel = plugin?.label || integrationType; - // No integrations - show error button to add one + // Separate managed and manual integrations for AI Gateway + const managedIntegrations = integrations.filter((i) => i.isManaged); + const manualIntegrations = integrations.filter((i) => !i.isManaged); + + // No integrations - show add button if (integrations.length === 0) { return ( <> @@ -172,16 +314,20 @@ export function IntegrationSelector({
+ setShowNewDialog(false)} + onSuccess={handleNewIntegrationCreated} + open={showNewDialog} + preselectedType={integrationType} + /> + {editingIntegration && ( setEditingIntegration(null)} - onDelete={async () => { - await loadIntegrations(); - setEditingIntegration(null); - setIntegrationsVersion((v) => v + 1); - }} + onDelete={handleDelete} onSuccess={handleEditSuccess} open /> @@ -190,11 +336,54 @@ export function IntegrationSelector({ ); } - // Multiple integrations - show radio-style selection list + // Multiple integrations or AI Gateway with option to add managed key return ( <>
- {integrations.map((integration) => { + {/* Show managed integrations first */} + {managedIntegrations.map((integration) => { + const isSelected = value === integration.id; + const displayName = integration.name || `${integrationLabel} API Key`; + return ( +
+ + +
+ ); + })} + + {/* Show manual integrations */} + {manualIntegrations.map((integration) => { const isSelected = value === integration.id; const displayName = integration.name || `${integrationLabel} API Key`; @@ -202,9 +391,7 @@ export function IntegrationSelector({
); })} + {onOpenSettings && (
- {integrationType && ( + {integrationType && isOwner && (
@@ -418,24 +451,15 @@ export function ActionConfig({
- - - - - - - - Add Secondary Connection(s) - - - +
onUpdateConfig("integrationId", id)} value={(config?.integrationId as string) || ""} /> + setShowAddConnectionDialog(false)} + onSuccess={(integrationId) => { + setShowAddConnectionDialog(false); + setIntegrationsVersion((v) => v + 1); + onUpdateConfig("integrationId", integrationId); + }} + open={showAddConnectionDialog} + preselectedType={integrationType} + />
)} diff --git a/components/workflow/config/action-grid.tsx b/components/workflow/config/action-grid.tsx index 69decbb2b..20f986d60 100644 --- a/components/workflow/config/action-grid.tsx +++ b/components/workflow/config/action-grid.tsx @@ -251,7 +251,7 @@ export function ActionGrid({
- + { ) : null} diff --git a/components/workflow/workflow-runs.tsx b/components/workflow/workflow-runs.tsx index ec93e1670..e8f95ffa6 100644 --- a/components/workflow/workflow-runs.tsx +++ b/components/workflow/workflow-runs.tsx @@ -426,7 +426,7 @@ function ExecutionLogEntry({ return (
{/* Timeline connector */} -
+
{!isFirst && (
)} diff --git a/components/workflows/user-menu.tsx b/components/workflows/user-menu.tsx index 43013b132..772872c39 100644 --- a/components/workflows/user-menu.tsx +++ b/components/workflows/user-menu.tsx @@ -149,7 +149,7 @@ export const UserMenu = () => { - + Theme diff --git a/drizzle/0004_real_wither.sql b/drizzle/0004_real_wither.sql new file mode 100644 index 000000000..1eb476a85 --- /dev/null +++ b/drizzle/0004_real_wither.sql @@ -0,0 +1 @@ +ALTER TABLE "integrations" ADD COLUMN "is_managed" boolean DEFAULT false; \ No newline at end of file diff --git a/drizzle/meta/0004_snapshot.json b/drizzle/meta/0004_snapshot.json new file mode 100644 index 000000000..3a4b9ccb0 --- /dev/null +++ b/drizzle/meta/0004_snapshot.json @@ -0,0 +1,731 @@ +{ + "id": "0832c6e0-e9ef-4979-af70-f53f1f15d10a", + "prevId": "725be3c3-851b-481a-91d1-080feccc324d", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "accounts_user_id_users_id_fk": { + "name": "accounts_user_id_users_id_fk", + "tableFrom": "accounts", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_prefix": { + "name": "key_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integrations": { + "name": "integrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "is_managed": { + "name": "is_managed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "integrations_user_id_users_id_fk": { + "name": "integrations_user_id_users_id_fk", + "tableFrom": "integrations", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sessions_token_unique": { + "name": "sessions_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "is_anonymous": { + "name": "is_anonymous", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verifications": { + "name": "verifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_logs": { + "name": "workflow_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "node_id": { + "name": "node_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "node_name": { + "name": "node_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "node_type": { + "name": "node_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input": { + "name": "input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "duration": { + "name": "duration", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workflow_execution_logs_execution_id_workflow_executions_id_fk": { + "name": "workflow_execution_logs_execution_id_workflow_executions_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_executions", + "columnsFrom": ["execution_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_executions": { + "name": "workflow_executions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input": { + "name": "input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "duration": { + "name": "duration", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "workflow_executions_workflow_id_workflows_id_fk": { + "name": "workflow_executions_workflow_id_workflows_id_fk", + "tableFrom": "workflow_executions", + "tableTo": "workflows", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_executions_user_id_users_id_fk": { + "name": "workflow_executions_user_id_users_id_fk", + "tableFrom": "workflow_executions", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflows": { + "name": "workflows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "nodes": { + "name": "nodes", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "edges": { + "name": "edges", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'private'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workflows_user_id_users_id_fk": { + "name": "workflows_user_id_users_id_fk", + "tableFrom": "workflows", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index c760c27f3..c3cd4f1a1 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -29,6 +29,13 @@ "when": 1764694021611, "tag": "0003_clammy_tusk", "breakpoints": true + }, + { + "idx": 4, + "version": "7", + "when": 1765834764376, + "tag": "0004_real_wither", + "breakpoints": true } ] } diff --git a/lib/ai-gateway/config.ts b/lib/ai-gateway/config.ts new file mode 100644 index 000000000..5cb047165 --- /dev/null +++ b/lib/ai-gateway/config.ts @@ -0,0 +1,27 @@ +/** + * AI Gateway Managed Keys Configuration + * + * This feature allows signed-in users to use their own Vercel AI Gateway + * API keys (and credits) instead of manually entering an API key. + * + * The AI Gateway itself is available to everyone via AI_GATEWAY_API_KEY. + * This feature flag only controls the ability to create API keys on behalf + * of users through OAuth - which is an internal Vercel feature. + * + * Set AI_GATEWAY_MANAGED_KEYS_ENABLED=true to enable. + */ + +export function isAiGatewayManagedKeysEnabled(): boolean { + return process.env.AI_GATEWAY_MANAGED_KEYS_ENABLED === "true"; +} + +/** + * Check if managed keys feature is enabled on the client side + * Uses NEXT_PUBLIC_ prefix for client-side access + */ +export function isAiGatewayManagedKeysEnabledClient(): boolean { + if (typeof window === "undefined") { + return process.env.AI_GATEWAY_MANAGED_KEYS_ENABLED === "true"; + } + return process.env.NEXT_PUBLIC_AI_GATEWAY_MANAGED_KEYS_ENABLED === "true"; +} diff --git a/lib/ai-gateway/state.ts b/lib/ai-gateway/state.ts new file mode 100644 index 000000000..736c45026 --- /dev/null +++ b/lib/ai-gateway/state.ts @@ -0,0 +1,65 @@ +"use client"; + +import { atom } from "jotai"; +import type { VercelTeam } from "@/lib/api-client"; + +/** + * AI Gateway consent modal state + */ +export const showAiGatewayConsentModalAtom = atom(false); + +/** + * Callbacks for the consent modal - stored in atoms so any component can set them + */ +export type AiGatewayConsentCallbacks = { + onConsent?: (integrationId: string) => void; + onManualEntry?: () => void; + onDecline?: () => void; +}; + +export const aiGatewayConsentCallbacksAtom = atom( + {} +); + +/** + * Write-only atom to open the consent modal with specific callbacks. + * Usage: const openModal = useSetAtom(openAiGatewayConsentModalAtom); + * openModal({ onConsent: (id) => ..., onManualEntry: () => ... }); + */ +export const openAiGatewayConsentModalAtom = atom( + null, + (get, set, callbacks: AiGatewayConsentCallbacks) => { + set(aiGatewayConsentCallbacksAtom, callbacks); + set(showAiGatewayConsentModalAtom, true); + } +); + +/** + * AI Gateway status (fetched from API) + */ +export type AiGatewayStatus = { + /** Whether the user keys feature is enabled */ + enabled: boolean; + /** Whether the user is signed in */ + signedIn: boolean; + /** Whether the user signed in with Vercel OAuth */ + isVercelUser: boolean; + /** Whether the user has a managed AI Gateway integration */ + hasManagedKey: boolean; + /** The ID of the managed integration (if exists) */ + managedIntegrationId?: string; +} | null; + +export const aiGatewayStatusAtom = atom(null); + +/** + * Loading state for consent action + */ +export const aiGatewayConsentLoadingAtom = atom(false); + +/** + * Vercel teams for the current user + */ +export const aiGatewayTeamsAtom = atom([]); +export const aiGatewayTeamsLoadingAtom = atom(false); +export const aiGatewayTeamsFetchedAtom = atom(false); diff --git a/lib/api-client.ts b/lib/api-client.ts index 583e372ca..73224e40a 100644 --- a/lib/api-client.ts +++ b/lib/api-client.ts @@ -320,6 +320,7 @@ export type Integration = { id: string; name: string; type: IntegrationType; + isManaged?: boolean; createdAt: string; updatedAt: string; }; @@ -328,6 +329,34 @@ export type IntegrationWithConfig = Integration & { config: IntegrationConfig; }; +// AI Gateway types +export type AiGatewayStatusResponse = { + enabled: boolean; + signedIn: boolean; + isVercelUser: boolean; + hasManagedKey: boolean; + managedIntegrationId?: string; +}; + +export type AiGatewayConsentResponse = { + success: boolean; + hasManagedKey: boolean; + managedIntegrationId?: string; + error?: string; +}; + +export type VercelTeam = { + id: string; + name: string; + slug: string; + avatar?: string; + isPersonal: boolean; +}; + +export type AiGatewayTeamsResponse = { + teams: VercelTeam[]; +}; + // Integration API export const integrationApi = { // List all integrations @@ -603,9 +632,32 @@ export const workflowApi = { })(), }; +// AI Gateway API (User Keys feature) +export const aiGatewayApi = { + // Get status (whether feature is enabled, user has managed key, etc.) + getStatus: () => apiCall("/api/ai-gateway/status"), + + // Get available Vercel teams + getTeams: () => apiCall("/api/ai-gateway/teams"), + + // Grant consent and create managed API key + consent: (teamId: string, teamName: string) => + apiCall("/api/ai-gateway/consent", { + method: "POST", + body: JSON.stringify({ teamId, teamName }), + }), + + // Revoke consent and delete managed API key + revokeConsent: () => + apiCall("/api/ai-gateway/consent", { + method: "DELETE", + }), +}; + // Export all APIs as a single object export const api = { ai: aiApi, + aiGateway: aiGatewayApi, integration: integrationApi, user: userApi, workflow: workflowApi, diff --git a/lib/auth.ts b/lib/auth.ts index 9be288e84..3149038e2 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -2,6 +2,7 @@ import { betterAuth } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { anonymous, genericOAuth } from "better-auth/plugins"; import { eq } from "drizzle-orm"; +import { isAiGatewayManagedKeysEnabled } from "./ai-gateway/config"; import { db } from "./db"; import { accounts, @@ -105,7 +106,11 @@ const plugins = [ authorizationUrl: "https://vercel.com/oauth/authorize", tokenUrl: "https://api.vercel.com/login/oauth/token", userInfoUrl: "https://api.vercel.com/login/oauth/userinfo", - scopes: ["openid", "email", "profile"], + // Include read-write:team scope when AI Gateway User Keys is enabled + // This grants APIKey and APIKeyAiGateway permissions for creating user keys + scopes: isAiGatewayManagedKeysEnabled() + ? ["openid", "email", "profile", "read-write:team"] + : ["openid", "email", "profile"], discoveryUrl: undefined, pkce: true, getUserInfo: async (tokens) => { diff --git a/lib/db/index.ts b/lib/db/index.ts index 8f4c186d2..0596f0e15 100644 --- a/lib/db/index.ts +++ b/lib/db/index.ts @@ -1,8 +1,10 @@ +import type { PostgresJsDatabase } from "drizzle-orm/postgres-js"; import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; import { accounts, apiKeys, + integrations, sessions, users, verifications, @@ -23,6 +25,7 @@ const schema = { workflowExecutionLogs, workflowExecutionsRelations, apiKeys, + integrations, }; const connectionString = @@ -31,6 +34,18 @@ const connectionString = // For migrations export const migrationClient = postgres(connectionString, { max: 1 }); -// For queries -const queryClient = postgres(connectionString); -export const db = drizzle(queryClient, { schema }); +// Use global singleton to prevent connection exhaustion during HMR +const globalForDb = globalThis as unknown as { + queryClient: ReturnType | undefined; + db: PostgresJsDatabase | undefined; +}; + +// For queries - reuse connection in development +const queryClient = + globalForDb.queryClient ?? postgres(connectionString, { max: 10 }); +export const db = globalForDb.db ?? drizzle(queryClient, { schema }); + +if (process.env.NODE_ENV !== "production") { + globalForDb.queryClient = queryClient; + globalForDb.db = db; +} diff --git a/lib/db/integrations.ts b/lib/db/integrations.ts index 42a04c1f1..c9454b000 100644 --- a/lib/db/integrations.ts +++ b/lib/db/integrations.ts @@ -101,6 +101,7 @@ export type DecryptedIntegration = { name: string; type: IntegrationType; config: IntegrationConfig; + isManaged: boolean | null; createdAt: Date; updatedAt: Date; }; diff --git a/lib/db/schema.ts b/lib/db/schema.ts index 8f1dad644..abcf48cfd 100644 --- a/lib/db/schema.ts +++ b/lib/db/schema.ts @@ -93,6 +93,8 @@ export const integrations = pgTable("integrations", { type: text("type").notNull().$type(), // biome-ignore lint/suspicious/noExplicitAny: JSONB type - encrypted credentials stored as JSON config: jsonb("config").notNull().$type(), + // Whether this integration was created via OAuth (managed by app) vs manual entry + isManaged: boolean("is_managed").default(false), createdAt: timestamp("created_at").notNull().defaultNow(), updatedAt: timestamp("updated_at").notNull().defaultNow(), }); diff --git a/package.json b/package.json index e700a06d5..3e86c840c 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@linear/sdk": "^63.2.0", "@mendable/firecrawl-js": "^4.6.2", "@monaco-editor/react": "^4.7.0", + "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-context-menu": "^2.2.16", "@radix-ui/react-dialog": "^1.1.15", @@ -41,6 +42,7 @@ "clsx": "^2.1.1", "dotenv": "^17.2.3", "drizzle-orm": "^0.44.7", + "jose": "^6.1.3", "jotai": "^2.15.1", "jszip": "^3.10.1", "lucide-react": "^0.552.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6163a35eb..f5c01ca9c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@monaco-editor/react': specifier: ^4.7.0 version: 4.7.0(monaco-editor@0.54.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-checkbox': + specifier: ^1.3.3 + version: 1.3.3(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) '@radix-ui/react-collapsible': specifier: ^1.1.12 version: 1.1.12(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) @@ -68,6 +71,9 @@ importers: drizzle-orm: specifier: ^0.44.7 version: 0.44.7(@opentelemetry/api@1.9.0)(kysely@0.28.8)(postgres@3.4.7) + jose: + specifier: ^6.1.3 + version: 6.1.3 jotai: specifier: ^2.15.1 version: 2.15.1(@babel/core@7.28.5)(@babel/template@7.27.2)(@types/react@19.2.2)(react@19.2.1) @@ -3532,8 +3538,8 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true - jose@6.1.0: - resolution: {integrity: sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA==} + jose@6.1.3: + resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} jotai@2.15.1: resolution: {integrity: sha512-yHT1HAZ3ba2Q8wgaUQ+xfBzEtcS8ie687I8XVCBinfg4bNniyqLIN+utPXWKQE93LMF5fPbQSVRZqgpcN5yd6Q==} @@ -5172,19 +5178,19 @@ snapshots: '@babel/helper-validator-identifier': 7.28.5 optional: true - '@better-auth/core@1.3.34(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.0.19)(jose@6.1.0)(kysely@0.28.8)(nanostores@1.0.1)': + '@better-auth/core@1.3.34(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.0.19)(jose@6.1.3)(kysely@0.28.8)(nanostores@1.0.1)': dependencies: '@better-auth/utils': 0.3.0 '@better-fetch/fetch': 1.1.18 better-call: 1.0.19 - jose: 6.1.0 + jose: 6.1.3 kysely: 0.28.8 nanostores: 1.0.1 zod: 4.1.12 - '@better-auth/telemetry@1.3.34(better-call@1.0.19)(jose@6.1.0)(kysely@0.28.8)(nanostores@1.0.1)': + '@better-auth/telemetry@1.3.34(better-call@1.0.19)(jose@6.1.3)(kysely@0.28.8)(nanostores@1.0.1)': dependencies: - '@better-auth/core': 1.3.34(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.0.19)(jose@6.1.0)(kysely@0.28.8)(nanostores@1.0.1) + '@better-auth/core': 1.3.34(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.0.19)(jose@6.1.3)(kysely@0.28.8)(nanostores@1.0.1) '@better-auth/utils': 0.3.0 '@better-fetch/fetch': 1.1.18 transitivePeerDependencies: @@ -7548,8 +7554,8 @@ snapshots: better-auth@1.3.34(next@16.0.10(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1): dependencies: - '@better-auth/core': 1.3.34(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.0.19)(jose@6.1.0)(kysely@0.28.8)(nanostores@1.0.1) - '@better-auth/telemetry': 1.3.34(better-call@1.0.19)(jose@6.1.0)(kysely@0.28.8)(nanostores@1.0.1) + '@better-auth/core': 1.3.34(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.0.19)(jose@6.1.3)(kysely@0.28.8)(nanostores@1.0.1) + '@better-auth/telemetry': 1.3.34(better-call@1.0.19)(jose@6.1.3)(kysely@0.28.8)(nanostores@1.0.1) '@better-auth/utils': 0.3.0 '@better-fetch/fetch': 1.1.18 '@noble/ciphers': 2.0.1 @@ -7558,7 +7564,7 @@ snapshots: '@simplewebauthn/server': 13.2.2 better-call: 1.0.19 defu: 6.1.4 - jose: 6.1.0 + jose: 6.1.3 kysely: 0.28.8 nanostores: 1.0.1 zod: 4.1.12 @@ -8269,7 +8275,7 @@ snapshots: jiti@2.6.1: {} - jose@6.1.0: {} + jose@6.1.3: {} jotai@2.15.1(@babel/core@7.28.5)(@babel/template@7.27.2)(@types/react@19.2.2)(react@19.2.1): optionalDependencies: From d2fee079bb29bf35beb369ae8d46b835390dd302 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 15 Dec 2025 17:30:44 -0600 Subject: [PATCH 06/16] better models (#165) --- plugins/ai-gateway/index.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/plugins/ai-gateway/index.ts b/plugins/ai-gateway/index.ts index 3d32d6fb6..04da05219 100644 --- a/plugins/ai-gateway/index.ts +++ b/plugins/ai-gateway/index.ts @@ -64,7 +64,22 @@ const aiGatewayPlugin: IntegrationPlugin = { type: "select", defaultValue: "meta/llama-4-scout", options: [ + // Current models + { value: "anthropic/claude-sonnet-4.5", label: "Claude Sonnet 4.5" }, + { value: "anthropic/claude-haiku-4.5", label: "Claude Haiku 4.5" }, { value: "anthropic/claude-opus-4.5", label: "Claude Opus 4.5" }, + { value: "meta/llama-4-scout", label: "Llama 4 Scout" }, + { value: "meta/llama-4-maverick", label: "Llama 4 Maverick" }, + { value: "openai/gpt-5.2", label: "GPT-5.2" }, + { value: "openai/gpt-5.2-pro", label: "GPT-5.2 Pro" }, + { + value: "google/gemini-3-pro-preview", + label: "Gemini 3 Pro Preview", + }, + { value: "google/gemini-2.5-flash-lite", label: "Gemini 2.5 Flash Lite" }, + { value: "google/gemini-2.5-flash", label: "Gemini 2.5 Flash" }, + { value: "google/gemini-2.5-pro", label: "Gemini 2.5 Pro" }, + // Legacy models (kept for backwards compatibility) { value: "anthropic/claude-sonnet-4.0", label: "Claude Sonnet 4.0" }, { value: "anthropic/claude-3.5-sonnet-20241022", @@ -83,7 +98,6 @@ const aiGatewayPlugin: IntegrationPlugin = { value: "google/gemini-2.0-flash-lite", label: "Gemini 2.0 Flash Lite", }, - { value: "meta/llama-4-scout", label: "Llama 4 Scout" }, { value: "meta/llama-4-instruct", label: "Llama 4 Instruct" }, ], }, From a4c037072b3074f225f3124b407578a95b4aa483 Mon Sep 17 00:00:00 2001 From: taitsengstock Date: Tue, 16 Dec 2025 13:36:28 +1100 Subject: [PATCH 07/16] feat: Restore beta API alongside AI Gateway API --- lib/api-client.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/lib/api-client.ts b/lib/api-client.ts index 955f01c75..357db48ce 100644 --- a/lib/api-client.ts +++ b/lib/api-client.ts @@ -633,6 +633,23 @@ export const workflowApi = { })(), }; +// Beta API +export const betaApi = { + // Check if an email is on the allowlist + checkEmail: (email: string) => + apiCall<{ isAllowlisted: boolean }>("/api/beta/check-email", { + method: "POST", + body: JSON.stringify({ email }), + }), + + // Request beta access (stores email in DB) + requestAccess: (email: string) => + apiCall<{ success: boolean }>("/api/beta/request-access", { + method: "POST", + body: JSON.stringify({ email }), + }), +}; + // AI Gateway API (User Keys feature) export const aiGatewayApi = { // Get status (whether feature is enabled, user has managed key, etc.) @@ -658,6 +675,7 @@ export const aiGatewayApi = { // Export all APIs as a single object export const api = { ai: aiApi, + beta: betaApi, aiGateway: aiGatewayApi, integration: integrationApi, user: userApi, From dc53327f5af74bbdbaab7f4ff36b3516ae2d57d2 Mon Sep 17 00:00:00 2001 From: taitsengstock Date: Tue, 16 Dec 2025 13:37:10 +1100 Subject: [PATCH 08/16] feat: Add ethers and @radix-ui/react-slot for Web3 wallet support --- package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package.json b/package.json index d2adf821c..6a8d6e3bf 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-context-menu": "^2.2.16", "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tooltip": "^1.2.8", "@slack/web-api": "^7.12.0", "@vercel/analytics": "^1.5.0", @@ -46,6 +47,7 @@ "clsx": "^2.1.1", "dotenv": "^17.2.3", "drizzle-orm": "^0.44.7", + "ethers": "^6.15.0", "jose": "^6.1.3", "jotai": "^2.15.1", "jszip": "^3.10.1", From 52b9b56027c74d811f283792fff02643cb082d30 Mon Sep 17 00:00:00 2001 From: taitsengstock Date: Tue, 16 Dec 2025 13:40:58 +1100 Subject: [PATCH 09/16] feat: Integrate Web3 and SendGrid special handling into upstream dialog --- .../settings/integration-form-dialog.tsx | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/components/settings/integration-form-dialog.tsx b/components/settings/integration-form-dialog.tsx index 550227b52..89842ff9c 100644 --- a/components/settings/integration-form-dialog.tsx +++ b/components/settings/integration-form-dialog.tsx @@ -59,6 +59,8 @@ import { getSortedIntegrationTypes, } from "@/plugins"; import { getIntegrationDescriptions } from "@/plugins/registry"; +import { SendGridIntegrationSection } from "./sendgrid-integration-section"; +import { Web3WalletSection } from "./web3-wallet-section"; type IntegrationFormDialogProps = { open: boolean; @@ -201,7 +203,7 @@ function ConfigFields({ isEditMode, }: { formData: IntegrationFormData; - updateConfig: (key: string, value: string) => void; + updateConfig: (key: string, value: string | boolean) => void; isEditMode: boolean; }) { if (!formData.type) { @@ -224,12 +226,28 @@ function ConfigFields({ ); } + // Handle Web3 wallet creation + if (formData.type === "web3") { + return ; + } + // Get plugin form fields from registry const plugin = getIntegration(formData.type); if (!plugin?.formFields) { return null; } + // Handle SendGrid integration with special checkbox logic + if (formData.type === "sendgrid") { + return ( + + ); + } + return plugin.formFields.map((field) => { const isSecretField = field.type === "password"; From b8f701e4e327268e84d9d03175f9e55935b21d94 Mon Sep 17 00:00:00 2001 From: taitsengstock Date: Tue, 16 Dec 2025 13:44:13 +1100 Subject: [PATCH 10/16] feat: Restore IntegrationSelector in action node footer --- components/workflow/node-config-panel.tsx | 105 +++++++++++++++++----- 1 file changed, 82 insertions(+), 23 deletions(-) diff --git a/components/workflow/node-config-panel.tsx b/components/workflow/node-config-panel.tsx index 8183efbcf..721a71a6c 100644 --- a/components/workflow/node-config-panel.tsx +++ b/components/workflow/node-config-panel.tsx @@ -53,6 +53,7 @@ import { findActionById, getIntegration } from "@/plugins"; import { Panel } from "../ai-elements/panel"; import { IntegrationsDialog } from "../settings/integrations-dialog"; import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer"; +import { IntegrationSelector } from "../ui/integration-selector"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs"; import { ActionConfig } from "./config/action-config"; import { ActionGrid } from "./config/action-grid"; @@ -870,30 +871,88 @@ export const PanelInner = () => {
)} {selectedNode.data.type === "action" && isOwner && ( -
- + +
+ + {(() => { + const actionType = selectedNode.data.config + ?.actionType as string; + + // Database Query is special - has integration but no plugin + const SYSTEM_INTEGRATION_MAP: Record = { + "Database Query": "database", + }; + + // Get integration type dynamically + let integrationType: string | undefined; + let requiresCredentials = true; // Default to true for system actions + + if (actionType) { + if (SYSTEM_INTEGRATION_MAP[actionType]) { + integrationType = SYSTEM_INTEGRATION_MAP[actionType]; + } else { + // Look up from plugin registry + const action = findActionById(actionType); + if (action) { + integrationType = action.integration; + // Check if plugin requires credentials (default to true) + const plugin = getIntegration(action.integration); + requiresCredentials = + plugin?.requiresCredentials !== false; + } + } } - variant="ghost" - > - {selectedNode.data.enabled === false ? ( - - ) : ( - - )} - - + + // Show integration selector if credentials required, otherwise show info message + if (integrationType && requiresCredentials) { + return ( + handleUpdateConfig("integrationId", id)} + onOpenSettings={() => setShowIntegrationsDialog(true)} + value={ + (selectedNode.data.config?.integrationId as string) || + "" + } + /> + ); + } + if (integrationType && !requiresCredentials) { + return ( +
+ + No integration required + +
+ ); + } + return null; + })()}
)} {selectedNode.data.type === "trigger" && isOwner && ( From b9c086547438502bba1c64d2f6098cc93e3713d8 Mon Sep 17 00:00:00 2001 From: taitsengstock Date: Tue, 16 Dec 2025 14:00:06 +1100 Subject: [PATCH 11/16] fix: TypeScript errors in ai-gateway plugin and integrations --- components/settings/integrations-manager.tsx | 6 +++++- components/workflow/node-config-panel.tsx | 1 - plugins/ai-gateway/icon.tsx | 3 +++ plugins/ai-gateway/index.ts | 6 +++--- plugins/ai-gateway/test.ts | 15 +++++++++++++++ pnpm-lock.yaml | 8 ++++++++ 6 files changed, 34 insertions(+), 5 deletions(-) create mode 100644 plugins/ai-gateway/icon.tsx create mode 100644 plugins/ai-gateway/test.ts diff --git a/components/settings/integrations-manager.tsx b/components/settings/integrations-manager.tsx index f4c46097e..244c57c4c 100644 --- a/components/settings/integrations-manager.tsx +++ b/components/settings/integrations-manager.tsx @@ -16,7 +16,11 @@ import { import { Button } from "@/components/ui/button"; import { IntegrationIcon } from "@/components/ui/integration-icon"; import { Spinner } from "@/components/ui/spinner"; -import { api, type Integration } from "@/lib/api-client"; +import { + api, + type Integration, + type IntegrationWithConfig, +} from "@/lib/api-client"; import { getIntegrationLabels } from "@/plugins"; import { IntegrationFormDialog } from "./integration-form-dialog"; diff --git a/components/workflow/node-config-panel.tsx b/components/workflow/node-config-panel.tsx index 721a71a6c..398e70a73 100644 --- a/components/workflow/node-config-panel.tsx +++ b/components/workflow/node-config-panel.tsx @@ -932,7 +932,6 @@ export const PanelInner = () => { return ( handleUpdateConfig("integrationId", id)} onOpenSettings={() => setShowIntegrationsDialog(true)} value={ diff --git a/plugins/ai-gateway/icon.tsx b/plugins/ai-gateway/icon.tsx new file mode 100644 index 000000000..7b30b3aed --- /dev/null +++ b/plugins/ai-gateway/icon.tsx @@ -0,0 +1,3 @@ +import { Bot } from "lucide-react"; + +export default Bot; diff --git a/plugins/ai-gateway/index.ts b/plugins/ai-gateway/index.ts index 04da05219..e3210c77f 100644 --- a/plugins/ai-gateway/index.ts +++ b/plugins/ai-gateway/index.ts @@ -1,6 +1,6 @@ import type { IntegrationPlugin } from "../registry"; import { registerIntegration } from "../registry"; -import { AiGatewayIcon } from "./icon"; +import AiGatewayIcon from "./icon"; const aiGatewayPlugin: IntegrationPlugin = { type: "ai-gateway", @@ -27,8 +27,8 @@ const aiGatewayPlugin: IntegrationPlugin = { testConfig: { getTestFunction: async () => { - const { testAiGateway } = await import("./test"); - return testAiGateway; + const { testConnection } = await import("./test"); + return testConnection; }, }, diff --git a/plugins/ai-gateway/test.ts b/plugins/ai-gateway/test.ts new file mode 100644 index 000000000..fb2972ad0 --- /dev/null +++ b/plugins/ai-gateway/test.ts @@ -0,0 +1,15 @@ +/** + * Test connection for AI Gateway integration + * AI Gateway managed keys are tested through the Vercel API + */ +export async function testConnection(): Promise<{ + status: "success" | "error"; + message: string; +}> { + // AI Gateway connections are validated through the Vercel API consent flow + // No separate test needed as the managed key is created by Vercel + return { + status: "success", + message: "AI Gateway connection is managed by Vercel", + }; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b9cbcade0..dca17aa99 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,6 +44,9 @@ importers: '@radix-ui/react-dialog': specifier: ^1.1.15 version: 1.1.15(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-slot': + specifier: ^1.2.4 + version: 1.2.4(@types/react@19.2.2)(react@19.2.1) '@radix-ui/react-tooltip': specifier: ^1.2.8 version: 1.2.8(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) @@ -83,6 +86,9 @@ importers: drizzle-orm: specifier: ^0.44.7 version: 0.44.7(@opentelemetry/api@1.9.0)(kysely@0.28.8)(postgres@3.4.7) + ethers: + specifier: ^6.15.0 + version: 6.15.0 jose: specifier: ^6.1.3 version: 6.1.3 @@ -8221,6 +8227,8 @@ snapshots: baseline-browser-mapping@2.8.23: optional: true + bech32@1.1.4: {} + better-auth@1.3.34(next@16.0.10(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1): dependencies: '@better-auth/core': 1.3.34(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.0.19)(jose@6.1.3)(kysely@0.28.8)(nanostores@1.0.1) From 32e0903f4cca09ddf526e5042b341073ca615662 Mon Sep 17 00:00:00 2001 From: taitsengstock Date: Tue, 16 Dec 2025 14:03:13 +1100 Subject: [PATCH 12/16] fix: TypeScript type errors for config values and FormData --- components/settings/integration-form-dialog.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/components/settings/integration-form-dialog.tsx b/components/settings/integration-form-dialog.tsx index 89842ff9c..0f6f87bad 100644 --- a/components/settings/integration-form-dialog.tsx +++ b/components/settings/integration-form-dialog.tsx @@ -74,7 +74,7 @@ type IntegrationFormDialogProps = { type IntegrationFormData = { name: string; - type: IntegrationType; + type: IntegrationType | null; config: Record; }; @@ -221,7 +221,7 @@ function ConfigFields({ label="Database URL" onChange={updateConfig} placeholder="postgresql://user:password@host:port/database" - value={formData.config.url || ""} + value={(formData.config.url as string) || ""} /> ); } @@ -263,7 +263,7 @@ function ConfigFields({ label={field.label} onChange={updateConfig} placeholder={field.placeholder} - value={formData.config[field.configKey] || ""} + value={(formData.config[field.configKey] as string) || ""} /> ); } @@ -276,7 +276,7 @@ function ConfigFields({ onChange={(e) => updateConfig(field.configKey, e.target.value)} placeholder={field.placeholder} type={field.type} - value={formData.config[field.configKey] || ""} + value={(formData.config[field.configKey] as string) || ""} /> {(field.helpText || field.helpLink) && (

@@ -707,7 +707,7 @@ export function IntegrationFormDialog({ if (mode === "edit" && integration) { // Only include config if there are actual new values entered const hasNewConfig = Object.values(formData.config).some( - (v) => v && v.length > 0 + (v) => v && (typeof v === "boolean" || v.length > 0) ); await api.integration.update(integration.id, { name: integrationName, @@ -739,7 +739,7 @@ export function IntegrationFormDialog({ // Check if we have config values to test const hasConfig = Object.values(formData.config).some( - (v) => v && v.length > 0 + (v) => v && (typeof v === "boolean" || v.length > 0) ); // In edit mode without new config, skip testing @@ -813,7 +813,7 @@ export function IntegrationFormDialog({ // Check if we have any config values to test const hasConfig = Object.values(formData.config).some( - (v) => v && v.length > 0 + (v) => v && (typeof v === "boolean" || v.length > 0) ); if (!hasConfig && mode === "create") { toast.error("Please enter credentials first"); From 5282ea0be6ee8f4d64f73bda58630cd58ad2216f Mon Sep 17 00:00:00 2001 From: taitsengstock Date: Tue, 16 Dec 2025 14:29:32 +1100 Subject: [PATCH 13/16] fix: Remove SendGrid checkbox feature to simplify types - Remove sendgrid-integration-section component - Change config values from string|boolean to string only - Simplify SendGrid to always use KeeperHub API key - Fix IntegrationWithConfig handling in integrations-manager - Fix ai-gateway test function signature - All TypeScript errors resolved --- app/api/integrations/test/route.ts | 9 +- .../settings/integration-form-dialog.tsx | 22 +-- components/settings/integrations-manager.tsx | 5 +- .../settings/sendgrid-integration-section.tsx | 133 ------------------ plugins/ai-gateway/index.ts | 10 +- plugins/sendgrid/steps/send-email.ts | 13 +- 6 files changed, 27 insertions(+), 165 deletions(-) delete mode 100644 components/settings/sendgrid-integration-section.tsx diff --git a/app/api/integrations/test/route.ts b/app/api/integrations/test/route.ts index fc7838e5c..ce85c3141 100644 --- a/app/api/integrations/test/route.ts +++ b/app/api/integrations/test/route.ts @@ -44,7 +44,14 @@ export async function POST(request: Request) { } if (body.type === "database") { - const result = await testDatabaseConnection(body.config.url); + const url = body.config.url; + if (typeof url !== "string") { + return NextResponse.json( + { error: "Database URL must be a string" }, + { status: 400 } + ); + } + const result = await testDatabaseConnection(url); return NextResponse.json(result); } diff --git a/components/settings/integration-form-dialog.tsx b/components/settings/integration-form-dialog.tsx index 0f6f87bad..cbe2aeace 100644 --- a/components/settings/integration-form-dialog.tsx +++ b/components/settings/integration-form-dialog.tsx @@ -59,7 +59,6 @@ import { getSortedIntegrationTypes, } from "@/plugins"; import { getIntegrationDescriptions } from "@/plugins/registry"; -import { SendGridIntegrationSection } from "./sendgrid-integration-section"; import { Web3WalletSection } from "./web3-wallet-section"; type IntegrationFormDialogProps = { @@ -75,7 +74,7 @@ type IntegrationFormDialogProps = { type IntegrationFormData = { name: string; type: IntegrationType | null; - config: Record; + config: Record; }; // System integrations that don't have plugins @@ -203,7 +202,7 @@ function ConfigFields({ isEditMode, }: { formData: IntegrationFormData; - updateConfig: (key: string, value: string | boolean) => void; + updateConfig: (key: string, value: string) => void; isEditMode: boolean; }) { if (!formData.type) { @@ -237,17 +236,6 @@ function ConfigFields({ return null; } - // Handle SendGrid integration with special checkbox logic - if (formData.type === "sendgrid") { - return ( - - ); - } - return plugin.formFields.map((field) => { const isSecretField = field.type === "password"; @@ -707,7 +695,7 @@ export function IntegrationFormDialog({ if (mode === "edit" && integration) { // Only include config if there are actual new values entered const hasNewConfig = Object.values(formData.config).some( - (v) => v && (typeof v === "boolean" || v.length > 0) + (v) => v && v.length > 0 ); await api.integration.update(integration.id, { name: integrationName, @@ -739,7 +727,7 @@ export function IntegrationFormDialog({ // Check if we have config values to test const hasConfig = Object.values(formData.config).some( - (v) => v && (typeof v === "boolean" || v.length > 0) + (v) => v && v.length > 0 ); // In edit mode without new config, skip testing @@ -813,7 +801,7 @@ export function IntegrationFormDialog({ // Check if we have any config values to test const hasConfig = Object.values(formData.config).some( - (v) => v && (typeof v === "boolean" || v.length > 0) + (v) => v && v.length > 0 ); if (!hasConfig && mode === "create") { toast.error("Please enter credentials first"); diff --git a/components/settings/integrations-manager.tsx b/components/settings/integrations-manager.tsx index 244c57c4c..5adfca9c3 100644 --- a/components/settings/integrations-manager.tsx +++ b/components/settings/integrations-manager.tsx @@ -214,7 +214,10 @@ export function IntegrationsManager({

- {integrationType && isOwner && ( -
-
-
- - - - - - - -

API key or OAuth credentials for this service

-
-
-
+ {integrationType && + isOwner && + getIntegration(integrationType)?.requiresCredentials !== false && ( +
+
+
+ + + + + + + +

API key or OAuth credentials for this service

+
+
+
+
+
- + integrationType={integrationType} + onChange={(id) => onUpdateConfig("integrationId", id)} + value={(config?.integrationId as string) || ""} + /> + setShowAddConnectionDialog(false)} + onSuccess={(integrationId) => { + setShowAddConnectionDialog(false); + setIntegrationsVersion((v) => v + 1); + onUpdateConfig("integrationId", integrationId); + }} + open={showAddConnectionDialog} + preselectedType={integrationType} + />
- onUpdateConfig("integrationId", id)} - value={(config?.integrationId as string) || ""} - /> - setShowAddConnectionDialog(false)} - onSuccess={(integrationId) => { - setShowAddConnectionDialog(false); - setIntegrationsVersion((v) => v + 1); - onUpdateConfig("integrationId", integrationId); - }} - open={showAddConnectionDialog} - preselectedType={integrationType} - /> -
- )} + )} {/* System actions - hardcoded config fields */} {config?.actionType === "HTTP Request" && (