Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/icy-islands-dress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@thirdweb-dev/service-utils": patch
---

Add encryption utilities
5 changes: 5 additions & 0 deletions .changeset/loud-mails-peel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"thirdweb": patch
---

Make vault access token optional
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { createProjectClient } from "@/hooks/useApi";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { projectDomainsSchema, projectNameSchema } from "@/schema/validations";
import { toArrFromList } from "@/utils/string";
import { createVaultAccountAndAccessToken } from "../../../../app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/lib/vault.client";

const ALL_PROJECT_SERVICES = SERVICES.filter(
(srv) => srv.name !== "relayer" && srv.name !== "chainsaw",
Expand All @@ -63,6 +64,10 @@ const CreateProjectDialog = (props: CreateProjectDialogProps) => {
<CreateProjectDialogUI
createProject={async (params) => {
const res = await createProjectClient(props.teamId, params);
await createVaultAccountAndAccessToken({
project: res.project,
projectSecretKey: res.secret,
});
return {
project: res.project,
secret: res.secret,
Expand Down
20 changes: 15 additions & 5 deletions apps/dashboard/src/@/hooks/useApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useActiveAccount } from "thirdweb/react";
import { apiServerProxy } from "@/actions/proxies";
import type { Project } from "@/api/projects";
import { createVaultAccountAndAccessToken } 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
Expand Down Expand Up @@ -311,26 +312,35 @@ export type RotateSecretKeyAPIReturnType = {
data: {
secret: string;
secretMasked: string;
secretHash: string;
};
};

export async function rotateSecretKeyClient(params: {
teamId: string;
projectId: string;
}) {
export async function rotateSecretKeyClient(params: { project: Project }) {
const res = await apiServerProxy<RotateSecretKeyAPIReturnType>({
body: JSON.stringify({}),
headers: {
"Content-Type": "application/json",
},
method: "POST",
pathname: `/v1/teams/${params.teamId}/projects/${params.projectId}/rotate-secret-key`,
pathname: `/v1/teams/${params.project.teamId}/projects/${params.project.id}/rotate-secret-key`,
});

if (!res.ok) {
throw new Error(res.error);
}

try {
// if the project has a vault admin key, rotate it as well
await createVaultAccountAndAccessToken({
project: params.project,
projectSecretKey: res.data.data.secret,
projectSecretHash: res.data.data.secretHash,
});
} catch (error) {
console.error("Failed to rotate vault admin key", error);
}

Comment on lines +333 to +343
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider the error handling approach for vault rotation.

The vault admin key rotation is attempted after secret key rotation, with errors being caught and logged silently. While this ensures the main flow isn't interrupted, it might leave the vault in an inconsistent state.

Consider:

  1. Returning a status indicator about the vault rotation result
  2. Adding user-facing feedback if vault rotation fails
  3. Documenting this behavior for API consumers
🤖 Prompt for AI Agents
In apps/dashboard/src/@/hooks/useApi.ts around lines 333 to 343, the vault admin
key rotation errors are caught and logged silently, which may cause inconsistent
state without user awareness. Modify the code to return a status indicator
reflecting the success or failure of the vault rotation, provide user-facing
feedback when rotation fails, and update the function documentation to clearly
describe this behavior for API consumers.

return res.data;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,8 @@ function IntegrateAPIKeySection({
<ClientIDSection clientId={clientId} />
{secretKeyMasked && (
<SecretKeySection
projectId={project.id}
project={project}
secretKeyMasked={secretKeyMasked}
teamId={project.teamId}
/>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
"use client";

import { useState } from "react";
import type { Project } from "@/api/projects";
import { rotateSecretKeyClient } from "@/hooks/useApi";
import { RotateSecretKeyButton } from "../../settings/ProjectGeneralSettingsPage";

export function SecretKeySection(props: {
secretKeyMasked: string;
teamId: string;
projectId: string;
project: Project;
}) {
const [secretKeyMasked, setSecretKeyMasked] = useState(props.secretKeyMasked);

Expand All @@ -31,8 +31,7 @@ export function SecretKeySection(props: {
}}
rotateSecretKey={async () => {
return rotateSecretKeyClient({
projectId: props.projectId,
teamId: props.teamId,
project: props.project,
});
}}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ function Story(props: { isOwnerAccount: boolean }) {
data: {
secret: `sk_${new Array(86).fill("x").join("")}`,
secretMasked: "sk_123...4567",
secretHash: "sk_123...4567",
},
};
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,7 @@ export function ProjectGeneralSettingsPage(props: {
project={props.project}
rotateSecretKey={async () => {
return rotateSecretKeyClient({
projectId: props.project.id,
teamId: props.project.teamId,
project: props.project,
});
}}
showNebulaSettings={props.showNebulaSettings}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";
import Link from "next/link";
import { useMemo, useState } from "react";
import { useMemo } from "react";
import type { ThirdwebClient } from "thirdweb";
import type { Project } from "@/api/projects";
import { type Step, StepsCard } from "@/components/blocks/StepsCard";
Expand All @@ -9,7 +9,6 @@ import { CreateVaultAccountButton } from "../../vault/components/create-vault-ac
import CreateServerWallet from "../server-wallets/components/create-server-wallet.client";
import type { Wallet } from "../server-wallets/wallet-table/types";
import { SendTestTransaction } from "./send-test-tx.client";
import { deleteUserAccessToken } from "./utils";

interface Props {
managementAccessToken: string | undefined;
Expand All @@ -19,27 +18,12 @@ interface Props {
teamSlug: string;
testTxWithWallet?: string | undefined;
client: ThirdwebClient;
isManagedVault: boolean;
}

export const EngineChecklist: React.FC<Props> = (props) => {
const [userAccessToken, setUserAccessToken] = useState<string | undefined>();

const finalSteps = useMemo(() => {
const steps: Step[] = [];
steps.push({
children: (
<CreateVaultAccountStep
onUserAccessTokenCreated={(token) => setUserAccessToken(token)}
project={props.project}
teamSlug={props.teamSlug}
/>
),
completed: !!props.managementAccessToken,
description:
"Vault is thirdweb's key management system. It allows you to create secure server wallets and manage access tokens.",
showCompletedChildren: false,
title: "Create a Vault Admin Account",
});
steps.push({
children: (
<CreateServerWalletStep
Expand All @@ -48,7 +32,7 @@ export const EngineChecklist: React.FC<Props> = (props) => {
teamSlug={props.teamSlug}
/>
),
completed: props.wallets.length > 0,
completed: props.wallets.length > 0 || props.hasTransactions,
description:
"Server wallets are smart wallets, they don't require any gas funds to send transactions.",
showCompletedChildren: false,
Expand All @@ -58,10 +42,10 @@ export const EngineChecklist: React.FC<Props> = (props) => {
steps.push({
children: (
<SendTestTransaction
isManagedVault={props.isManagedVault}
client={props.client}
project={props.project}
teamSlug={props.teamSlug}
userAccessToken={userAccessToken}
wallets={props.wallets}
/>
),
Expand All @@ -78,9 +62,9 @@ export const EngineChecklist: React.FC<Props> = (props) => {
props.project,
props.wallets,
props.hasTransactions,
userAccessToken,
props.teamSlug,
props.client,
props.isManagedVault,
]);

const isComplete = useMemo(
Expand All @@ -91,19 +75,17 @@ export const EngineChecklist: React.FC<Props> = (props) => {
if (props.testTxWithWallet) {
return (
<SendTestTransaction
isManagedVault={props.isManagedVault}
client={props.client}
project={props.project}
teamSlug={props.teamSlug}
userAccessToken={userAccessToken}
walletId={props.testTxWithWallet}
wallets={props.wallets}
/>
);
}

if (finalSteps.length === 0 || isComplete) {
// clear token from local storage after FTUX is complete
deleteUserAccessToken(props.project.id);
return null;
}
return (
Expand All @@ -122,10 +104,7 @@ function CreateVaultAccountStep(props: {
}) {
return (
<div className="mt-4 flex flex-row gap-4">
<CreateVaultAccountButton
onUserAccessTokenCreated={props.onUserAccessTokenCreated}
project={props.project}
/>
<CreateVaultAccountButton project={props.project} />
<Button asChild variant="outline">
<Link
href="https://portal.thirdweb.com/vault"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { engineCloudProxy } from "@/actions/proxies";
import type { Project } from "@/api/projects";
import { SingleNetworkSelector } from "@/components/blocks/NetworkSelectors";
import { Button } from "@/components/ui/button";
import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { Input } from "@/components/ui/input";
import {
Select,
Expand All @@ -24,10 +23,9 @@ import { useAllChainsData } from "@/hooks/chains/allChains";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import type { Wallet } from "../server-wallets/wallet-table/types";
import { SmartAccountCell } from "../server-wallets/wallet-table/wallet-table-ui.client";
import { deleteUserAccessToken, getUserAccessToken } from "./utils";

const formSchema = z.object({
accessToken: z.string().min(1, "Access token is required"),
secretKey: z.string().min(1, "Secret key is required"),
chainId: z.number(),
walletIndex: z.string(),
});
Expand All @@ -38,9 +36,9 @@ export function SendTestTransaction(props: {
wallets?: Wallet[];
project: Project;
teamSlug: string;
userAccessToken?: string;
expanded?: boolean;
walletId?: string;
isManagedVault: boolean;
client: ThirdwebClient;
}) {
const queryClient = useQueryClient();
Expand All @@ -49,12 +47,9 @@ export function SendTestTransaction(props: {

const chainsQuery = useAllChainsData();

const userAccessToken =
props.userAccessToken ?? getUserAccessToken(props.project.id) ?? "";

const form = useForm<FormValues>({
defaultValues: {
accessToken: userAccessToken,
secretKey: "",
chainId: 84532,
walletIndex:
props.wallets && props.walletId
Expand All @@ -73,7 +68,7 @@ export function SendTestTransaction(props: {
const sendDummyTxMutation = useMutation({
mutationFn: async (args: {
walletAddress: string;
accessToken: string;
secretKey: string;
chainId: number;
}) => {
const response = await engineCloudProxy({
Expand All @@ -93,7 +88,9 @@ export function SendTestTransaction(props: {
"Content-Type": "application/json",
"x-client-id": props.project.publishableKey,
"x-team-id": props.project.teamId,
"x-vault-access-token": args.accessToken,
...(props.isManagedVault
? { "x-secret-key": args.secretKey }
: { "x-vault-access-token": args.secretKey }),
},
method: "POST",
pathname: "/v1/write/transaction",
Expand Down Expand Up @@ -123,7 +120,7 @@ export function SendTestTransaction(props: {

const onSubmit = async (data: FormValues) => {
await sendDummyTxMutation.mutateAsync({
accessToken: data.accessToken,
secretKey: data.secretKey,
chainId: data.chainId,
walletAddress: selectedWallet.address,
});
Expand All @@ -141,44 +138,39 @@ export function SendTestTransaction(props: {
)}
<p className="flex items-center gap-2 text-sm text-warning-text">
<LockIcon className="h-4 w-4" />
{userAccessToken
? "Copy your Vault access token, you'll need it for every HTTP call to Engine."
: "Every wallet action requires your Vault access token."}
Every server wallet action requires your{" "}
{props.isManagedVault ? "project secret key" : "vault access token"}.
</p>
<div className="h-4" />
{/* Responsive container */}
<div className="flex flex-col gap-2 md:flex-row md:items-end md:gap-2">
<div className="flex-grow">
<div className="flex flex-col gap-2">
<p className="text-sm">Vault Access Token</p>
{userAccessToken ? (
<div className="flex flex-col gap-2 ">
<CopyTextButton
className="!h-auto w-full justify-between bg-background px-3 py-3 font-mono text-xs"
copyIconPosition="right"
textToCopy={userAccessToken}
textToShow={userAccessToken}
tooltip="Copy Vault Access Token"
/>
<p className="text-muted-foreground text-xs">
This is a project-wide access token to access your server
wallets. You can create more access tokens using your admin
key, with granular scopes and permissions.
</p>
</div>
) : (
<Input
placeholder="vt_act_1234....ABCD"
type={userAccessToken ? "text" : "password"}
{...form.register("accessToken")}
className="text-xs"
disabled={isLoading}
/>
)}
<p className="text-sm">
{props.isManagedVault
? "Project Secret Key"
: "Vault Access Token"}
</p>
<Input
placeholder={
props.isManagedVault
? "Enter your project secret key"
: "Enter your vault access token"
}
type={"password"}
{...form.register("secretKey")}
className="text-xs"
disabled={isLoading}
/>
<p className="text-muted-foreground text-xs">
{props.isManagedVault
? "Your project secret key was generated when you created your project. If you lost it, you can regenerate one in the project settings."
: "Your vault access token was generated when you created your vault. If you lost it, you can regenerate one in the vault settings."}
</p>
</div>
</div>
</div>
<div className="h-4" />
<div className="h-6" />
{/* Wallet Selector */}
<div className="flex flex-col gap-2">
<div className="flex flex-col gap-2 md:flex-row md:items-end md:gap-2">
Expand Down Expand Up @@ -268,8 +260,6 @@ export function SendTestTransaction(props: {
} else {
router.refresh();
}
// clear token from local storage after FTUX is complete
deleteUserAccessToken(props.project.id);
}}
variant="primary"
>
Expand Down
Loading
Loading