From f4ce53afa4c5794a3f16a49d4a0b398cce4dd891 Mon Sep 17 00:00:00 2001 From: 0xFirekeeper <0xFirekeeper@gmail.com> Date: Tue, 21 Apr 2026 01:24:04 +0700 Subject: [PATCH 1/2] Handle managed vault rotation and redirect Add structured handling for secret key rotation when a managed vault blocks rotation: introduce MANAGED_VAULT_BLOCKS_ROTATION_CODE and a RotateSecretKeyError class, parse JSON error bodies from the API, and throw the structured error with status and code. Propagate a vaultConfigUrl prop through FTUX, SecretKeySection, and ProjectGeneralSettingsPage, and update the rotation modal to detect the managed-vault error, show an explanatory toast, close the modal, and redirect the user to the vault configuration page. Also remove the now-unused direct vault rotation import and improve error messaging so the UI can react appropriately. --- apps/dashboard/src/@/hooks/useApi.ts | 46 +++++++++++++------ .../components/ProjectFTUX/ProjectFTUX.tsx | 14 +++++- .../ProjectFTUX/SecretKeySection.tsx | 2 + .../settings/ProjectGeneralSettingsPage.tsx | 24 ++++++++++ 4 files changed, 70 insertions(+), 16 deletions(-) diff --git a/apps/dashboard/src/@/hooks/useApi.ts b/apps/dashboard/src/@/hooks/useApi.ts index faf7d93c048..9bf41fa537a 100644 --- a/apps/dashboard/src/@/hooks/useApi.ts +++ b/apps/dashboard/src/@/hooks/useApi.ts @@ -2,7 +2,6 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useActiveAccount } from "thirdweb/react"; import { apiServerProxy } from "@/actions/proxies"; import type { Project } from "@/api/project/projects"; -import { rotateVaultAccountAndAccessToken } from "../../app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/lib/vault.client"; import { accountKeys, authorizedWallets } from "../query-keys/cache-keys"; // FIXME: We keep repeating types, API server should provide them @@ -316,6 +315,20 @@ export type RotateSecretKeyAPIReturnType = { }; }; +export const MANAGED_VAULT_BLOCKS_ROTATION_CODE = + "MANAGED_VAULT_BLOCKS_ROTATION"; + +export class RotateSecretKeyError extends Error { + code: string | undefined; + status: number; + constructor(message: string, status: number, code?: string) { + super(message); + this.name = "RotateSecretKeyError"; + this.status = status; + this.code = code; + } +} + export async function rotateSecretKeyClient(params: { project: Project }) { const res = await apiServerProxy({ body: JSON.stringify({}), @@ -327,19 +340,24 @@ export async function rotateSecretKeyClient(params: { project: Project }) { }); if (!res.ok) { - throw new Error(res.error); - } - - // if the project has an encrypted vault admin key, rotate it as well - const service = params.project.services.find( - (service) => service.name === "engineCloud", - ); - if (service?.encryptedAdminKey) { - await rotateVaultAccountAndAccessToken({ - project: params.project, - projectSecretKey: res.data.data.secret, - projectSecretHash: res.data.data.secretHash, - }); + // The error body is a JSON-serialized `{ error: { code, message, ... } }` + // payload from the api-server. Try to extract the structured code so the + // UI can react (e.g. redirect users with a managed vault to the vault + // configuration page so they can eject first). + let code: string | undefined; + let message = res.error; + try { + const parsed = JSON.parse(res.error) as { + error?: { code?: string; message?: string }; + }; + code = parsed.error?.code; + if (parsed.error?.message) { + message = parsed.error.message; + } + } catch { + // not JSON, fall through with raw text + } + throw new RotateSecretKeyError(message, res.status, code); } return res.data; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectFTUX/ProjectFTUX.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectFTUX/ProjectFTUX.tsx index c1099a5243a..99b217535ef 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectFTUX/ProjectFTUX.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectFTUX/ProjectFTUX.tsx @@ -32,7 +32,10 @@ export function ProjectFTUX(props: { }) { return (
- + {props.projectWalletSection} )}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectFTUX/SecretKeySection.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectFTUX/SecretKeySection.tsx index 7db372715a5..400a59f2854 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectFTUX/SecretKeySection.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectFTUX/SecretKeySection.tsx @@ -8,6 +8,7 @@ import { RotateSecretKeyButton } from "../../settings/ProjectGeneralSettingsPage export function SecretKeySection(props: { secretKeyMasked: string; project: Project; + vaultConfigUrl: string; }) { const [secretKeyMasked, setSecretKeyMasked] = useState(props.secretKeyMasked); @@ -34,6 +35,7 @@ export function SecretKeySection(props: { project: props.project, }); }} + vaultConfigUrl={props.vaultConfigUrl} /> diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/ProjectGeneralSettingsPage.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/ProjectGeneralSettingsPage.tsx index 3fa9646708e..e59860eab93 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/ProjectGeneralSettingsPage.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/ProjectGeneralSettingsPage.tsx @@ -62,6 +62,8 @@ import { ToolTipLabel } from "@/components/ui/tooltip"; import type { RotateSecretKeyAPIReturnType } from "@/hooks/useApi"; import { deleteProjectClient, + MANAGED_VAULT_BLOCKS_ROTATION_CODE, + RotateSecretKeyError, rotateSecretKeyClient, updateProjectClient, } from "@/hooks/useApi"; @@ -343,6 +345,7 @@ export function ProjectGeneralSettingsPageUI(props: { @@ -975,6 +981,7 @@ function DeleteProject(props: { export function RotateSecretKeyButton(props: { rotateSecretKey: RotateSecretKey; onSuccess: (data: RotateSecretKeyAPIReturnType) => void; + vaultConfigUrl: string; }) { const [isOpen, setIsOpen] = useState(false); const [isModalCloseAllowed, setIsModalCloseAllowed] = useState(true); @@ -1011,6 +1018,7 @@ export function RotateSecretKeyButton(props: { disableModalClose={() => setIsModalCloseAllowed(false)} onSuccess={props.onSuccess} rotateSecretKey={props.rotateSecretKey} + vaultConfigUrl={props.vaultConfigUrl} /> @@ -1026,6 +1034,7 @@ function RotateSecretKeyModalContent(props: { closeModal: () => void; disableModalClose: () => void; onSuccess: (data: RotateSecretKeyAPIReturnType) => void; + vaultConfigUrl: string; }) { const [screen, setScreen] = useState({ id: "initial", @@ -1050,6 +1059,7 @@ function RotateSecretKeyModalContent(props: { setScreen({ id: "save-newkey", secretKey: data.data.secret }); }} rotateSecretKey={props.rotateSecretKey} + vaultConfigUrl={props.vaultConfigUrl} /> ); } @@ -1061,12 +1071,26 @@ function RotateSecretKeyInitialScreen(props: { rotateSecretKey: RotateSecretKey; onSuccess: (data: RotateSecretKeyAPIReturnType) => void; closeModal: () => void; + vaultConfigUrl: string; }) { + const router = useDashboardRouter(); const [isConfirmed, setIsConfirmed] = useState(false); const rotateKeyMutation = useMutation({ mutationFn: props.rotateSecretKey, onError: (err) => { console.error(err); + if ( + err instanceof RotateSecretKeyError && + err.code === MANAGED_VAULT_BLOCKS_ROTATION_CODE + ) { + toast.error("Eject your server-wallet vault first", { + description: + "This project has a managed vault. Redirecting you to the vault configuration page so you can eject it before rotating the secret key.", + }); + props.closeModal(); + router.push(props.vaultConfigUrl); + return; + } toast.error("Failed to rotate secret key"); }, onSuccess: (data) => { From d36e28eef260d0c14a363bb0f6ecb491ecf307e7 Mon Sep 17 00:00:00 2001 From: 0xFirekeeper <0xFirekeeper@gmail.com> Date: Tue, 21 Apr 2026 01:38:10 +0700 Subject: [PATCH 2/2] Add vaultConfig path and improve error toast Add a new vaultConfig entry to ProjectSettingPaths and include it in the paths map to centralize the server-wallets configuration URL. Replace a hardcoded vault config URL in ProjectKeyDetails with paths.vaultConfig. Also enhance the secret key rotation error handling by including the actual error message in the toast description for better debugging. --- .../(sidebar)/settings/ProjectGeneralSettingsPage.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/ProjectGeneralSettingsPage.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/ProjectGeneralSettingsPage.tsx index e59860eab93..1b31742e57a 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/ProjectGeneralSettingsPage.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/ProjectGeneralSettingsPage.tsx @@ -102,6 +102,7 @@ type ProjectSettingPaths = { inAppConfig: string; aaConfig: string; payConfig: string; + vaultConfig: string; afterDeleteRedirectTo: string; }; @@ -226,6 +227,7 @@ export function ProjectGeneralSettingsPageUI(props: { afterDeleteRedirectTo: `/team/${props.teamSlug}`, inAppConfig: `${projectLayout}/wallets/user-wallets/configuration`, payConfig: `${projectLayout}/bridge/configuration`, + vaultConfig: `${projectLayout}/wallets/server-wallets/configuration`, }; const { project } = props; @@ -345,7 +347,7 @@ export function ProjectGeneralSettingsPageUI(props: { { props.onSuccess(data);