Compare commits

...

2 Commits

Author SHA1 Message Date
alejandrobailo
5267ca0710 feat(ui): add Vercel provider UI and documentation
- Add Vercel provider badge, credentials form, and wizard integration
- Add Vercel to provider type selectors, radio groups, and icon mappings
- Add Vercel credential fields, build logic, and Zod form schemas
- Fix provider wizard validation on back navigation (superRefine → refine)
- Add Vercel docs link to provider wizard help text
- Add Vercel getting started and authentication documentation pages
- Register Vercel docs in navigation config
2026-02-27 11:54:14 +01:00
alejandrobailo
4918a9b018 feat(api): integrate Vercel provider into API layer
- Add VERCEL to ProviderChoices enum with validate_vercel_uid validator
- Add PostgreSQL enum migration with RunSQL for forward/reverse
- Add Vercel branch to connection test, provider kwargs, and type hints in utils
- Add VercelProviderSecret serializer for API token + team credentials
2026-02-27 11:53:24 +01:00
24 changed files with 508 additions and 40 deletions

View File

@@ -0,0 +1,49 @@
# Generated by Django 5.1.15 on 2026-02-26 22:11
import django.contrib.postgres.indexes
from django.db import migrations
import api.db_utils
class Migration(migrations.Migration):
dependencies = [
("api", "0083_image_provider"),
]
operations = [
migrations.RunSQL(
"ALTER TYPE provider ADD VALUE IF NOT EXISTS 'vercel';",
reverse_sql=migrations.RunSQL.noop,
),
migrations.AlterField(
model_name="provider",
name="provider",
field=api.db_utils.ProviderEnumField(
choices=[
("aws", "AWS"),
("azure", "Azure"),
("gcp", "GCP"),
("kubernetes", "Kubernetes"),
("m365", "M365"),
("github", "GitHub"),
("mongodbatlas", "MongoDB Atlas"),
("iac", "IaC"),
("oraclecloud", "Oracle Cloud Infrastructure"),
("alibabacloud", "Alibaba Cloud"),
("cloudflare", "Cloudflare"),
("openstack", "OpenStack"),
("vercel", "Vercel"),
("image", "Image"),
],
default="aws",
),
),
migrations.AddIndex(
model_name="resource",
index=django.contrib.postgres.indexes.GinIndex(
fields=["text_search"], name="gin_resources_search_idx"
),
),
]

View File

@@ -292,6 +292,7 @@ class Provider(RowLevelSecurityProtectedModel):
ALIBABACLOUD = "alibabacloud", _("Alibaba Cloud")
CLOUDFLARE = "cloudflare", _("Cloudflare")
OPENSTACK = "openstack", _("OpenStack")
VERCEL = "vercel", _("Vercel")
IMAGE = "image", _("Image")
@staticmethod
@@ -427,6 +428,15 @@ class Provider(RowLevelSecurityProtectedModel):
pointer="/data/attributes/uid",
)
@staticmethod
def validate_vercel_uid(value):
if not re.match(r"^team_[a-zA-Z0-9]{16,32}$", value):
raise ModelValidationError(
detail="Vercel provider ID must be a valid Vercel Team ID (e.g., team_xxxxxxxxxxxxxxxxxxxxxxxx).",
code="vercel-uid",
pointer="/data/attributes/uid",
)
@staticmethod
def validate_image_uid(value):
if not re.match(r"^[a-zA-Z0-9][a-zA-Z0-9._/:@-]{2,249}$", value):

View File

