From 0207d13855e1de21a7f812416764a3af0f293d18 Mon Sep 17 00:00:00 2001 From: Marc Kelechava Date: Wed, 10 Dec 2025 08:56:12 -0800 Subject: [PATCH] [Frontend] Add SECRET credential type for storing generic sensitive values (#4258) --- skyvern-frontend/src/api/types.ts | 39 +++++- .../src/routes/credentials/CredentialItem.tsx | 112 +++++++++++------- .../routes/credentials/CredentialsList.tsx | 3 +- .../routes/credentials/CredentialsModal.tsx | 77 ++++++++++-- .../routes/credentials/CredentialsPage.tsx | 26 +++- .../credentials/SecretCredentialContent.tsx | 82 +++++++++++++ .../credentials/useCredentialModalState.ts | 1 + .../components/CredentialSelector.tsx | 4 +- .../nodes/HttpRequestNode/HttpRequestNode.tsx | 5 +- 9 files changed, 283 insertions(+), 66 deletions(-) create mode 100644 skyvern-frontend/src/routes/credentials/SecretCredentialContent.tsx diff --git a/skyvern-frontend/src/api/types.ts b/skyvern-frontend/src/api/types.ts index f91a139a..b7f08654 100644 --- a/skyvern-frontend/src/api/types.ts +++ b/skyvern-frontend/src/api/types.ts @@ -500,29 +500,51 @@ export type CreditCardCredentialApiResponse = { brand: string; }; +export type SecretCredentialResponse = { + secret_label?: string | null; +}; + export type CredentialApiResponse = { credential_id: string; - credential: PasswordCredentialApiResponse | CreditCardCredentialApiResponse; - credential_type: "password" | "credit_card"; + credential: + | PasswordCredentialApiResponse + | CreditCardCredentialApiResponse + | SecretCredentialResponse; + credential_type: "password" | "credit_card" | "secret"; name: string; }; export function isPasswordCredential( - credential: PasswordCredentialApiResponse | CreditCardCredentialApiResponse, + credential: + | PasswordCredentialApiResponse + | CreditCardCredentialApiResponse + | SecretCredentialResponse, ): credential is PasswordCredentialApiResponse { return "username" in credential; } export function isCreditCardCredential( - credential: PasswordCredentialApiResponse | CreditCardCredentialApiResponse, + credential: + | PasswordCredentialApiResponse + | CreditCardCredentialApiResponse + | SecretCredentialResponse, ): credential is CreditCardCredentialApiResponse { return "last_four" in credential; } +export function isSecretCredential( + credential: + | PasswordCredentialApiResponse + | CreditCardCredentialApiResponse + | SecretCredentialResponse, +): credential is SecretCredentialResponse { + return !("username" in credential) && !("last_four" in credential); +} + export type CreateCredentialRequest = { name: string; - credential_type: "password" | "credit_card"; - credential: PasswordCredential | CreditCardCredential; + credential_type: "password" | "credit_card" | "secret"; + credential: PasswordCredential | CreditCardCredential | SecretCredential; }; export type PasswordCredential = { @@ -542,6 +564,11 @@ export type CreditCardCredential = { card_holder_name: string; }; +export type SecretCredential = { + secret_value: string; + secret_label?: string | null; +}; + export const OtpType = { Totp: "totp", MagicLink: "magic_link", diff --git a/skyvern-frontend/src/routes/credentials/CredentialItem.tsx b/skyvern-frontend/src/routes/credentials/CredentialItem.tsx index d1314cb3..2857cbea 100644 --- a/skyvern-frontend/src/routes/credentials/CredentialItem.tsx +++ b/skyvern-frontend/src/routes/credentials/CredentialItem.tsx @@ -1,12 +1,17 @@ -import { isPasswordCredential } from "@/api/types"; +import { + CredentialApiResponse, + isCreditCardCredential, + isPasswordCredential, + isSecretCredential, +} from "@/api/types"; import { DeleteCredentialButton } from "./DeleteCredentialButton"; -import { CredentialApiResponse } from "@/api/types"; type Props = { credential: CredentialApiResponse; }; function CredentialItem({ credential }: Props) { + const credentialData = credential.credential; const getTotpTypeDisplay = (totpType: string) => { switch (totpType) { case "authenticator": @@ -21,6 +26,69 @@ function CredentialItem({ credential }: Props) { } }; + let credentialDetails = null; + + if (isPasswordCredential(credentialData)) { + credentialDetails = ( +
+
+
+

Username/Email

+

Password

+ {credentialData.totp_type !== "none" && ( +

2FA Type

+ )} +
+
+

{credentialData.username}

+

{"********"}

+ {credentialData.totp_type !== "none" && ( +

+ {getTotpTypeDisplay(credentialData.totp_type)} +

+ )} +
+
+
+ ); + } else if (isCreditCardCredential(credentialData)) { + credentialDetails = ( +
+
+
+

Card Number

+

Brand

+
+
+
+
+

+ {"************" + credentialData.last_four} +

+

{credentialData.brand}

+
+
+
+ ); + } else if (isSecretCredential(credentialData)) { + credentialDetails = ( +
+
+

Secret Value

+ {credentialData.secret_label ? ( +

Type

+ ) : null} +
+
+

{"************"}

+ {credentialData.secret_label ? ( +

{credentialData.secret_label}

+ ) : null} +
+
+ ); + } + return (
@@ -29,45 +97,7 @@ function CredentialItem({ credential }: Props) {

{credential.credential_id}

- {isPasswordCredential(credential.credential) ? ( -
-
-
-

Username/Email

-

Password

- {credential.credential.totp_type !== "none" && ( -

2FA Type

- )} -
-
-

{credential.credential.username}

-

{"********"}

- {credential.credential.totp_type !== "none" && ( -

- {getTotpTypeDisplay(credential.credential.totp_type)} -

- )} -
-
-
- ) : ( -
-
-
-

Card Number

-

Brand

-
-
-
-
-

- {"************" + credential.credential.last_four} -

-

{credential.credential.brand}

-
-
-
- )} + {credentialDetails}
diff --git a/skyvern-frontend/src/routes/credentials/CredentialsList.tsx b/skyvern-frontend/src/routes/credentials/CredentialsList.tsx index ad9be3f6..2dd5fb68 100644 --- a/skyvern-frontend/src/routes/credentials/CredentialsList.tsx +++ b/skyvern-frontend/src/routes/credentials/CredentialsList.tsx @@ -2,7 +2,7 @@ import { Skeleton } from "@/components/ui/skeleton"; import { CredentialItem } from "./CredentialItem"; import { useCredentialsQuery } from "@/routes/workflows/hooks/useCredentialsQuery"; -type CredentialFilter = "password" | "credit_card"; +type CredentialFilter = "password" | "credit_card" | "secret"; type Props = { filter?: CredentialFilter; @@ -11,6 +11,7 @@ type Props = { const EMPTY_MESSAGE: Record = { password: "No password credentials stored yet.", credit_card: "No credit cards stored yet.", + secret: "No secrets stored yet.", }; function CredentialsList({ filter }: Props = {}) { diff --git a/skyvern-frontend/src/routes/credentials/CredentialsModal.tsx b/skyvern-frontend/src/routes/credentials/CredentialsModal.tsx index e75c14ed..7dea5ae5 100644 --- a/skyvern-frontend/src/routes/credentials/CredentialsModal.tsx +++ b/skyvern-frontend/src/routes/credentials/CredentialsModal.tsx @@ -10,6 +10,7 @@ import { CredentialModalTypes, } from "./useCredentialModalState"; import { PasswordCredentialContent } from "./PasswordCredentialContent"; +import { SecretCredentialContent } from "./SecretCredentialContent"; import { useState, useEffect } from "react"; import { Button } from "@/components/ui/button"; import { CreditCardCredentialContent } from "./CreditCardCredentialContent"; @@ -40,6 +41,12 @@ const CREDIT_CARD_CREDENTIAL_INITIAL_VALUES = { cardHolderName: "", }; +const SECRET_CREDENTIAL_INITIAL_VALUES = { + name: "", + secretLabel: "", + secretValue: "", +}; + // Function to generate a unique credential name function generateDefaultCredentialName(existingNames: string[]): string { const baseName = "credentials"; @@ -73,6 +80,9 @@ function CredentialsModal({ onCredentialCreated }: Props) { const [creditCardCredentialValues, setCreditCardCredentialValues] = useState( CREDIT_CARD_CREDENTIAL_INITIAL_VALUES, ); + const [secretCredentialValues, setSecretCredentialValues] = useState( + SECRET_CREDENTIAL_INITIAL_VALUES, + ); // Set default name when modal opens useEffect(() => { @@ -88,12 +98,17 @@ function CredentialsModal({ onCredentialCreated }: Props) { ...prev, name: defaultName, })); + setSecretCredentialValues((prev) => ({ + ...prev, + name: defaultName, + })); } }, [isOpen, credentials]); function reset() { setPasswordCredentialValues(PASSWORD_CREDENTIAL_INITIAL_VALUES); setCreditCardCredentialValues(CREDIT_CARD_CREDENTIAL_INITIAL_VALUES); + setSecretCredentialValues(SECRET_CREDENTIAL_INITIAL_VALUES); } const createCredentialMutation = useMutation({ @@ -128,7 +143,9 @@ function CredentialsModal({ onCredentialCreated }: Props) { const name = type === CredentialModalTypes.PASSWORD ? passwordCredentialValues.name.trim() - : creditCardCredentialValues.name.trim(); + : type === CredentialModalTypes.CREDIT_CARD + ? creditCardCredentialValues.name.trim() + : secretCredentialValues.name.trim(); if (name === "") { toast({ title: "Error", @@ -219,9 +236,55 @@ function CredentialsModal({ onCredentialCreated }: Props) { card_holder_name: cardHolderName, }, }); + } else if (type === CredentialModalTypes.SECRET) { + const secretValue = secretCredentialValues.secretValue.trim(); + const secretLabel = secretCredentialValues.secretLabel.trim(); + + if (secretValue === "") { + toast({ + title: "Error", + description: "Secret value is required", + variant: "destructive", + }); + return; + } + + createCredentialMutation.mutate({ + name, + credential_type: "secret", + credential: { + secret_value: secretValue, + secret_label: secretLabel === "" ? null : secretLabel, + }, + }); } }; + const credentialContent = (() => { + if (type === CredentialModalTypes.PASSWORD) { + return ( + + ); + } + if (type === CredentialModalTypes.CREDIT_CARD) { + return ( + + ); + } + return ( + + ); + })(); + return ( Add Credential - {type === CredentialModalTypes.PASSWORD ? ( - - ) : ( - - )} + {credentialContent}
@@ -90,6 +105,7 @@ function CredentialsPage() { Passwords Credit Cards + Secrets 2FA @@ -101,6 +117,10 @@ function CredentialsPage() { + + + + diff --git a/skyvern-frontend/src/routes/credentials/SecretCredentialContent.tsx b/skyvern-frontend/src/routes/credentials/SecretCredentialContent.tsx new file mode 100644 index 00000000..b5e1bbaa --- /dev/null +++ b/skyvern-frontend/src/routes/credentials/SecretCredentialContent.tsx @@ -0,0 +1,82 @@ +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Separator } from "@/components/ui/separator"; +import { EyeNoneIcon, EyeOpenIcon } from "@radix-ui/react-icons"; +import { useState } from "react"; + +type Props = { + values: { + name: string; + secretLabel: string; + secretValue: string; + }; + onChange: (values: { + name: string; + secretLabel: string; + secretValue: string; + }) => void; +}; + +function SecretCredentialContent({ values, onChange }: Props) { + const { name, secretLabel, secretValue } = values; + const [showSecret, setShowSecret] = useState(false); + + return ( +
+
+
+ +
+ The name of the credential +
+
+ onChange({ ...values, name: e.target.value })} + /> +
+ +
+ + onChange({ ...values, secretLabel: e.target.value })} + /> +
+
+ +
+ + onChange({ ...values, secretValue: e.target.value }) + } + /> +
{ + setShowSecret((value) => !value); + }} + aria-label="Toggle secret value visibility" + > + {showSecret ? ( + + ) : ( + + )} +
+
+

+ { + "Use in HTTP Request blocks with: {{ credential_name.secret_value }}" + } +

+
+
+ ); +} + +export { SecretCredentialContent }; diff --git a/skyvern-frontend/src/routes/credentials/useCredentialModalState.ts b/skyvern-frontend/src/routes/credentials/useCredentialModalState.ts index ab5eb4f3..be336d85 100644 --- a/skyvern-frontend/src/routes/credentials/useCredentialModalState.ts +++ b/skyvern-frontend/src/routes/credentials/useCredentialModalState.ts @@ -6,6 +6,7 @@ const typeParam = "type"; export const CredentialModalTypes = { PASSWORD: "password", CREDIT_CARD: "credit-card", + SECRET: "secret", } as const; export type CredentialModalType = diff --git a/skyvern-frontend/src/routes/workflows/components/CredentialSelector.tsx b/skyvern-frontend/src/routes/workflows/components/CredentialSelector.tsx index e526466f..d3516662 100644 --- a/skyvern-frontend/src/routes/workflows/components/CredentialSelector.tsx +++ b/skyvern-frontend/src/routes/workflows/components/CredentialSelector.tsx @@ -62,7 +62,9 @@ function CredentialSelector({ value, onChange }: Props) {

{credential.credential_type === "password" ? "Password" - : "Credit Card"} + : credential.credential_type === "credit_card" + ? "Credit Card" + : "Secret"}

diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/HttpRequestNode/HttpRequestNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/HttpRequestNode/HttpRequestNode.tsx index 963156da..c22fe91b 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/HttpRequestNode/HttpRequestNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/HttpRequestNode/HttpRequestNode.tsx @@ -399,9 +399,10 @@ function HttpRequestNode({ id, data }: NodeProps) { authentication and content headers
  • - Pass a credential/secret parameter and reference it in headers - or body with {"{{ my_credential.password }}"} + Password credential: {"{{ my_credential.username }}"} /{" "} + {"{{ my_credential.password }}"}
  • +
  • Secret credential: {"{{ my_secret.secret_value }}"}
  • The request will return response data including status, headers, and body