mirror of
https://github.com/Skyvern-AI/skyvern.git
synced 2026-04-29 20:20:22 +00:00
359 lines
13 KiB
Python
359 lines
13 KiB
Python
from typing import Any
|
|
|
|
import structlog
|
|
|
|
from skyvern.exceptions import HttpException
|
|
from skyvern.forge.sdk.core.aiohttp_helper import aiohttp_request
|
|
from skyvern.forge.sdk.schemas.credentials import (
|
|
CredentialItem,
|
|
CredentialType,
|
|
CreditCardCredential,
|
|
PasswordCredential,
|
|
SecretCredential,
|
|
)
|
|
|
|
LOG = structlog.get_logger()
|
|
|
|
|
|
class CustomCredentialAPIClient:
|
|
"""HTTP client for interacting with custom credential service APIs."""
|
|
|
|
def __init__(self, api_base_url: str, api_token: str):
|
|
"""
|
|
Initialize the custom credential API client.
|
|
|
|
Args:
|
|
api_base_url: Base URL for the custom credential API
|
|
api_token: Bearer token for authentication
|
|
"""
|
|
self.api_base_url = api_base_url.rstrip("/")
|
|
self.api_token = api_token
|
|
|
|
def _get_auth_headers(self) -> dict[str, str]:
|
|
"""Get headers for API authentication."""
|
|
return {
|
|
"Authorization": f"Bearer {self.api_token}",
|
|
"Content-Type": "application/json",
|
|
}
|
|
|
|
def _credential_to_api_payload(
|
|
self, credential: PasswordCredential | CreditCardCredential | SecretCredential
|
|
) -> dict[str, Any]:
|
|
"""Convert Skyvern credential to API payload format."""
|
|
if isinstance(credential, PasswordCredential):
|
|
return {
|
|
"type": "password",
|
|
"username": credential.username,
|
|
"password": credential.password,
|
|
"totp": credential.totp,
|
|
"totp_type": credential.totp_type,
|
|
}
|
|
elif isinstance(credential, CreditCardCredential):
|
|
return {
|
|
"type": "credit_card",
|
|
"card_holder_name": credential.card_holder_name,
|
|
"card_number": credential.card_number,
|
|
"card_exp_month": credential.card_exp_month,
|
|
"card_exp_year": credential.card_exp_year,
|
|
"card_cvv": credential.card_cvv,
|
|
"card_brand": credential.card_brand,
|
|
}
|
|
elif isinstance(credential, SecretCredential):
|
|
payload = {
|
|
"type": "secret",
|
|
"secret_value": credential.secret_value,
|
|
}
|
|
if credential.secret_label is not None:
|
|
payload["secret_label"] = credential.secret_label
|
|
return payload
|
|
else:
|
|
raise TypeError(f"Unsupported credential type: {type(credential)}")
|
|
|
|
@staticmethod
|
|
def _format_error_response(response_body: Any) -> str:
|
|
"""Extract a human-readable error message from an HTTP error response body."""
|
|
if isinstance(response_body, dict):
|
|
for key in ["message", "error", "detail", "error_message"]:
|
|
if key in response_body and response_body[key]:
|
|
return str(response_body[key])
|
|
return str(response_body)
|
|
if isinstance(response_body, str) and response_body.strip():
|
|
return response_body.strip()
|
|
return ""
|
|
|
|
def _api_response_to_credential(self, credential_data: dict[str, Any], name: str, item_id: str) -> CredentialItem:
|
|
"""Convert API response to Skyvern CredentialItem."""
|
|
credential_type = credential_data.get("type")
|
|
|
|
if credential_type == "password":
|
|
required_fields = ["username", "password"]
|
|
missing = [f for f in required_fields if f not in credential_data]
|
|
if missing:
|
|
raise ValueError(f"Missing required password fields from API: {missing}")
|
|
|
|
credential = PasswordCredential(
|
|
username=credential_data["username"],
|
|
password=credential_data["password"],
|
|
totp=credential_data.get("totp"),
|
|
totp_type=credential_data.get("totp_type", "none"),
|
|
)
|
|
return CredentialItem(
|
|
item_id=item_id,
|
|
credential=credential,
|
|
name=name,
|
|
credential_type=CredentialType.PASSWORD,
|
|
)
|
|
elif credential_type == "credit_card":
|
|
required_fields = [
|
|
"card_holder_name",
|
|
"card_number",
|
|
"card_exp_month",
|
|
"card_exp_year",
|
|
"card_cvv",
|
|
"card_brand",
|
|
]
|
|
missing = [f for f in required_fields if f not in credential_data]
|
|
if missing:
|
|
raise ValueError(f"Missing required credit card fields from API: {missing}")
|
|
|
|
credential = CreditCardCredential(
|
|
card_holder_name=credential_data["card_holder_name"],
|
|
card_number=credential_data["card_number"],
|
|
card_exp_month=credential_data["card_exp_month"],
|
|
card_exp_year=credential_data["card_exp_year"],
|
|
card_cvv=credential_data["card_cvv"],
|
|
card_brand=credential_data["card_brand"],
|
|
)
|
|
return CredentialItem(
|
|
item_id=item_id,
|
|
credential=credential,
|
|
name=name,
|
|
credential_type=CredentialType.CREDIT_CARD,
|
|
)
|
|
elif credential_type == "secret":
|
|
required_fields = ["secret_value"]
|
|
missing = [f for f in required_fields if f not in credential_data]
|
|
if missing:
|
|
raise ValueError(f"Missing required secret fields from API: {missing}")
|
|
|
|
credential = SecretCredential(
|
|
secret_value=credential_data["secret_value"],
|
|
secret_label=credential_data.get("secret_label"),
|
|
)
|
|
return CredentialItem(
|
|
item_id=item_id,
|
|
credential=credential,
|
|
name=name,
|
|
credential_type=CredentialType.SECRET,
|
|
)
|
|
else:
|
|
raise ValueError(f"Unsupported credential type from API: {credential_type}")
|
|
|
|
async def create_credential(
|
|
self, name: str, credential: PasswordCredential | CreditCardCredential | SecretCredential
|
|
) -> str:
|
|
"""
|
|
Create a credential using the custom API.
|
|
|
|
Args:
|
|
name: Name of the credential
|
|
credential: Credential data to store
|
|
|
|
Returns:
|
|
The credential ID returned by the API
|
|
|
|
Raises:
|
|
HttpException: If the API request fails
|
|
"""
|
|
url = f"{self.api_base_url}"
|
|
headers = self._get_auth_headers()
|
|
|
|
payload = {
|
|
"name": name,
|
|
**self._credential_to_api_payload(credential),
|
|
}
|
|
|
|
LOG.info(
|
|
"Creating credential via custom API",
|
|
url=url,
|
|
name=name,
|
|
credential_type=type(credential).__name__,
|
|
)
|
|
|
|
try:
|
|
status_code, _, response_body = await aiohttp_request(
|
|
method="POST",
|
|
url=url,
|
|
headers=headers,
|
|
data=payload,
|
|
)
|
|
|
|
if status_code < 200 or status_code >= 300:
|
|
error_detail = self._format_error_response(response_body)
|
|
msg = f"HTTP {status_code}"
|
|
if error_detail:
|
|
msg += f": {error_detail}"
|
|
LOG.error(
|
|
"Custom credential API returned error",
|
|
url=url,
|
|
status_code=status_code,
|
|
response_body=response_body,
|
|
)
|
|
raise HttpException(status_code, url, msg)
|
|
|
|
if not response_body or not isinstance(response_body, dict):
|
|
raise HttpException(500, url, "Empty or invalid response from custom credential API")
|
|
|
|
# Extract credential ID from response
|
|
credential_id = response_body.get("id")
|
|
if not credential_id:
|
|
LOG.error(
|
|
"Custom credential API response missing id field",
|
|
url=url,
|
|
response=response_body,
|
|
)
|
|
raise HttpException(500, url, "Invalid response format from custom credential API: missing 'id' field")
|
|
|
|
LOG.info(
|
|
"Successfully created credential via custom API",
|
|
url=url,
|
|
name=name,
|
|
credential_id=credential_id,
|
|
)
|
|
|
|
return str(credential_id)
|
|
|
|
except HttpException:
|
|
raise
|
|
except Exception as e:
|
|
LOG.error(
|
|
"Failed to create credential via custom API",
|
|
url=url,
|
|
name=name,
|
|
error=str(e),
|
|
exc_info=True,
|
|
)
|
|
raise HttpException(500, url, f"Failed to create credential via custom API: {e!s}") from e
|
|
|
|
async def get_credential(self, credential_id: str, name: str) -> CredentialItem:
|
|
"""
|
|
Get a credential using the custom API.
|
|
|
|
Args:
|
|
credential_id: ID of the credential to retrieve
|
|
name: Name of the credential (for constructing CredentialItem)
|
|
|
|
Returns:
|
|
The credential data
|
|
|
|
Raises:
|
|
HttpException: If the API request fails
|
|
"""
|
|
url = f"{self.api_base_url}/{credential_id}"
|
|
headers = self._get_auth_headers()
|
|
|
|
LOG.info(
|
|
"Retrieving credential via custom API",
|
|
url=url,
|
|
credential_id=credential_id,
|
|
)
|
|
|
|
try:
|
|
status_code, _, response_body = await aiohttp_request(
|
|
method="GET",
|
|
url=url,
|
|
headers=headers,
|
|
)
|
|
|
|
if status_code < 200 or status_code >= 300:
|
|
error_detail = self._format_error_response(response_body)
|
|
msg = f"HTTP {status_code}"
|
|
if error_detail:
|
|
msg += f": {error_detail}"
|
|
LOG.error(
|
|
"Custom credential API returned error",
|
|
url=url,
|
|
status_code=status_code,
|
|
response_body=response_body,
|
|
)
|
|
raise HttpException(status_code, url, msg)
|
|
|
|
if not response_body or not isinstance(response_body, dict):
|
|
raise HttpException(404, url, f"Credential not found: {credential_id}")
|
|
|
|
LOG.info(
|
|
"Successfully retrieved credential via custom API",
|
|
url=url,
|
|
credential_id=credential_id,
|
|
)
|
|
|
|
return self._api_response_to_credential(response_body, name, credential_id)
|
|
|
|
except HttpException:
|
|
raise
|
|
except Exception as e:
|
|
LOG.error(
|
|
"Failed to retrieve credential via custom API",
|
|
url=url,
|
|
credential_id=credential_id,
|
|
error=str(e),
|
|
exc_info=True,
|
|
)
|
|
raise HttpException(500, url, f"Failed to retrieve credential via custom API: {e!s}") from e
|
|
|
|
async def delete_credential(self, credential_id: str) -> None:
|
|
"""
|
|
Delete a credential using the custom API.
|
|
|
|
Args:
|
|
credential_id: ID of the credential to delete
|
|
|
|
Raises:
|
|
HttpException: If the API request fails
|
|
"""
|
|
url = f"{self.api_base_url}/{credential_id}"
|
|
headers = self._get_auth_headers()
|
|
|
|
LOG.info(
|
|
"Deleting credential via custom API",
|
|
url=url,
|
|
credential_id=credential_id,
|
|
)
|
|
|
|
try:
|
|
status_code, _, response_body = await aiohttp_request(
|
|
method="DELETE",
|
|
url=url,
|
|
headers=headers,
|
|
)
|
|
|
|
if status_code < 200 or status_code >= 300:
|
|
error_detail = self._format_error_response(response_body)
|
|
msg = f"HTTP {status_code}"
|
|
if error_detail:
|
|
msg += f": {error_detail}"
|
|
LOG.error(
|
|
"Custom credential API returned error on delete",
|
|
url=url,
|
|
status_code=status_code,
|
|
response_body=response_body,
|
|
)
|
|
raise HttpException(status_code, url, msg)
|
|
|
|
LOG.info(
|
|
"Successfully deleted credential via custom API",
|
|
url=url,
|
|
credential_id=credential_id,
|
|
)
|
|
|
|
except HttpException:
|
|
raise
|
|
except Exception as e:
|
|
LOG.error(
|
|
"Failed to delete credential via custom API",
|
|
url=url,
|
|
credential_id=credential_id,
|
|
error=str(e),
|
|
exc_info=True,
|
|
)
|
|
raise HttpException(500, url, f"Failed to delete credential via custom API: {e!s}") from e
|