@@ -36,6 +36,7 @@ if TYPE_CHECKING:
)
from prowler.providers.openstack.openstack_provider import OpenstackProvider
from prowler.providers.oraclecloud.oraclecloud_provider import OraclecloudProvider
from prowler.providers.vercel.vercel_provider import VercelProvider
class CustomOAuth2Client(OAuth2Client):
@@ -90,6 +91,7 @@ def return_prowler_provider(
| MongodbatlasProvider
| OpenstackProvider
| OraclecloudProvider
| VercelProvider
):
"""Return the Prowler provider class based on the given provider type.
@@ -165,6 +167,10 @@ def return_prowler_provider(
from prowler.providers.image.image_provider import ImageProvider
prowler_provider = ImageProvider
case Provider.ProviderChoices.VERCEL.value:
from prowler.providers.vercel.vercel_provider import VercelProvider
prowler_provider = VercelProvider
case _:
raise ValueError(f"Provider type {provider.provider} not supported")
return prowler_provider
@@ -225,6 +231,11 @@ def get_prowler_provider_kwargs(
# clouds_yaml_content, clouds_yaml_cloud and provider_id are validated
# in the provider itself, so it's not needed here.
pass
elif provider.provider == Provider.ProviderChoices.VERCEL.value:
prowler_provider_kwargs = {
**prowler_provider_kwargs,
"team_id": provider.uid,
}
elif provider.provider == Provider.ProviderChoices.IMAGE.value:
# Detect whether uid is a registry URL (e.g. "docker.io/andoniaf") or
# a concrete image reference (e.g. "docker.io/andoniaf/myimage:latest").
@@ -270,6 +281,7 @@ def initialize_prowler_provider(
| MongodbatlasProvider
| OpenstackProvider
| OraclecloudProvider
| VercelProvider
):
"""Initialize a Prowler provider instance based on the given provider type.
@@ -321,6 +333,13 @@ def prowler_provider_connection_test(provider: Provider) -> Connection:
"raise_on_exception": False,
}
return prowler_provider.test_connection(**openstack_kwargs)
elif provider.provider == Provider.ProviderChoices.VERCEL.value:
vercel_kwargs = {
**prowler_provider_kwargs,
"team_id": provider.uid,
"raise_on_exception": False,
}
return prowler_provider.test_connection(**vercel_kwargs)
elif provider.provider == Provider.ProviderChoices.IMAGE.value:
image_kwargs = {
"image": provider.uid,

View File

@@ -1550,6 +1550,8 @@ class BaseWriteProviderSecretSerializer(BaseWriteSerializer):
serializer = OpenStackCloudsYamlProviderSecret(data=secret)
elif provider_type == Provider.ProviderChoices.IMAGE.value:
serializer = ImageProviderSecret(data=secret)
elif provider_type == Provider.ProviderChoices.VERCEL.value:
serializer = VercelProviderSecret(data=secret)
else:
raise serializers.ValidationError(
{"provider": f"Provider type not supported {provider_type}"}
@@ -1748,6 +1750,13 @@ class ImageProviderSecret(serializers.Serializer):
return attrs
class VercelProviderSecret(serializers.Serializer):
api_token = serializers.CharField()
class Meta:
resource_name = "provider-secrets"
class AlibabaCloudProviderSecret(serializers.Serializer):
access_key_id = serializers.CharField()
access_key_secret = serializers.CharField()

View File

@@ -288,6 +288,13 @@
"user-guide/providers/openstack/getting-started-openstack",
"user-guide/providers/openstack/authentication"
]
},
{
"group": "Vercel",
"pages": [
"user-guide/providers/vercel/getting-started-vercel",
"user-guide/providers/vercel/authentication"
]
}
]
},

View File

@@ -0,0 +1,115 @@
---
title: 'Vercel Authentication in Prowler'
---
Prowler for Vercel authenticates using a Vercel API Token (Bearer Token). Tokens can be scoped to a specific team or granted account-wide access.
## Required Permissions
Prowler requires read-only access to Vercel resources. The following API token scopes are needed:
| Scope | Description |
|-------|-------------|
| `User:Read` | Read access to user profile and token metadata |
| `Team:Read` | Read access to team settings, members, and SSO configuration |
| `Project:Read` | Read access to project settings and environment variables |
| `Deployment:Read` | Read access to deployment details and protection settings |
| `Domain:Read` | Read access to domain configuration, DNS records, and SSL certificates |
| `Firewall:Read` | Read access to WAF configuration and rules |
<Warning>
Ensure the API token has access to all resources to be scanned. If permissions are missing, some checks may fail or return incomplete results. Features gated by Vercel plan (such as managed WAF rulesets) are reported with a MANUAL status when inaccessible.
</Warning>
## API Token
### Step 1: Create an API Token
1. **Log into the Vercel Dashboard**
- Go to [https://vercel.com/account/tokens](https://vercel.com/account/tokens) and sign in
2. **Create a New Token**
- Click "Create"
- Give the token a descriptive name (e.g., "Prowler Security Scanner")
3. **Configure Token Scope**
- **Full Account:** Select "Full Account" to scan all teams and projects
- **Team-scoped:** Select a specific team to restrict the token to that team's resources
4. **Set Expiration**
- Choose an appropriate expiration period
- Prowler checks for expired and stale tokens, so regular rotation is recommended
5. **Copy the Token**
- Copy the token immediately after creation — Vercel only displays it once
### Step 2: Store the Token Securely
Store the API token as an environment variable:
```bash
export VERCEL_TOKEN="your-api-token-here"
```
For team-scoped scans, also set the team ID:
```bash
export VERCEL_TEAM="team_xxxxxxxxxxxxxxxxxxxxxxxx"
```
<Warning>
Never commit API tokens to version control or share them in plain text. Use environment variables or a secrets manager.
</Warning>
### Step 3: Run Prowler
```bash
prowler vercel
```
Alternatively, pass credentials directly via CLI arguments:
```bash
prowler vercel --vercel-token "your-api-token-here" --vercel-team "your-team-id"
```
## Team-Scoped Tokens
When a token is scoped to a specific team, all API requests must include the `teamId` parameter. Prowler handles this automatically when the team ID is provided via `VERCEL_TEAM` or `--vercel-team`.
<Note>
The Team ID can be found in the Vercel Dashboard under "Settings" → "General" for the selected team. It follows the format `team_xxxxxxxxxxxxxxxxxxxxxxxx`.
</Note>
## Best Practices
### Security Recommendations
- **Use team-scoped tokens** — Restrict access to the minimum required scope
- **Use environment variables** — Never hardcode credentials in scripts or commands
- **Rotate tokens regularly** — Create new tokens periodically and revoke old ones
- **Set token expiration** — Avoid creating tokens without an expiration date
- **Monitor token usage** — Review the "Last Used" column in the Vercel tokens dashboard for suspicious activity
## Troubleshooting
### "Vercel Credentials Not Found" Error
This error occurs when no API token is provided. Ensure `VERCEL_TOKEN` is set or pass `--vercel-token` as a CLI argument.
### "Insufficient Permissions for the Vercel API Token" (403)
- Verify the API token is correct and has not expired
- For team-scoped tokens, ensure `VERCEL_TEAM` or `--vercel-team` is set with the correct team ID
- Check that the token has the [required permissions](#required-permissions)
### "Invalid or Expired Vercel API Token" (401)
- The token has been revoked or has expired
- Generate a new token at [https://vercel.com/account/tokens](https://vercel.com/account/tokens)
### "Team Not Found or Not Accessible" (404/403)
- Verify the team ID or slug is correct
- Ensure the API token has access to the specified team
- Check that the team exists and has not been deleted

View File

@@ -0,0 +1,122 @@
---
title: 'Getting Started with Vercel'
---
import { VersionBadge } from "/snippets/version-badge.mdx";
<VersionBadge version="5.20.0" />
Prowler for Vercel scans team and project configurations for security misconfigurations, including deployment protection, environment variable exposure, WAF settings, SSO enforcement, and more.
## Prerequisites
Before running Prowler with the Vercel provider, ensure the following requirements are met:
1. A Vercel account with at least one project
2. A Vercel API Token with read permissions (see [Authentication](/user-guide/providers/vercel/authentication))
3. (Optional) A Vercel Team ID or slug to scope the scan to a specific team
## Quick Start
### Step 1: Set Up Authentication
Store the Vercel API token as an environment variable:
```bash
export VERCEL_TOKEN="your-api-token-here"
```
To scope the scan to a specific team, set the team ID or slug:
```bash
export VERCEL_TEAM="team_xxxxxxxxxxxxxxxxxxxxxxxx"
```
### Step 2: Run Prowler
Run a scan across all accessible Vercel resources:
```bash
prowler vercel
```
Prowler will automatically discover projects, domains, deployments, and team settings, then run security checks against them.
## Authentication
Prowler reads Vercel credentials from environment variables or CLI arguments. To set credentials before running Prowler:
**Environment Variables:**
```bash
export VERCEL_TOKEN="your-api-token-here"
export VERCEL_TEAM="your-team-id-or-slug"
prowler vercel
```
**CLI Arguments:**
```bash
prowler vercel --vercel-token "your-api-token-here" --vercel-team "your-team-id-or-slug"
```
<Note>
If both environment variables and CLI arguments are provided, CLI arguments take precedence.
</Note>
## Filtering Projects
By default, Prowler scans all projects accessible with the provided credentials. To scan only specific projects, use the `--project` argument:
```bash
prowler vercel --project my-project-name
```
Multiple projects can be specified:
```bash
prowler vercel --project project-one project-two
```
## Scoping to a Team
When using a team-scoped API token, the `--vercel-team` argument or `VERCEL_TEAM` environment variable must be set. Prowler uses this value to scope all API requests to the team context:
```bash
prowler vercel --vercel-team team_xxxxxxxxxxxxxxxxxxxxxxxx
```
<Warning>
Team-scoped API tokens require the team ID to be provided. Without it, Vercel returns a 403 error because the token only has permissions within the team context.
</Warning>
## Supported Services
Prowler checks the following Vercel services:
| Service | Description | Example Checks |
|---------|-------------|----------------|
| **Authentication** | API token hygiene | Expired tokens, stale tokens |
| **Deployment** | Deployment security settings | Preview access protection, stable production targets |
| **Domain** | Domain and DNS configuration | SSL certificates, DNS verification, wildcard exposure |
| **Project** | Project-level security settings | Deployment protection, environment variable encryption, fork protection |
| **Security** | Web Application Firewall (WAF) | WAF enablement, managed rulesets, rate limiting, IP blocking |
| **Team** | Team governance and access | SSO enforcement, directory sync, member role least privilege |
## Configuration
Prowler uses a configuration file to customize provider behavior. The Vercel configuration includes:
```yaml
vercel:
# Maximum number of retries for API requests (default is 2)
max_retries: 2
```
To use a custom configuration:
```bash
prowler vercel --config-file /path/to/config.yaml
```
## Next Steps
- [Authentication](/user-guide/providers/vercel/authentication) - Detailed guide on creating API tokens and required permissions

View File

@@ -16,6 +16,7 @@ import {
MongoDBAtlasProviderBadge,
OpenStackProviderBadge,
OracleCloudProviderBadge,
VercelProviderBadge,
} from "@/components/icons/providers-badge";
import {
MultiSelect,
@@ -40,6 +41,7 @@ const PROVIDER_ICON: Record<ProviderType, ReactNode> = {
alibabacloud: <AlibabaCloudProviderBadge width={18} height={18} />,
cloudflare: <CloudflareProviderBadge width={18} height={18} />,
openstack: <OpenStackProviderBadge width={18} height={18} />,
vercel: <VercelProviderBadge width={18} height={18} />,
};
interface AccountsSelectorProps {

View File

@@ -73,6 +73,11 @@ const OpenStackProviderBadge = lazy(() =>
default: m.OpenStackProviderBadge,
})),
);
const VercelProviderBadge = lazy(() =>
import("@/components/icons/providers-badge").then((m) => ({
default: m.VercelProviderBadge,
})),
);
type IconProps = { width: number; height: number };
@@ -132,6 +137,10 @@ const PROVIDER_DATA: Record<
label: "OpenStack",
icon: OpenStackProviderBadge,
},
vercel: {
label: "Vercel",
icon: VercelProviderBadge,
},
};
type ProviderTypeSelectorProps = {

View File

@@ -11,6 +11,7 @@ import {
MongoDBAtlasProviderBadge,
OpenStackProviderBadge,
OracleCloudProviderBadge,
VercelProviderBadge,
} from "@/components/icons/providers-badge";
import { ProviderType } from "@/types";
@@ -27,6 +28,7 @@ export const PROVIDER_ICONS = {
alibabacloud: AlibabaCloudProviderBadge,
cloudflare: CloudflareProviderBadge,
openstack: OpenStackProviderBadge,
vercel: VercelProviderBadge,
} as const;
interface ProviderIconCellProps {

View File

@@ -14,6 +14,7 @@ import { M365ProviderBadge } from "./m365-provider-badge";
import { MongoDBAtlasProviderBadge } from "./mongodbatlas-provider-badge";
import { OpenStackProviderBadge } from "./openstack-provider-badge";
import { OracleCloudProviderBadge } from "./oraclecloud-provider-badge";
import { VercelProviderBadge } from "./vercel-provider-badge";
export {
AlibabaCloudProviderBadge,
@@ -28,6 +29,7 @@ export {
MongoDBAtlasProviderBadge,
OpenStackProviderBadge,
OracleCloudProviderBadge,
VercelProviderBadge,
};
// Map provider display names to their icon components
@@ -44,4 +46,5 @@ export const PROVIDER_ICONS: Record<string, FC<IconSvgProps>> = {
"Alibaba Cloud": AlibabaCloudProviderBadge,
Cloudflare: CloudflareProviderBadge,
OpenStack: OpenStackProviderBadge,
Vercel: VercelProviderBadge,
};

View File

@@ -0,0 +1,23 @@
import { IconSvgProps } from "@/types";
export const VercelProviderBadge: React.FC<IconSvgProps> = ({
size,
width,
height,
...props
}) => (
<svg
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
fill="none"
focusable="false"
height={size || height}
role="presentation"
viewBox="0 0 256 256"
width={size || width}
{...props}
>
<rect width="256" height="256" fill="#000000" rx="60" />
<path d="M128 45L217 195H39L128 45Z" fill="#ffffff" />
</svg>
);

View File

@@ -21,6 +21,7 @@ import {
MongoDBAtlasProviderBadge,
OpenStackProviderBadge,
OracleCloudProviderBadge,
VercelProviderBadge,
} from "../icons/providers-badge";
import { FormMessage } from "../ui/form";
@@ -85,6 +86,11 @@ const PROVIDERS = [
label: "OpenStack",
badge: OpenStackProviderBadge,
},
{
value: "vercel",
label: "Vercel",
badge: VercelProviderBadge,
},
] as const;
interface RadioGroupProviderProps {

View File

@@ -30,6 +30,7 @@ import {
OCICredentials,
OpenStackCredentials,
ProviderType,
VercelCredentials,
} from "@/types";
import { ProviderTitleDocs } from "../provider-title-docs";
@@ -56,6 +57,7 @@ import { KubernetesCredentialsForm } from "./via-credentials/k8s-credentials-for
import { MongoDBAtlasCredentialsForm } from "./via-credentials/mongodbatlas-credentials-form";
import { OpenStackCredentialsForm } from "./via-credentials/openstack-credentials-form";
import { OracleCloudCredentialsForm } from "./via-credentials/oraclecloud-credentials-form";
import { VercelCredentialsForm } from "./via-credentials/vercel-credentials-form";
type BaseCredentialsFormProps = {
providerType: ProviderType;
@@ -256,6 +258,11 @@ export const BaseCredentialsForm = ({
control={form.control as unknown as Control<OpenStackCredentials>}
/>
)}
{providerType === "vercel" && (
<VercelCredentialsForm
control={form.control as unknown as Control<VercelCredentials>}
/>
)}
{!hideActions && (
<div className="flex w-full justify-end gap-4">

View File

@@ -106,6 +106,11 @@ const getProviderFieldDetails = (providerType?: ProviderType) => {
label: "Project ID",
placeholder: "e.g. a1b2c3d4-e5f6-7890-abcd-ef1234567890",
};
case "vercel":
return {
label: "Team ID",
placeholder: "e.g. team_xxxxxxxxxxxxxxxxxxxxxxxx",
};
default:
return {
label: "Provider UID",
@@ -123,27 +128,31 @@ function applyBackStep({
}: {
prevStep: number;
awsMethod: "single" | null;
form: Pick<UseFormReturn<FormValues>, "setValue">;
form: Pick<UseFormReturn<FormValues>, "setValue" | "clearErrors">;
setPrevStep: Dispatch<SetStateAction<number>>;
setAwsMethod: Dispatch<SetStateAction<"single" | null>>;
}) {
// If in UID form after choosing single, go back to method selector
if (prevStep === 2 && awsMethod === "single") {
setAwsMethod(null);
form.setValue("providerUid", "");
form.setValue("providerAlias", "");
form.setValue("providerUid", "", { shouldValidate: false });
form.setValue("providerAlias", "", { shouldValidate: false });
return;
}
setPrevStep((prev) => prev - 1);
// Deselect the providerType if the user is going back to the first step
if (prevStep === 2) {
form.setValue("providerType", undefined as unknown as ProviderType);
form.setValue("providerType", undefined as unknown as ProviderType, {
shouldValidate: false,
});
setAwsMethod(null);
}
// Reset the providerUid and providerAlias fields when going back
form.setValue("providerUid", "");
form.setValue("providerAlias", "");
form.setValue("providerUid", "", { shouldValidate: false });
form.setValue("providerAlias", "", { shouldValidate: false });
// Clear all validation errors so the radio buttons don't show red borders
form.clearErrors();
}
export const ConnectAccountForm = ({

View File

@@ -0,0 +1,42 @@
"use client";
import { Control } from "react-hook-form";
import { CustomInput } from "@/components/ui/custom";
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
import { VercelCredentials } from "@/types";
export const VercelCredentialsForm = ({
control,
}: {
control: Control<VercelCredentials>;
}) => {
return (
<>
<div className="flex flex-col">
<div className="text-md text-default-foreground leading-9 font-bold">
Connect via API Token
</div>
<div className="text-default-500 text-sm">
Provide a Vercel API Token with read permissions to the resources you
want Prowler to assess.
</div>
</div>
<CustomInput
control={control}
name={ProviderCredentialFields.VERCEL_API_TOKEN}
type="password"
label="API Token"
labelPlacement="inside"
placeholder="Enter your Vercel API Token"
variant="bordered"
isRequired
/>
<div className="text-default-400 text-xs">
Tokens never leave your browser unencrypted and are stored as secrets in
the backend. You can revoke the token from the Vercel dashboard anytime
at vercel.com/account/tokens.
</div>
</>
);
};

View File

@@ -11,6 +11,7 @@ import {
MongoDBAtlasProviderBadge,
OpenStackProviderBadge,
OracleCloudProviderBadge,
VercelProviderBadge,
} from "@/components/icons/providers-badge";
import { ProviderType } from "@/types";
@@ -40,6 +41,8 @@ export const getProviderLogo = (provider: ProviderType) => {
return <CloudflareProviderBadge width={35} height={35} />;
case "openstack":
return <OpenStackProviderBadge width={35} height={35} />;
case "vercel":
return <VercelProviderBadge width={35} height={35} />;
default:
return null;
}
@@ -71,6 +74,8 @@ export const getProviderName = (provider: ProviderType): string => {
return "Cloudflare";
case "openstack":
return "OpenStack";
case "vercel":
return "Vercel";
default:
return "Unknown Provider";
}

View File

@@ -226,6 +226,11 @@ export const useCredentialsForm = ({
[ProviderCredentialFields.OPENSTACK_CLOUDS_YAML_CONTENT]: "",
[ProviderCredentialFields.OPENSTACK_CLOUDS_YAML_CLOUD]: "",
};
case "vercel":
return {
...baseDefaults,
[ProviderCredentialFields.VERCEL_API_TOKEN]: "",
};
default:
return baseDefaults;
}

View File

@@ -4,8 +4,6 @@ import { IntegrationType } from "../types/integrations";
export const DOCS_URLS = {
FINDINGS_ANALYSIS:
"https://docs.prowler.com/user-guide/tutorials/prowler-app#step-8:-analyze-the-findings",
AWS_ORGANIZATIONS:
"https://docs.prowler.com/user-guide/tutorials/prowler-cloud-aws-organizations",
} as const;
export const getProviderHelpText = (provider: string) => {
@@ -70,6 +68,11 @@ export const getProviderHelpText = (provider: string) => {
text: "Need help connecting your OpenStack cloud?",
link: "https://goto.prowler.com/provider-openstack",
};
case "vercel":
return {
text: "Need help connecting your Vercel team?",
link: "https://docs.prowler.com/user-guide/providers/vercel/getting-started-vercel",
};
default:
return {
text: "How to setup a provider?",

View File

@@ -250,6 +250,16 @@ export const buildAlibabaCloudSecret = (
return filterEmptyValues(secret);
};
export const buildVercelSecret = (formData: FormData) => {
const secret = {
[ProviderCredentialFields.VERCEL_API_TOKEN]: getFormValue(
formData,
ProviderCredentialFields.VERCEL_API_TOKEN,
),
};
return filterEmptyValues(secret);
};
export const buildOpenStackSecret = (formData: FormData) => {
const secret = {
[ProviderCredentialFields.OPENSTACK_CLOUDS_YAML_CONTENT]: getFormValue(
@@ -450,6 +460,10 @@ export const buildSecretConfig = (
secretType: "static",
secret: buildOpenStackSecret(formData),
}),
vercel: () => ({
secretType: "static",
secret: buildVercelSecret(formData),
}),
};
const builder = secretBuilders[providerType];

View File

@@ -76,6 +76,9 @@ export const ProviderCredentialFields = {
// OpenStack fields
OPENSTACK_CLOUDS_YAML_CONTENT: "clouds_yaml_content",
OPENSTACK_CLOUDS_YAML_CLOUD: "clouds_yaml_cloud",
// Vercel fields
VERCEL_API_TOKEN: "api_token",
} as const;
// Type for credential field values
@@ -125,6 +128,7 @@ export const ErrorPointers = {
CLOUDFLARE_API_EMAIL: "/data/attributes/secret/api_email",
OPENSTACK_CLOUDS_YAML_CONTENT: "/data/attributes/secret/clouds_yaml_content",
OPENSTACK_CLOUDS_YAML_CLOUD: "/data/attributes/secret/clouds_yaml_cloud",
VERCEL_API_TOKEN: "/data/attributes/secret/api_token",
} as const;
export type ErrorPointer = (typeof ErrorPointers)[keyof typeof ErrorPointers];

View File

@@ -355,6 +355,11 @@ export type OpenStackCredentials = {
[ProviderCredentialFields.PROVIDER_ID]: string;
};
export type VercelCredentials = {
[ProviderCredentialFields.VERCEL_API_TOKEN]: string;
[ProviderCredentialFields.PROVIDER_ID]: string;
};
export type CredentialsFormSchema =
| AWSCredentials
| AWSCredentialsRole
@@ -369,7 +374,8 @@ export type CredentialsFormSchema =
| AlibabaCloudCredentials
| AlibabaCloudCredentialsRole
| CloudflareCredentials
| OpenStackCredentials;
| OpenStackCredentials
| VercelCredentials;
export interface SearchParamsProps {
[key: string]: string | string[] | undefined;

View File

@@ -140,6 +140,14 @@ export const addProviderFormSchema = z
[ProviderCredentialFields.PROVIDER_ALIAS]: z.string(),
providerUid: z.string(),
}),
z.object({
providerType: z.literal("vercel"),
[ProviderCredentialFields.PROVIDER_ALIAS]: z.string(),
providerUid: z
.string()
.trim()
.min(1, "Team ID is required"),
}),
]),
);
@@ -306,7 +314,14 @@ export const addCredentialsFormSchema = (
[ProviderCredentialFields.OPENSTACK_CLOUDS_YAML_CLOUD]:
z.string().min(1, "Cloud name is required"),
}
: {}),
: providerType === "vercel"
? {
[ProviderCredentialFields.VERCEL_API_TOKEN]:
z
.string()
.min(1, "API Token is required"),
}
: {}),
})
.superRefine((data: Record<string, string | undefined>, ctx) => {
if (providerType === "m365") {
@@ -420,37 +435,17 @@ export const addCredentialsRoleFormSchema = (providerType: string) =>
[ProviderCredentialFields.ROLE_SESSION_NAME]: z.string().optional(),
[ProviderCredentialFields.CREDENTIALS_TYPE]: z.string().optional(),
})
.superRefine((data, ctx) => {
if (
.refine(
(data) =>
data[ProviderCredentialFields.CREDENTIALS_TYPE] !==
"access-secret-key"
) {
return;
}
const hasAccessKey =
(data[ProviderCredentialFields.AWS_ACCESS_KEY_ID] || "").trim()
.length > 0;
const hasSecretAccessKey =
(data[ProviderCredentialFields.AWS_SECRET_ACCESS_KEY] || "").trim()
.length > 0;
if (!hasAccessKey) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "AWS Access Key ID is required.",
path: [ProviderCredentialFields.AWS_ACCESS_KEY_ID],
});
}
if (!hasSecretAccessKey) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "AWS Secret Access Key is required.",
path: [ProviderCredentialFields.AWS_SECRET_ACCESS_KEY],
});
}
})
"access-secret-key" ||
(data[ProviderCredentialFields.AWS_ACCESS_KEY_ID] &&
data[ProviderCredentialFields.AWS_SECRET_ACCESS_KEY]),
{
message: "AWS Access Key ID and Secret Access Key are required.",
path: [ProviderCredentialFields.AWS_ACCESS_KEY_ID],
},
)
: providerType === "alibabacloud"
? z.object({
[ProviderCredentialFields.PROVIDER_ID]: z.string(),

View File

@@ -11,6 +11,7 @@ export const PROVIDER_TYPES = [
"alibabacloud",
"cloudflare",
"openstack",
"vercel",
] as const;
export type ProviderType = (typeof PROVIDER_TYPES)[number];
@@ -28,6 +29,7 @@ export const PROVIDER_DISPLAY_NAMES: Record<ProviderType, string> = {
alibabacloud: "Alibaba Cloud",
cloudflare: "Cloudflare",
openstack: "OpenStack",
vercel: "Vercel",
};
export function getProviderDisplayName(providerId: string): string {