Merge remote-tracking branch 'origin/main' into feat/camunda-app-connection-and-secret-sync

This commit is contained in:
Sheen Capadngan
2025-04-11 21:53:56 +08:00
152 changed files with 4896 additions and 670 deletions

View File

@@ -1,102 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL Advanced"
on:
push:
branches: [ "main", "development" ]
pull_request:
branches: [ "main", "development" ]
schedule:
- cron: '33 7 * * 3'
jobs:
analyze:
name: Analyze (${{ matrix.language }})
# Runner size impacts CodeQL analysis time. To learn more, please see:
# - https://gh.io/recommended-hardware-resources-for-running-codeql
# - https://gh.io/supported-runners-and-hardware-resources
# - https://gh.io/using-larger-runners (GitHub.com only)
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
permissions:
# required for all workflows
security-events: write
# required to fetch internal or private CodeQL packs
packages: read
# only required for workflows in private repositories
actions: read
contents: read
strategy:
fail-fast: false
matrix:
include:
- language: actions
build-mode: none
- language: go
build-mode: autobuild
- language: javascript-typescript
build-mode: none
# CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
# Use `c-cpp` to analyze code written in C, C++ or both
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Add any setup steps before running the `github/codeql-action/init` action.
# This includes steps like installing compilers or runtimes (`actions/setup-node`
# or others). This is typically only required for manual builds.
# - name: Setup runtime (example)
# uses: actions/setup-example@v1
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# If the analyze step fails for one of the languages you are analyzing with
# "We were unable to automatically build your code", modify the matrix above
# to set the build mode to "manual" for that language. Then modify this step
# to build your code.
# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
- if: matrix.build-mode == 'manual'
shell: bash
run: |
echo 'If you are using a "manual" build mode for one or more of the' \
'languages you are analyzing, replace this with the commands to build' \
'your code, for example:'
echo ' make bootstrap'
echo ' make release'
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"

View File

@@ -1,132 +1,147 @@
name: Build and release CLI name: Build and release CLI
on: on:
workflow_dispatch: workflow_dispatch:
push: push:
# run only against tags # run only against tags
tags: tags:
- "infisical-cli/v*.*.*" - "infisical-cli/v*.*.*"
permissions: permissions:
contents: write contents: write
jobs: jobs:
cli-integration-tests: cli-integration-tests:
name: Run tests before deployment name: Run tests before deployment
uses: ./.github/workflows/run-cli-tests.yml uses: ./.github/workflows/run-cli-tests.yml
secrets: secrets:
CLI_TESTS_UA_CLIENT_ID: ${{ secrets.CLI_TESTS_UA_CLIENT_ID }} CLI_TESTS_UA_CLIENT_ID: ${{ secrets.CLI_TESTS_UA_CLIENT_ID }}
CLI_TESTS_UA_CLIENT_SECRET: ${{ secrets.CLI_TESTS_UA_CLIENT_SECRET }} CLI_TESTS_UA_CLIENT_SECRET: ${{ secrets.CLI_TESTS_UA_CLIENT_SECRET }}
CLI_TESTS_SERVICE_TOKEN: ${{ secrets.CLI_TESTS_SERVICE_TOKEN }} CLI_TESTS_SERVICE_TOKEN: ${{ secrets.CLI_TESTS_SERVICE_TOKEN }}
CLI_TESTS_PROJECT_ID: ${{ secrets.CLI_TESTS_PROJECT_ID }} CLI_TESTS_PROJECT_ID: ${{ secrets.CLI_TESTS_PROJECT_ID }}
CLI_TESTS_ENV_SLUG: ${{ secrets.CLI_TESTS_ENV_SLUG }} CLI_TESTS_ENV_SLUG: ${{ secrets.CLI_TESTS_ENV_SLUG }}
CLI_TESTS_USER_EMAIL: ${{ secrets.CLI_TESTS_USER_EMAIL }} CLI_TESTS_USER_EMAIL: ${{ secrets.CLI_TESTS_USER_EMAIL }}
CLI_TESTS_USER_PASSWORD: ${{ secrets.CLI_TESTS_USER_PASSWORD }} CLI_TESTS_USER_PASSWORD: ${{ secrets.CLI_TESTS_USER_PASSWORD }}
CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }} CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }}
npm-release: npm-release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
working-directory: ./npm
needs:
- cli-integration-tests
- goreleaser
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Extract version
run: |
VERSION=$(echo ${{ github.ref_name }} | sed 's/infisical-cli\/v//')
echo "Version extracted: $VERSION"
echo "CLI_VERSION=$VERSION" >> $GITHUB_ENV
- name: Print version
run: echo ${{ env.CLI_VERSION }}
- name: Setup Node
uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0
with:
node-version: 20
cache: "npm"
cache-dependency-path: ./npm/package-lock.json
- name: Install dependencies
working-directory: ${{ env.working-directory }}
run: npm install --ignore-scripts
- name: Set NPM version
working-directory: ${{ env.working-directory }}
run: npm version ${{ env.CLI_VERSION }} --allow-same-version --no-git-tag-version
- name: Setup NPM
working-directory: ${{ env.working-directory }}
run: |
echo 'registry="https://registry.npmjs.org/"' > ./.npmrc
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ./.npmrc
echo 'registry="https://registry.npmjs.org/"' > ~/.npmrc
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc
env: env:
working-directory: ./npm NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
needs:
- cli-integration-tests
- goreleaser
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Extract version - name: Pack NPM
run: | working-directory: ${{ env.working-directory }}
VERSION=$(echo ${{ github.ref_name }} | sed 's/infisical-cli\/v//') run: npm pack
echo "Version extracted: $VERSION"
echo "CLI_VERSION=$VERSION" >> $GITHUB_ENV
- name: Print version - name: Publish NPM
run: echo ${{ env.CLI_VERSION }} working-directory: ${{ env.working-directory }}
run: npm publish --tarball=./infisical-sdk-${{github.ref_name}} --access public --registry=https://registry.npmjs.org/
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Setup Node goreleaser:
uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0 runs-on: ubuntu-latest
with: needs: [cli-integration-tests]
node-version: 20 steps:
cache: "npm" - uses: actions/checkout@v3
cache-dependency-path: ./npm/package-lock.json with:
- name: Install dependencies fetch-depth: 0
working-directory: ${{ env.working-directory }} - name: 🐋 Login to Docker Hub
run: npm install --ignore-scripts uses: docker/login-action@v2
with:
- name: Set NPM version username: ${{ secrets.DOCKERHUB_USERNAME }}
working-directory: ${{ env.working-directory }} password: ${{ secrets.DOCKERHUB_TOKEN }}
run: npm version ${{ env.CLI_VERSION }} --allow-same-version --no-git-tag-version - name: 🔧 Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Setup NPM - run: git fetch --force --tags
working-directory: ${{ env.working-directory }} - run: echo "Ref name ${{github.ref_name}}"
run: | - uses: actions/setup-go@v3
echo 'registry="https://registry.npmjs.org/"' > ./.npmrc with:
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ./.npmrc go-version: ">=1.19.3"
cache: true
echo 'registry="https://registry.npmjs.org/"' > ~/.npmrc cache-dependency-path: cli/go.sum
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc - name: Setup for libssl1.0-dev
env: run: |
NPM_TOKEN: ${{ secrets.NPM_TOKEN }} echo 'deb http://security.ubuntu.com/ubuntu bionic-security main' | sudo tee -a /etc/apt/sources.list
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 3B4FE6ACC0B21F32
- name: Pack NPM sudo apt update
working-directory: ${{ env.working-directory }} sudo apt-get install -y libssl1.0-dev
run: npm pack - name: OSXCross for CGO Support
run: |
- name: Publish NPM mkdir ../../osxcross
working-directory: ${{ env.working-directory }} git clone https://github.com/plentico/osxcross-target.git ../../osxcross/target
run: npm publish --tarball=./infisical-sdk-${{github.ref_name}} --access public --registry=https://registry.npmjs.org/ - uses: goreleaser/goreleaser-action@v4
env: with:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }} distribution: goreleaser-pro
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} version: v1.26.2-pro
args: release --clean
goreleaser: env:
runs-on: ubuntu-latest GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }}
needs: [cli-integration-tests] POSTHOG_API_KEY_FOR_CLI: ${{ secrets.POSTHOG_API_KEY_FOR_CLI }}
steps: FURY_TOKEN: ${{ secrets.FURYPUSHTOKEN }}
- uses: actions/checkout@v3 AUR_KEY: ${{ secrets.AUR_KEY }}
with: GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
fetch-depth: 0 - uses: actions/setup-python@v4
- name: 🐋 Login to Docker Hub - run: pip install --upgrade cloudsmith-cli
uses: docker/login-action@v2 - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} ruby-version: "3.3" # Not needed with a .ruby-version, .tool-versions or mise.toml
password: ${{ secrets.DOCKERHUB_TOKEN }} bundler-cache: true # runs 'bundle install' and caches installed gems automatically
- name: 🔧 Set up Docker Buildx - name: Install deb-s3
uses: docker/setup-buildx-action@v2 run: gem install deb-s3
- run: git fetch --force --tags - name: Configure GPG Key
- run: echo "Ref name ${{github.ref_name}}" run: echo -n "$GPG_SIGNING_KEY" | base64 --decode | gpg --batch --import
- uses: actions/setup-go@v3 env:
with: GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }}
go-version: ">=1.19.3" GPG_SIGNING_KEY_PASSPHRASE: ${{ secrets.GPG_SIGNING_KEY_PASSPHRASE }}
cache: true - name: Publish to CloudSmith
cache-dependency-path: cli/go.sum run: sh cli/upload_to_cloudsmith.sh
- name: Setup for libssl1.0-dev env:
run: | CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}
echo 'deb http://security.ubuntu.com/ubuntu bionic-security main' | sudo tee -a /etc/apt/sources.list INFISICAL_CLI_S3_BUCKET: ${{ secrets.INFISICAL_CLI_S3_BUCKET }}
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 3B4FE6ACC0B21F32 INFISICAL_CLI_REPO_SIGNING_KEY_ID: ${{ secrets.INFISICAL_CLI_REPO_SIGNING_KEY_ID }}
sudo apt update AWS_ACCESS_KEY_ID: ${{ secrets.INFISICAL_CLI_REPO_AWS_ACCESS_KEY_ID }}
sudo apt-get install -y libssl1.0-dev AWS_SECRET_ACCESS_KEY: ${{ secrets.INFISICAL_CLI_REPO_AWS_SECRET_ACCESS_KEY }}
- name: OSXCross for CGO Support
run: |
mkdir ../../osxcross
git clone https://github.com/plentico/osxcross-target.git ../../osxcross/target
- uses: goreleaser/goreleaser-action@v4
with:
distribution: goreleaser-pro
version: v1.26.2-pro
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }}
POSTHOG_API_KEY_FOR_CLI: ${{ secrets.POSTHOG_API_KEY_FOR_CLI }}
FURY_TOKEN: ${{ secrets.FURYPUSHTOKEN }}
AUR_KEY: ${{ secrets.AUR_KEY }}
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
- uses: actions/setup-python@v4
- run: pip install --upgrade cloudsmith-cli
- name: Publish to CloudSmith
run: sh cli/upload_to_cloudsmith.sh
env:
CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}

View File

@@ -162,6 +162,24 @@ scoop:
description: "The official Infisical CLI" description: "The official Infisical CLI"
license: MIT license: MIT
winget:
- name: infisical
publisher: infisical
license: MIT
homepage: https://infisical.com
short_description: "The official Infisical CLI"
repository:
owner: infisical
name: winget-pkgs
branch: "infisical-{{.Version}}"
pull_request:
enabled: true
draft: false
base:
owner: microsoft
name: winget-pkgs
branch: master
aurs: aurs:
- name: infisical-bin - name: infisical-bin
homepage: "https://infisical.com" homepage: "https://infisical.com"

View File

@@ -594,6 +594,7 @@ export const scimServiceFactory = ({
}, },
tx tx
); );
await orgMembershipDAL.updateById( await orgMembershipDAL.updateById(
membership.id, membership.id,
{ {

View File

@@ -262,13 +262,14 @@ export const secretApprovalRequestServiceFactory = ({
id: el.id, id: el.id,
version: el.version, version: el.version,
secretMetadata: el.secretMetadata as ResourceMetadataDTO, secretMetadata: el.secretMetadata as ResourceMetadataDTO,
isRotatedSecret: el.secret.isRotatedSecret, isRotatedSecret: el.secret?.isRotatedSecret ?? false,
// eslint-disable-next-line no-nested-ternary secretValue:
secretValue: el.secret.isRotatedSecret // eslint-disable-next-line no-nested-ternary
? undefined el.secret && el.secret.isRotatedSecret
: el.encryptedValue ? undefined
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : el.encryptedValue
: "", ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
: "",
secretComment: el.encryptedComment secretComment: el.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
: "", : "",
@@ -615,7 +616,7 @@ export const secretApprovalRequestServiceFactory = ({
tx, tx,
inputSecrets: secretUpdationCommits.map((el) => { inputSecrets: secretUpdationCommits.map((el) => {
const encryptedValue = const encryptedValue =
!el.secret.isRotatedSecret && typeof el.encryptedValue !== "undefined" !el.secret?.isRotatedSecret && typeof el.encryptedValue !== "undefined"
? { ? {
encryptedValue: el.encryptedValue as Buffer, encryptedValue: el.encryptedValue as Buffer,
references: el.encryptedValue references: el.encryptedValue

View File

@@ -66,6 +66,17 @@ export const IDENTITIES = {
}, },
LIST: { LIST: {
orgId: "The ID of the organization to list identities." orgId: "The ID of the organization to list identities."
},
SEARCH: {
search: {
desc: "The filters to apply to the search.",
name: "The name of the identity to filter by.",
role: "The organizational role of the identity to filter by."
},
offset: "The offset to start from. If you enter 10, it will start from the 10th identity.",
limit: "The number of identities to return.",
orderBy: "The column to order identities by.",
orderDirection: "The direction to order identities in."
} }
} as const; } as const;
@@ -1694,6 +1705,9 @@ export const AppConnections = {
sslEnabled: "Whether or not to use SSL when connecting to the database.", sslEnabled: "Whether or not to use SSL when connecting to the database.",
sslRejectUnauthorized: "Whether or not to reject unauthorized SSL certificates.", sslRejectUnauthorized: "Whether or not to reject unauthorized SSL certificates.",
sslCertificate: "The SSL certificate to use for connection." sslCertificate: "The SSL certificate to use for connection."
},
VERCEL: {
apiToken: "The API token used to authenticate with Vercel."
} }
} }
}; };
@@ -1813,6 +1827,13 @@ export const SecretSyncs = {
org: "The ID of the Humanitec org to sync secrets to.", org: "The ID of the Humanitec org to sync secrets to.",
env: "The ID of the Humanitec environment to sync secrets to.", env: "The ID of the Humanitec environment to sync secrets to.",
scope: "The Humanitec scope that secrets should be synced to." scope: "The Humanitec scope that secrets should be synced to."
},
VERCEL: {
app: "The ID of the Vercel app to sync secrets to.",
appName: "The name of the Vercel app to sync secrets to.",
env: "The ID of the Vercel environment to sync secrets to.",
branch: "The branch to sync preview secrets to.",
teamId: "The ID of the Vercel team to sync secrets to."
} }
} }
}; };

View File

@@ -0,0 +1,141 @@
import { Knex } from "knex";
import { SearchResourceOperators, TSearchResourceOperator } from "./search";
const buildKnexQuery = (
query: Knex.QueryBuilder,
// when it's multiple table field means it's field1 or field2
fields: string | string[],
operator: SearchResourceOperators,
value: unknown
) => {
switch (operator) {
case SearchResourceOperators.$eq: {
if (typeof value !== "string" && typeof value !== "number")
throw new Error("Invalid value type for $eq operator");
if (typeof fields === "string") {
return void query.where(fields, "=", value);
}
return void query.where((qb) => {
return fields.forEach((el, index) => {
if (index === 0) {
return void qb.where(el, "=", value);
}
return void qb.orWhere(el, "=", value);
});
});
}
case SearchResourceOperators.$neq: {
if (typeof value !== "string" && typeof value !== "number")
throw new Error("Invalid value type for $neq operator");
if (typeof fields === "string") {
return void query.where(fields, "<>", value);
}
return void query.where((qb) => {
return fields.forEach((el, index) => {
if (index === 0) {
return void qb.where(el, "<>", value);
}
return void qb.orWhere(el, "<>", value);
});
});
}
case SearchResourceOperators.$in: {
if (!Array.isArray(value)) throw new Error("Invalid value type for $in operator");
if (typeof fields === "string") {
return void query.whereIn(fields, value);
}
return void query.where((qb) => {
return fields.forEach((el, index) => {
if (index === 0) {
return void qb.whereIn(el, value);
}
return void qb.orWhereIn(el, value);
});
});
}
case SearchResourceOperators.$contains: {
if (typeof value !== "string") throw new Error("Invalid value type for $contains operator");
if (typeof fields === "string") {
return void query.whereILike(fields, `%${value}%`);
}
return void query.where((qb) => {
return fields.forEach((el, index) => {
if (index === 0) {
return void qb.whereILike(el, `%${value}%`);
}
return void qb.orWhereILike(el, `%${value}%`);
});
});
}
default:
throw new Error(`Unsupported operator: ${String(operator)}`);
}
};
export const buildKnexFilterForSearchResource = <T extends { [K: string]: TSearchResourceOperator }, K extends keyof T>(
rootQuery: Knex.QueryBuilder,
searchFilter: T & { $or?: T[] },
getAttributeField: (attr: K) => string | string[] | null
) => {
const { $or: orFilters = [] } = searchFilter;
(Object.keys(searchFilter) as K[]).forEach((key) => {
// akhilmhdh: yes, we could have split in top. This is done to satisfy ts type error
if (key === "$or") return;
const dbField = getAttributeField(key);
if (!dbField) throw new Error(`DB field not found for ${String(key)}`);
const dbValue = searchFilter[key];
if (typeof dbValue === "string" || typeof dbValue === "number") {
buildKnexQuery(rootQuery, dbField, SearchResourceOperators.$eq, dbValue);
return;
}
Object.keys(dbValue as Record<string, unknown>).forEach((el) => {
buildKnexQuery(
rootQuery,
dbField,
el as SearchResourceOperators,
(dbValue as Record<SearchResourceOperators, unknown>)[el as SearchResourceOperators]
);
});
});
if (orFilters.length) {
void rootQuery.andWhere((andQb) => {
return orFilters.forEach((orFilter) => {
return void andQb.orWhere((qb) => {
(Object.keys(orFilter) as K[]).forEach((key) => {
const dbField = getAttributeField(key);
if (!dbField) throw new Error(`DB field not found for ${String(key)}`);
const dbValue = orFilter[key];
if (typeof dbValue === "string" || typeof dbValue === "number") {
buildKnexQuery(qb, dbField, SearchResourceOperators.$eq, dbValue);
return;
}
Object.keys(dbValue as Record<string, unknown>).forEach((el) => {
buildKnexQuery(
qb,
dbField,
el as SearchResourceOperators,
(dbValue as Record<SearchResourceOperators, unknown>)[el as SearchResourceOperators]
);
});
});
});
});
});
}
};

View File

@@ -0,0 +1,43 @@
import { z } from "zod";
export enum SearchResourceOperators {
$eq = "$eq",
$neq = "$neq",
$in = "$in",
$contains = "$contains"
}
export const SearchResourceOperatorSchema = z.union([
z.string(),
z.number(),
z
.object({
[SearchResourceOperators.$eq]: z.string().optional(),
[SearchResourceOperators.$neq]: z.string().optional(),
[SearchResourceOperators.$in]: z.string().array().optional(),
[SearchResourceOperators.$contains]: z.string().array().optional()
})
.partial()
]);
export type TSearchResourceOperator = z.infer<typeof SearchResourceOperatorSchema>;
export type TSearchResource = {
[k: string]: z.ZodOptional<
z.ZodUnion<
[
z.ZodEffects<z.ZodString | z.ZodNumber>,
z.ZodObject<{
[SearchResourceOperators.$eq]?: z.ZodOptional<z.ZodEffects<z.ZodString | z.ZodNumber>>;
[SearchResourceOperators.$neq]?: z.ZodOptional<z.ZodEffects<z.ZodString | z.ZodNumber>>;
[SearchResourceOperators.$in]?: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodString | z.ZodNumber>>>;
[SearchResourceOperators.$contains]?: z.ZodOptional<z.ZodEffects<z.ZodString>>;
}>
]
>
>;
};
export const buildSearchZodSchema = <T extends TSearchResource>(schema: z.ZodObject<T>) => {
return schema.extend({ $or: schema.array().max(5).optional() }).optional();
};

View File

@@ -1,3 +1,5 @@
import { z } from "zod";
export enum CharacterType { export enum CharacterType {
Alphabets = "alphabets", Alphabets = "alphabets",
Numbers = "numbers", Numbers = "numbers",
@@ -101,3 +103,10 @@ export const characterValidator = (allowedCharacters: CharacterType[]) => {
return regex.test(input); return regex.test(input);
}; };
}; };
export const zodValidateCharacters = (allowedCharacters: CharacterType[]) => {
const validator = characterValidator(allowedCharacters);
return (schema: z.ZodString, fieldName: string) => {
return schema.refine(validator, { message: `${fieldName} can only contain ${allowedCharacters.join(",")}` });
};
};

View File

@@ -113,7 +113,7 @@ export const main = async ({ db, hsmModule, auditLogDb, smtp, logger, queue, key
await server.register(fastifyErrHandler); await server.register(fastifyErrHandler);
// Rate limiters and security headers // Rate limiters and security headers
if (appCfg.isProductionMode) { if (appCfg.isProductionMode && appCfg.isCloud) {
await server.register<FastifyRateLimitOptions>(ratelimiter, globalRateLimiterCfg()); await server.register<FastifyRateLimitOptions>(ratelimiter, globalRateLimiterCfg());
} }

View File

@@ -45,4 +45,6 @@ export const BaseSecretNameSchema = z.string().trim().min(1);
export const SecretNameSchema = BaseSecretNameSchema.refine( export const SecretNameSchema = BaseSecretNameSchema.refine(
(el) => !el.includes(" "), (el) => !el.includes(" "),
"Secret name cannot contain spaces." "Secret name cannot contain spaces."
).refine((el) => !el.includes(":"), "Secret name cannot contain colon."); )
.refine((el) => !el.includes(":"), "Secret name cannot contain colon.")
.refine((el) => !el.includes("/"), "Secret name cannot contain forward slash.");

View File

@@ -31,6 +31,7 @@ import {
PostgresConnectionListItemSchema, PostgresConnectionListItemSchema,
SanitizedPostgresConnectionSchema SanitizedPostgresConnectionSchema
} from "@app/services/app-connection/postgres"; } from "@app/services/app-connection/postgres";
import { SanitizedVercelConnectionSchema, VercelConnectionListItemSchema } from "@app/services/app-connection/vercel";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
// can't use discriminated due to multiple schemas for certain apps // can't use discriminated due to multiple schemas for certain apps
@@ -42,6 +43,7 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedAzureAppConfigurationConnectionSchema.options, ...SanitizedAzureAppConfigurationConnectionSchema.options,
...SanitizedDatabricksConnectionSchema.options, ...SanitizedDatabricksConnectionSchema.options,
...SanitizedHumanitecConnectionSchema.options, ...SanitizedHumanitecConnectionSchema.options,
...SanitizedVercelConnectionSchema.options,
...SanitizedPostgresConnectionSchema.options, ...SanitizedPostgresConnectionSchema.options,
...SanitizedMsSqlConnectionSchema.options, ...SanitizedMsSqlConnectionSchema.options,
...SanitizedCamundaConnectionSchema.options ...SanitizedCamundaConnectionSchema.options
@@ -55,6 +57,7 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
AzureAppConfigurationConnectionListItemSchema, AzureAppConfigurationConnectionListItemSchema,
DatabricksConnectionListItemSchema, DatabricksConnectionListItemSchema,
HumanitecConnectionListItemSchema, HumanitecConnectionListItemSchema,
VercelConnectionListItemSchema,
PostgresConnectionListItemSchema, PostgresConnectionListItemSchema,
MsSqlConnectionListItemSchema, MsSqlConnectionListItemSchema,
CamundaConnectionListItemSchema CamundaConnectionListItemSchema

View File

@@ -10,6 +10,7 @@ import { registerGitHubConnectionRouter } from "./github-connection-router";
import { registerHumanitecConnectionRouter } from "./humanitec-connection-router"; import { registerHumanitecConnectionRouter } from "./humanitec-connection-router";
import { registerMsSqlConnectionRouter } from "./mssql-connection-router"; import { registerMsSqlConnectionRouter } from "./mssql-connection-router";
import { registerPostgresConnectionRouter } from "./postgres-connection-router"; import { registerPostgresConnectionRouter } from "./postgres-connection-router";
import { registerVercelConnectionRouter } from "./vercel-connection-router";
export * from "./app-connection-router"; export * from "./app-connection-router";
@@ -22,6 +23,7 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
[AppConnection.AzureAppConfiguration]: registerAzureAppConfigurationConnectionRouter, [AppConnection.AzureAppConfiguration]: registerAzureAppConfigurationConnectionRouter,
[AppConnection.Databricks]: registerDatabricksConnectionRouter, [AppConnection.Databricks]: registerDatabricksConnectionRouter,
[AppConnection.Humanitec]: registerHumanitecConnectionRouter, [AppConnection.Humanitec]: registerHumanitecConnectionRouter,
[AppConnection.Vercel]: registerVercelConnectionRouter,
[AppConnection.Postgres]: registerPostgresConnectionRouter, [AppConnection.Postgres]: registerPostgresConnectionRouter,
[AppConnection.MsSql]: registerMsSqlConnectionRouter, [AppConnection.MsSql]: registerMsSqlConnectionRouter,
[AppConnection.Camunda]: registerCamundaConnectionRouter [AppConnection.Camunda]: registerCamundaConnectionRouter

View File

@@ -0,0 +1,77 @@
import z from "zod";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
CreateVercelConnectionSchema,
SanitizedVercelConnectionSchema,
UpdateVercelConnectionSchema,
VercelOrgWithApps
} from "@app/services/app-connection/vercel";
import { AuthMode } from "@app/services/auth/auth-type";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerVercelConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({
app: AppConnection.Vercel,
server,
sanitizedResponseSchema: SanitizedVercelConnectionSchema,
createSchema: CreateVercelConnectionSchema,
updateSchema: UpdateVercelConnectionSchema
});
// The below endpoints are not exposed and for Infisical App use
server.route({
method: "GET",
url: `/:connectionId/projects`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid()
}),
response: {
200: z
.object({
id: z.string(),
name: z.string(),
slug: z.string(),
apps: z
.object({
id: z.string(),
name: z.string(),
envs: z
.object({
id: z.string(),
slug: z.string(),
type: z.string(),
target: z.array(z.string()).optional(),
description: z.string().optional(),
createdAt: z.number().optional(),
updatedAt: z.number().optional()
})
.array()
.optional(),
previewBranches: z.array(z.string()).optional()
})
.array()
})
.array()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { connectionId } = req.params;
const projects: VercelOrgWithApps[] = await server.services.appConnection.vercel.listProjects(
connectionId,
req.permission
);
return projects;
}
});
};

View File

@@ -3,15 +3,26 @@ import { z } from "zod";
import { IdentitiesSchema, IdentityOrgMembershipsSchema, OrgMembershipRole, OrgRolesSchema } from "@app/db/schemas"; import { IdentitiesSchema, IdentityOrgMembershipsSchema, OrgMembershipRole, OrgRolesSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { IDENTITIES } from "@app/lib/api-docs"; import { IDENTITIES } from "@app/lib/api-docs";
import { buildSearchZodSchema, SearchResourceOperators } from "@app/lib/search-resource/search";
import { OrderByDirection } from "@app/lib/types";
import { CharacterType, zodValidateCharacters } from "@app/lib/validator/validate-string";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { OrgIdentityOrderBy } from "@app/services/identity/identity-types";
import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns"; import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
import { SanitizedProjectSchema } from "../sanitizedSchemas"; import { SanitizedProjectSchema } from "../sanitizedSchemas";
const searchResourceZodValidate = zodValidateCharacters([
CharacterType.AlphaNumeric,
CharacterType.Spaces,
CharacterType.Underscore,
CharacterType.Hyphen
]);
export const registerIdentityRouter = async (server: FastifyZodProvider) => { export const registerIdentityRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
method: "POST", method: "POST",
@@ -245,7 +256,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
method: "GET", method: "GET",
url: "/", url: "/",
config: { config: {
rateLimit: writeLimit rateLimit: readLimit
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
@@ -289,6 +300,103 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
} }
}); });
server.route({
method: "POST",
url: "/search",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Search identities",
security: [
{
bearerAuth: []
}
],
body: z.object({
orderBy: z
.nativeEnum(OrgIdentityOrderBy)
.default(OrgIdentityOrderBy.Name)
.describe(IDENTITIES.SEARCH.orderBy)
.optional(),
orderDirection: z
.nativeEnum(OrderByDirection)
.default(OrderByDirection.ASC)
.describe(IDENTITIES.SEARCH.orderDirection)
.optional(),
limit: z.number().max(100).default(50).describe(IDENTITIES.SEARCH.limit),
offset: z.number().default(0).describe(IDENTITIES.SEARCH.offset),
search: buildSearchZodSchema(
z
.object({
name: z
.union([
searchResourceZodValidate(z.string().max(255), "Name"),
z
.object({
[SearchResourceOperators.$eq]: searchResourceZodValidate(z.string().max(255), "Name $eq"),
[SearchResourceOperators.$contains]: searchResourceZodValidate(
z.string().max(255),
"Name $contains"
),
[SearchResourceOperators.$in]: searchResourceZodValidate(z.string().max(255), "Name $in").array()
})
.partial()
])
.describe(IDENTITIES.SEARCH.search.name),
role: z
.union([
searchResourceZodValidate(z.string().max(255), "Role"),
z
.object({
[SearchResourceOperators.$eq]: searchResourceZodValidate(z.string().max(255), "Role $eq"),
[SearchResourceOperators.$in]: searchResourceZodValidate(z.string().max(255), "Role $in").array()
})
.partial()
])
.describe(IDENTITIES.SEARCH.search.role)
})
.describe(IDENTITIES.SEARCH.search.desc)
.partial()
)
}),
response: {
200: z.object({
identities: IdentityOrgMembershipsSchema.extend({
customRole: OrgRolesSchema.pick({
id: true,
name: true,
slug: true,
permissions: true,
description: true
}).optional(),
identity: IdentitiesSchema.pick({ name: true, id: true }).extend({
authMethods: z.array(z.string())
})
}).array(),
totalCount: z.number()
})
}
},
handler: async (req) => {
const { identityMemberships, totalCount } = await server.services.identity.searchOrgIdentities({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
searchFilter: req.body.search,
orgId: req.permission.orgId,
limit: req.body.limit,
offset: req.body.offset,
orderBy: req.body.orderBy,
orderDirection: req.body.orderDirection
});
return { identities: identityMemberships, totalCount };
}
});
server.route({ server.route({
method: "GET", method: "GET",
url: "/:identityId/identity-memberships", url: "/:identityId/identity-memberships",

View File

@@ -9,6 +9,7 @@ import { registerDatabricksSyncRouter } from "./databricks-sync-router";
import { registerGcpSyncRouter } from "./gcp-sync-router"; import { registerGcpSyncRouter } from "./gcp-sync-router";
import { registerGitHubSyncRouter } from "./github-sync-router"; import { registerGitHubSyncRouter } from "./github-sync-router";
import { registerHumanitecSyncRouter } from "./humanitec-sync-router"; import { registerHumanitecSyncRouter } from "./humanitec-sync-router";
import { registerVercelSyncRouter } from "./vercel-sync-router";
export * from "./secret-sync-router"; export * from "./secret-sync-router";
@@ -21,5 +22,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: Fastif
[SecretSync.AzureAppConfiguration]: registerAzureAppConfigurationSyncRouter, [SecretSync.AzureAppConfiguration]: registerAzureAppConfigurationSyncRouter,
[SecretSync.Databricks]: registerDatabricksSyncRouter, [SecretSync.Databricks]: registerDatabricksSyncRouter,
[SecretSync.Humanitec]: registerHumanitecSyncRouter, [SecretSync.Humanitec]: registerHumanitecSyncRouter,
[SecretSync.Camunda]: registerCamundaSyncRouter [SecretSync.Camunda]: registerCamundaSyncRouter,
[SecretSync.Vercel]: registerVercelSyncRouter
}; };

View File

@@ -23,6 +23,7 @@ import { DatabricksSyncListItemSchema, DatabricksSyncSchema } from "@app/service
import { GcpSyncListItemSchema, GcpSyncSchema } from "@app/services/secret-sync/gcp"; import { GcpSyncListItemSchema, GcpSyncSchema } from "@app/services/secret-sync/gcp";
import { GitHubSyncListItemSchema, GitHubSyncSchema } from "@app/services/secret-sync/github"; import { GitHubSyncListItemSchema, GitHubSyncSchema } from "@app/services/secret-sync/github";
import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/secret-sync/humanitec"; import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/secret-sync/humanitec";
import { VercelSyncListItemSchema, VercelSyncSchema } from "@app/services/secret-sync/vercel";
const SecretSyncSchema = z.discriminatedUnion("destination", [ const SecretSyncSchema = z.discriminatedUnion("destination", [
AwsParameterStoreSyncSchema, AwsParameterStoreSyncSchema,
@@ -33,7 +34,8 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
AzureAppConfigurationSyncSchema, AzureAppConfigurationSyncSchema,
DatabricksSyncSchema, DatabricksSyncSchema,
HumanitecSyncSchema, HumanitecSyncSchema,
CamundaSyncSchema CamundaSyncSchema,
VercelSyncSchema
]); ]);
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
@@ -45,7 +47,8 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
AzureAppConfigurationSyncListItemSchema, AzureAppConfigurationSyncListItemSchema,
DatabricksSyncListItemSchema, DatabricksSyncListItemSchema,
HumanitecSyncListItemSchema, HumanitecSyncListItemSchema,
CamundaSyncListItemSchema CamundaSyncListItemSchema,
VercelSyncListItemSchema
]); ]);
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => { export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {

View File

@@ -0,0 +1,13 @@
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { CreateVercelSyncSchema, UpdateVercelSyncSchema, VercelSyncSchema } from "@app/services/secret-sync/vercel";
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
export const registerVercelSyncRouter = async (server: FastifyZodProvider) =>
registerSyncSecretsEndpoints({
destination: SecretSync.Vercel,
server,
responseSchema: VercelSyncSchema,
createSchema: CreateVercelSyncSchema,
updateSchema: UpdateVercelSyncSchema
});

View File

@@ -351,4 +351,56 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
return { identityMembership }; return { identityMembership };
} }
}); });
server.route({
method: "GET",
url: "/identity-memberships/:identityMembershipId",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
params: z.object({
identityMembershipId: z.string().trim()
}),
response: {
200: z.object({
identityMembership: z.object({
id: z.string(),
identityId: z.string(),
createdAt: z.date(),
updatedAt: z.date(),
roles: z.array(
z.object({
id: z.string(),
role: z.string(),
customRoleId: z.string().optional().nullable(),
customRoleName: z.string().optional().nullable(),
customRoleSlug: z.string().optional().nullable(),
isTemporary: z.boolean(),
temporaryMode: z.string().optional().nullable(),
temporaryRange: z.string().nullable().optional(),
temporaryAccessStartTime: z.date().nullable().optional(),
temporaryAccessEndTime: z.date().nullable().optional()
})
),
identity: IdentitiesSchema.pick({ name: true, id: true }).extend({
authMethods: z.array(z.string())
}),
project: SanitizedProjectSchema.pick({ name: true, id: true })
})
})
}
},
handler: async (req) => {
const identityMembership = await server.services.identityProject.getProjectIdentityByMembershipId({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
identityMembershipId: req.params.identityMembershipId
});
return { identityMembership };
}
});
}; };

View File

@@ -6,6 +6,7 @@ export enum AppConnection {
AzureKeyVault = "azure-key-vault", AzureKeyVault = "azure-key-vault",
AzureAppConfiguration = "azure-app-configuration", AzureAppConfiguration = "azure-app-configuration",
Humanitec = "humanitec", Humanitec = "humanitec",
Vercel = "vercel",
Postgres = "postgres", Postgres = "postgres",
MsSql = "mssql", MsSql = "mssql",
Camunda = "camunda" Camunda = "camunda"

View File

@@ -42,6 +42,8 @@ import {
} from "./humanitec"; } from "./humanitec";
import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql"; import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql";
import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres"; import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres";
import { VercelConnectionMethod } from "./vercel";
import { getVercelConnectionListItem, validateVercelConnectionCredentials } from "./vercel/vercel-connection-fns";
export const listAppConnectionOptions = () => { export const listAppConnectionOptions = () => {
return [ return [
@@ -52,6 +54,7 @@ export const listAppConnectionOptions = () => {
getAzureAppConfigurationConnectionListItem(), getAzureAppConfigurationConnectionListItem(),
getDatabricksConnectionListItem(), getDatabricksConnectionListItem(),
getHumanitecConnectionListItem(), getHumanitecConnectionListItem(),
getVercelConnectionListItem(),
getPostgresConnectionListItem(), getPostgresConnectionListItem(),
getMsSqlConnectionListItem(), getMsSqlConnectionListItem(),
getCamundaConnectionListItem() getCamundaConnectionListItem()
@@ -111,7 +114,8 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TAppConnect
[AppConnection.Humanitec]: validateHumanitecConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Humanitec]: validateHumanitecConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Postgres]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Postgres]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.MsSql]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.MsSql]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Camunda]: validateCamundaConnectionCredentials as TAppConnectionCredentialsValidator [AppConnection.Camunda]: validateCamundaConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Vercel]: validateVercelConnectionCredentials as TAppConnectionCredentialsValidator
}; };
export const validateAppConnectionCredentials = async ( export const validateAppConnectionCredentials = async (
@@ -137,6 +141,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
case CamundaConnectionMethod.ClientCredentials: case CamundaConnectionMethod.ClientCredentials:
return "Client Credentials"; return "Client Credentials";
case HumanitecConnectionMethod.ApiToken: case HumanitecConnectionMethod.ApiToken:
case VercelConnectionMethod.ApiToken:
return "API Token"; return "API Token";
case PostgresConnectionMethod.UsernameAndPassword: case PostgresConnectionMethod.UsernameAndPassword:
case MsSqlConnectionMethod.UsernameAndPassword: case MsSqlConnectionMethod.UsernameAndPassword:
@@ -181,5 +186,6 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
[AppConnection.Humanitec]: platformManagedCredentialsNotSupported, [AppConnection.Humanitec]: platformManagedCredentialsNotSupported,
[AppConnection.Postgres]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform, [AppConnection.Postgres]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform,
[AppConnection.MsSql]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform, [AppConnection.MsSql]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform,
[AppConnection.Camunda]: platformManagedCredentialsNotSupported [AppConnection.Camunda]: platformManagedCredentialsNotSupported,
[AppConnection.Vercel]: platformManagedCredentialsNotSupported
}; };

View File

@@ -8,6 +8,7 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.AzureAppConfiguration]: "Azure App Configuration", [AppConnection.AzureAppConfiguration]: "Azure App Configuration",
[AppConnection.Databricks]: "Databricks", [AppConnection.Databricks]: "Databricks",
[AppConnection.Humanitec]: "Humanitec", [AppConnection.Humanitec]: "Humanitec",
[AppConnection.Vercel]: "Vercel",
[AppConnection.Postgres]: "PostgreSQL", [AppConnection.Postgres]: "PostgreSQL",
[AppConnection.MsSql]: "Microsoft SQL Server", [AppConnection.MsSql]: "Microsoft SQL Server",
[AppConnection.Camunda]: "Camunda" [AppConnection.Camunda]: "Camunda"

View File

@@ -43,6 +43,8 @@ import { ValidateHumanitecConnectionCredentialsSchema } from "./humanitec";
import { humanitecConnectionService } from "./humanitec/humanitec-connection-service"; import { humanitecConnectionService } from "./humanitec/humanitec-connection-service";
import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql"; import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql";
import { ValidatePostgresConnectionCredentialsSchema } from "./postgres"; import { ValidatePostgresConnectionCredentialsSchema } from "./postgres";
import { ValidateVercelConnectionCredentialsSchema } from "./vercel";
import { vercelConnectionService } from "./vercel/vercel-connection-service";
export type TAppConnectionServiceFactoryDep = { export type TAppConnectionServiceFactoryDep = {
appConnectionDAL: TAppConnectionDALFactory; appConnectionDAL: TAppConnectionDALFactory;
@@ -60,6 +62,7 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
[AppConnection.AzureAppConfiguration]: ValidateAzureAppConfigurationConnectionCredentialsSchema, [AppConnection.AzureAppConfiguration]: ValidateAzureAppConfigurationConnectionCredentialsSchema,
[AppConnection.Databricks]: ValidateDatabricksConnectionCredentialsSchema, [AppConnection.Databricks]: ValidateDatabricksConnectionCredentialsSchema,
[AppConnection.Humanitec]: ValidateHumanitecConnectionCredentialsSchema, [AppConnection.Humanitec]: ValidateHumanitecConnectionCredentialsSchema,
[AppConnection.Vercel]: ValidateVercelConnectionCredentialsSchema,
[AppConnection.Postgres]: ValidatePostgresConnectionCredentialsSchema, [AppConnection.Postgres]: ValidatePostgresConnectionCredentialsSchema,
[AppConnection.MsSql]: ValidateMsSqlConnectionCredentialsSchema, [AppConnection.MsSql]: ValidateMsSqlConnectionCredentialsSchema,
[AppConnection.Camunda]: ValidateCamundaConnectionCredentialsSchema [AppConnection.Camunda]: ValidateCamundaConnectionCredentialsSchema
@@ -434,6 +437,7 @@ export const appConnectionServiceFactory = ({
databricks: databricksConnectionService(connectAppConnectionById, appConnectionDAL, kmsService), databricks: databricksConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
aws: awsConnectionService(connectAppConnectionById), aws: awsConnectionService(connectAppConnectionById),
humanitec: humanitecConnectionService(connectAppConnectionById), humanitec: humanitecConnectionService(connectAppConnectionById),
camunda: camundaConnectionService(connectAppConnectionById, appConnectionDAL, kmsService) camunda: camundaConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
vercel: vercelConnectionService(connectAppConnectionById)
}; };
}; };

View File

@@ -57,6 +57,12 @@ import {
TPostgresConnectionInput, TPostgresConnectionInput,
TValidatePostgresConnectionCredentialsSchema TValidatePostgresConnectionCredentialsSchema
} from "./postgres"; } from "./postgres";
import {
TValidateVercelConnectionCredentialsSchema,
TVercelConnection,
TVercelConnectionConfig,
TVercelConnectionInput
} from "./vercel";
export type TAppConnection = { id: string } & ( export type TAppConnection = { id: string } & (
| TAwsConnection | TAwsConnection
@@ -66,6 +72,7 @@ export type TAppConnection = { id: string } & (
| TAzureAppConfigurationConnection | TAzureAppConfigurationConnection
| TDatabricksConnection | TDatabricksConnection
| THumanitecConnection | THumanitecConnection
| TVercelConnection
| TPostgresConnection | TPostgresConnection
| TMsSqlConnection | TMsSqlConnection
| TCamundaConnection | TCamundaConnection
@@ -83,6 +90,7 @@ export type TAppConnectionInput = { id: string } & (
| TAzureAppConfigurationConnectionInput | TAzureAppConfigurationConnectionInput
| TDatabricksConnectionInput | TDatabricksConnectionInput
| THumanitecConnectionInput | THumanitecConnectionInput
| TVercelConnectionInput
| TPostgresConnectionInput | TPostgresConnectionInput
| TMsSqlConnectionInput | TMsSqlConnectionInput
| TCamundaConnectionInput | TCamundaConnectionInput
@@ -108,7 +116,8 @@ export type TAppConnectionConfig =
| TDatabricksConnectionConfig | TDatabricksConnectionConfig
| THumanitecConnectionConfig | THumanitecConnectionConfig
| TSqlConnectionConfig | TSqlConnectionConfig
| TCamundaConnectionConfig; | TCamundaConnectionConfig
| TVercelConnectionConfig;
export type TValidateAppConnectionCredentialsSchema = export type TValidateAppConnectionCredentialsSchema =
| TValidateAwsConnectionCredentialsSchema | TValidateAwsConnectionCredentialsSchema
@@ -120,7 +129,8 @@ export type TValidateAppConnectionCredentialsSchema =
| TValidateHumanitecConnectionCredentialsSchema | TValidateHumanitecConnectionCredentialsSchema
| TValidatePostgresConnectionCredentialsSchema | TValidatePostgresConnectionCredentialsSchema
| TValidateMsSqlConnectionCredentialsSchema | TValidateMsSqlConnectionCredentialsSchema
| TValidateCamundaConnectionCredentialsSchema; | TValidateCamundaConnectionCredentialsSchema
| TValidateVercelConnectionCredentialsSchema;
export type TListAwsConnectionKmsKeys = { export type TListAwsConnectionKmsKeys = {
connectionId: string; connectionId: string;

View File

@@ -0,0 +1,4 @@
export * from "./vercel-connection-enums";
export * from "./vercel-connection-fns";
export * from "./vercel-connection-schemas";
export * from "./vercel-connection-types";

View File

@@ -0,0 +1,3 @@
export enum VercelConnectionMethod {
ApiToken = "api-token"
}

View File

@@ -0,0 +1,273 @@
/* eslint-disable no-await-in-loop */
import { AxiosError, AxiosResponse } from "axios";
import { request } from "@app/lib/config/request";
import { BadRequestError, InternalServerError } from "@app/lib/errors";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { TVercelBranches } from "@app/services/integration-auth/integration-auth-types";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { VercelConnectionMethod } from "./vercel-connection-enums";
import {
TVercelConnection,
TVercelConnectionConfig,
VercelApp,
VercelEnvironment,
VercelOrgWithApps
} from "./vercel-connection-types";
export const getVercelConnectionListItem = () => {
return {
name: "Vercel" as const,
app: AppConnection.Vercel as const,
methods: Object.values(VercelConnectionMethod) as [VercelConnectionMethod.ApiToken]
};
};
export const validateVercelConnectionCredentials = async (config: TVercelConnectionConfig) => {
const { credentials: inputCredentials } = config;
let response: AxiosResponse<VercelApp[]> | null = null;
try {
response = await request.get<VercelApp[]>(`${IntegrationUrls.VERCEL_API_URL}/v9/projects`, {
headers: {
Authorization: `Bearer ${inputCredentials.apiToken}`
}
});
} catch (error: unknown) {
if (error instanceof AxiosError) {
throw new BadRequestError({
message: `Failed to validate credentials: ${error.message || "Unknown error"}`
});
}
throw new BadRequestError({
message: "Unable to validate connection - verify credentials"
});
}
if (!response?.data) {
throw new InternalServerError({
message: "Failed to get organizations: Response was empty"
});
}
return inputCredentials;
};
interface ApiResponse<T> {
pagination?: {
count: number;
next: number;
};
data: T[];
[key: string]: unknown;
}
async function fetchAllPages<T>(
apiUrl: string,
apiToken: string,
initialParams: Record<string, string | number> = {},
dataPath?: string
): Promise<T[]> {
const allItems: T[] = [];
let hasMoreItems = true;
let params: Record<string, string | number> = { ...initialParams, limit: 100 };
while (hasMoreItems) {
try {
const response = await request.get<ApiResponse<T>>(apiUrl, {
params,
headers: {
Authorization: `Bearer ${apiToken}`,
"Accept-Encoding": "application/json"
}
});
if (!response?.data) {
throw new InternalServerError({
message: `Failed to fetch data from ${apiUrl}: Response was empty or malformed`
});
}
let itemsData: T[];
if (dataPath && dataPath in response.data) {
itemsData = response.data[dataPath] as T[];
} else {
itemsData = response.data.data;
}
if (!Array.isArray(itemsData)) {
throw new InternalServerError({
message: `Failed to fetch data from ${apiUrl}: Expected array but got ${typeof itemsData}`
});
}
allItems.push(...itemsData);
if (response.data.pagination?.next) {
params = { ...params, since: response.data.pagination.next };
} else {
hasMoreItems = false;
}
} catch (error) {
if (error instanceof AxiosError) {
throw new BadRequestError({
message: `Failed to fetch data from ${apiUrl}: ${error.message || "Unknown error"}`
});
}
throw error;
}
}
return allItems;
}
async function fetchOrgProjects(orgId: string, apiToken: string): Promise<VercelApp[]> {
return fetchAllPages<VercelApp>(
`${IntegrationUrls.VERCEL_API_URL}/v9/projects`,
apiToken,
{ teamId: orgId },
"projects"
);
}
async function fetchProjectEnvironments(
projectId: string,
teamId: string,
apiToken: string
): Promise<VercelEnvironment[]> {
try {
return await fetchAllPages<VercelEnvironment>(
`${IntegrationUrls.VERCEL_API_URL}/v9/projects/${projectId}/custom-environments?teamId=${teamId}`,
apiToken,
{},
"environments"
);
} catch (error) {
return [];
}
}
async function fetchPreviewBranches(projectId: string, apiToken: string): Promise<string[]> {
try {
const { data } = await request.get<TVercelBranches[]>(
`${IntegrationUrls.VERCEL_API_URL}/v1/integrations/git-branches`,
{
params: {
projectId
},
headers: {
Authorization: `Bearer ${apiToken}`,
"Accept-Encoding": "application/json"
}
}
);
return data.filter((b) => b.ref !== "main").map((b) => b.ref);
} catch (error) {
return [];
}
}
type VercelTeam = {
id: string;
name: string;
slug: string;
};
type VercelUserResponse = {
user: {
id: string;
name: string;
username: string;
};
};
export const listProjects = async (appConnection: TVercelConnection): Promise<VercelOrgWithApps[]> => {
const { credentials } = appConnection;
const { apiToken } = credentials;
const orgs = await fetchAllPages<VercelTeam>(`${IntegrationUrls.VERCEL_API_URL}/v2/teams`, apiToken, {}, "teams");
const personalAccountResponse = await request.get<VercelUserResponse>(`${IntegrationUrls.VERCEL_API_URL}/v2/user`, {
headers: {
Authorization: `Bearer ${apiToken}`,
"Accept-Encoding": "application/json"
}
});
if (personalAccountResponse?.data?.user) {
const { user } = personalAccountResponse.data;
orgs.push({
id: user.id,
name: user.name || "Personal Account",
slug: user.username || "personal"
});
}
const orgsWithApps: VercelOrgWithApps[] = [];
const orgPromises = orgs.map(async (org) => {
try {
const projects = await fetchOrgProjects(org.id, apiToken);
const enhancedProjectsPromises = projects.map(async (project) => {
try {
const [environments, previewBranches] = await Promise.all([
fetchProjectEnvironments(project.name, org.id, apiToken),
fetchPreviewBranches(project.id, apiToken)
]);
return {
name: project.name,
id: project.id,
envs: environments,
previewBranches
};
} catch (error) {
return {
name: project.name,
id: project.id,
envs: [],
previewBranches: []
};
}
});
const enhancedProjects = await Promise.all(enhancedProjectsPromises);
return {
...org,
apps: enhancedProjects
};
} catch (error) {
return null;
}
});
const results = await Promise.all(orgPromises);
results.forEach((result) => {
if (result !== null) {
orgsWithApps.push(result);
}
});
return orgsWithApps;
};
export const getProjectEnvironmentVariables = (project: VercelApp): Record<string, string> => {
const envVars: Record<string, string> = {};
if (!project.envs) return envVars;
project.envs.forEach((env) => {
if (env.slug && env.type !== "gitBranch") {
const { id, slug } = env;
envVars[id] = slug;
}
});
return envVars;
};

View File

@@ -0,0 +1,58 @@
import z from "zod";
import { AppConnections } from "@app/lib/api-docs";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
BaseAppConnectionSchema,
GenericCreateAppConnectionFieldsSchema,
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { VercelConnectionMethod } from "./vercel-connection-enums";
export const VercelConnectionAccessTokenCredentialsSchema = z.object({
apiToken: z.string().trim().min(1, "API Token required").describe(AppConnections.CREDENTIALS.VERCEL.apiToken)
});
const BaseVercelConnectionSchema = BaseAppConnectionSchema.extend({
app: z.literal(AppConnection.Vercel)
});
export const VercelConnectionSchema = BaseVercelConnectionSchema.extend({
method: z.literal(VercelConnectionMethod.ApiToken),
credentials: VercelConnectionAccessTokenCredentialsSchema
});
export const SanitizedVercelConnectionSchema = z.discriminatedUnion("method", [
BaseVercelConnectionSchema.extend({
method: z.literal(VercelConnectionMethod.ApiToken),
credentials: VercelConnectionAccessTokenCredentialsSchema.pick({})
})
]);
export const ValidateVercelConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: z.literal(VercelConnectionMethod.ApiToken).describe(AppConnections.CREATE(AppConnection.Vercel).method),
credentials: VercelConnectionAccessTokenCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.Vercel).credentials
)
})
]);
export const CreateVercelConnectionSchema = ValidateVercelConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.Vercel)
);
export const UpdateVercelConnectionSchema = z
.object({
credentials: VercelConnectionAccessTokenCredentialsSchema.optional().describe(
AppConnections.UPDATE(AppConnection.Vercel).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Vercel));
export const VercelConnectionListItemSchema = z.object({
name: z.literal("Vercel"),
app: z.literal(AppConnection.Vercel),
methods: z.nativeEnum(VercelConnectionMethod).array()
});

View File

@@ -0,0 +1,29 @@
import { logger } from "@app/lib/logger";
import { OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import { listProjects as getVercelProjects } from "./vercel-connection-fns";
import { TVercelConnection } from "./vercel-connection-types";
type TGetAppConnectionFunc = (
app: AppConnection,
connectionId: string,
actor: OrgServiceActor
) => Promise<TVercelConnection>;
export const vercelConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
const listProjects = async (connectionId: string, actor: OrgServiceActor) => {
const appConnection = await getAppConnection(AppConnection.Vercel, connectionId, actor);
try {
const projects = await getVercelProjects(appConnection);
return projects;
} catch (error) {
logger.error(error, "Failed to establish connection with Vercel");
return [];
}
};
return {
listProjects
};
};

View File

@@ -0,0 +1,73 @@
import z from "zod";
import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import {
CreateVercelConnectionSchema,
ValidateVercelConnectionCredentialsSchema,
VercelConnectionSchema
} from "./vercel-connection-schemas";
export type TVercelConnection = z.infer<typeof VercelConnectionSchema>;
export type TVercelConnectionInput = z.infer<typeof CreateVercelConnectionSchema> & {
app: AppConnection.Vercel;
};
export type TValidateVercelConnectionCredentialsSchema = typeof ValidateVercelConnectionCredentialsSchema;
export type TVercelConnectionConfig = DiscriminativePick<TVercelConnectionInput, "method" | "app" | "credentials"> & {
orgId: string;
};
export type VercelTeam = {
id: string;
name: string;
slug: string;
};
export type VercelEnvironment = {
id: string;
slug: string;
type: string;
target?: string[];
gitBranch?: string;
createdAt?: number;
updatedAt?: number;
};
export type VercelAppMeta = {
githubCommitRef?: string;
githubCommitSha?: string;
githubCommitMessage?: string;
githubCommitAuthorName?: string;
};
export type VercelDeployment = {
id: string;
name: string;
url: string;
created: number;
meta?: VercelAppMeta;
target?: "production" | "preview" | "development";
};
export type VercelApp = {
name: string;
id: string;
envs?: VercelEnvironment[];
previewBranches?: string[];
};
export type VercelOrgWithApps = VercelTeam & {
apps: VercelApp[];
};
export type VercelUserResponse = {
user: {
id: string;
name: string;
username: string;
};
};

View File

@@ -21,6 +21,7 @@ import {
TCreateProjectIdentityDTO, TCreateProjectIdentityDTO,
TDeleteProjectIdentityDTO, TDeleteProjectIdentityDTO,
TGetProjectIdentityByIdentityIdDTO, TGetProjectIdentityByIdentityIdDTO,
TGetProjectIdentityByMembershipIdDTO,
TListProjectIdentityDTO, TListProjectIdentityDTO,
TUpdateProjectIdentityDTO TUpdateProjectIdentityDTO
} from "./identity-project-types"; } from "./identity-project-types";
@@ -370,11 +371,48 @@ export const identityProjectServiceFactory = ({
return identityMembership; return identityMembership;
}; };
const getProjectIdentityByMembershipId = async ({
identityMembershipId,
actor,
actorId,
actorAuthMethod,
actorOrgId
}: TGetProjectIdentityByMembershipIdDTO) => {
const membership = await identityProjectDAL.findOne({ id: identityMembershipId });
if (!membership) {
throw new NotFoundError({
message: `Project membership with ID '${identityMembershipId}' not found`
});
}
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: membership.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionIdentityActions.Read,
subject(ProjectPermissionSub.Identity, { identityId: membership.identityId })
);
const [identityMembership] = await identityProjectDAL.findByProjectId(membership.projectId, {
identityId: membership.identityId
});
return identityMembership;
};
return { return {
createProjectIdentity, createProjectIdentity,
updateProjectIdentity, updateProjectIdentity,
deleteProjectIdentity, deleteProjectIdentity,
listProjectIdentities, listProjectIdentities,
getProjectIdentityByIdentityId getProjectIdentityByIdentityId,
getProjectIdentityByMembershipId
}; };
}; };

View File

@@ -52,6 +52,10 @@ export type TGetProjectIdentityByIdentityIdDTO = {
identityId: string; identityId: string;
} & TProjectPermission; } & TProjectPermission;
export type TGetProjectIdentityByMembershipIdDTO = {
identityMembershipId: string;
} & Omit<TProjectPermission, "projectId">;
export enum ProjectIdentityOrderBy { export enum ProjectIdentityOrderBy {
Name = "name" Name = "name"
} }

View File

@@ -14,10 +14,15 @@ import {
TIdentityUniversalAuths, TIdentityUniversalAuths,
TOrgRoles TOrgRoles
} from "@app/db/schemas"; } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { BadRequestError, DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
import { buildKnexFilterForSearchResource } from "@app/lib/search-resource/db";
import { OrderByDirection } from "@app/lib/types"; import { OrderByDirection } from "@app/lib/types";
import { OrgIdentityOrderBy, TListOrgIdentitiesByOrgIdDTO } from "@app/services/identity/identity-types"; import {
OrgIdentityOrderBy,
TListOrgIdentitiesByOrgIdDTO,
TSearchOrgIdentitiesByOrgIdDAL
} from "@app/services/identity/identity-types";
import { buildAuthMethods } from "./identity-fns"; import { buildAuthMethods } from "./identity-fns";
@@ -195,7 +200,6 @@ export const identityOrgDALFactory = (db: TDbClient) => {
"paginatedIdentity.identityId", "paginatedIdentity.identityId",
`${TableName.IdentityJwtAuth}.identityId` `${TableName.IdentityJwtAuth}.identityId`
) )
.select( .select(
db.ref("id").withSchema("paginatedIdentity"), db.ref("id").withSchema("paginatedIdentity"),
db.ref("role").withSchema("paginatedIdentity"), db.ref("role").withSchema("paginatedIdentity"),
@@ -309,6 +313,214 @@ export const identityOrgDALFactory = (db: TDbClient) => {
} }
}; };
const searchIdentities = async (
{
limit,
offset = 0,
orderBy = OrgIdentityOrderBy.Name,
orderDirection = OrderByDirection.ASC,
searchFilter,
orgId
}: TSearchOrgIdentitiesByOrgIdDAL,
tx?: Knex
) => {
try {
const searchQuery = (tx || db.replicaNode())(TableName.IdentityOrgMembership)
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.IdentityOrgMembership}.identityId`)
.where(`${TableName.IdentityOrgMembership}.orgId`, orgId)
.leftJoin(TableName.OrgRoles, `${TableName.IdentityOrgMembership}.roleId`, `${TableName.OrgRoles}.id`)
.orderBy(`${TableName.Identity}.${orderBy}`, orderDirection)
.select(`${TableName.IdentityOrgMembership}.id`)
.select<{ id: string; total_count: string }>(
db.raw(
`count(${TableName.IdentityOrgMembership}."identityId") OVER(PARTITION BY ${TableName.IdentityOrgMembership}."orgId") as total_count`
)
)
.as("searchedIdentities");
if (searchFilter) {
buildKnexFilterForSearchResource(searchQuery, searchFilter, (attr) => {
switch (attr) {
case "role":
return [`${TableName.OrgRoles}.slug`, `${TableName.IdentityOrgMembership}.role`];
case "name":
return `${TableName.Identity}.name`;
default:
throw new BadRequestError({ message: `Invalid ${String(attr)} provided` });
}
});
}
if (limit) {
void searchQuery.offset(offset).limit(limit);
}
type TSubquery = Awaited<typeof searchQuery>;
const query = (tx || db.replicaNode())(TableName.IdentityOrgMembership)
.where(`${TableName.IdentityOrgMembership}.orgId`, orgId)
.join<TSubquery>(searchQuery, `${TableName.IdentityOrgMembership}.id`, "searchedIdentities.id")
.join(TableName.Identity, `${TableName.IdentityOrgMembership}.identityId`, `${TableName.Identity}.id`)
.leftJoin(TableName.OrgRoles, `${TableName.IdentityOrgMembership}.roleId`, `${TableName.OrgRoles}.id`)
.leftJoin(TableName.IdentityMetadata, (queryBuilder) => {
void queryBuilder
.on(`${TableName.IdentityOrgMembership}.identityId`, `${TableName.IdentityMetadata}.identityId`)
.andOn(`${TableName.IdentityOrgMembership}.orgId`, `${TableName.IdentityMetadata}.orgId`);
})
.leftJoin(
TableName.IdentityUniversalAuth,
`${TableName.IdentityOrgMembership}.identityId`,
`${TableName.IdentityUniversalAuth}.identityId`
)
.leftJoin(
TableName.IdentityGcpAuth,
`${TableName.IdentityOrgMembership}.identityId`,
`${TableName.IdentityGcpAuth}.identityId`
)
.leftJoin(
TableName.IdentityAwsAuth,
`${TableName.IdentityOrgMembership}.identityId`,
`${TableName.IdentityAwsAuth}.identityId`
)
.leftJoin(
TableName.IdentityKubernetesAuth,
`${TableName.IdentityOrgMembership}.identityId`,
`${TableName.IdentityKubernetesAuth}.identityId`
)
.leftJoin(
TableName.IdentityOidcAuth,
`${TableName.IdentityOrgMembership}.identityId`,
`${TableName.IdentityOidcAuth}.identityId`
)
.leftJoin(
TableName.IdentityAzureAuth,
`${TableName.IdentityOrgMembership}.identityId`,
`${TableName.IdentityAzureAuth}.identityId`
)
.leftJoin(
TableName.IdentityTokenAuth,
`${TableName.IdentityOrgMembership}.identityId`,
`${TableName.IdentityTokenAuth}.identityId`
)
.leftJoin(
TableName.IdentityJwtAuth,
`${TableName.IdentityOrgMembership}.identityId`,
`${TableName.IdentityJwtAuth}.identityId`
)
.select(
db.ref("id").withSchema(TableName.IdentityOrgMembership),
db.ref("total_count").withSchema("searchedIdentities"),
db.ref("role").withSchema(TableName.IdentityOrgMembership),
db.ref("roleId").withSchema(TableName.IdentityOrgMembership),
db.ref("orgId").withSchema(TableName.IdentityOrgMembership),
db.ref("createdAt").withSchema(TableName.IdentityOrgMembership),
db.ref("updatedAt").withSchema(TableName.IdentityOrgMembership),
db.ref("identityId").withSchema(TableName.IdentityOrgMembership).as("identityId"),
db.ref("name").withSchema(TableName.Identity).as("identityName"),
db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth),
db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth),
db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth),
db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth),
db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth),
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth),
db.ref("id").as("jwtId").withSchema(TableName.IdentityJwtAuth)
)
// cr stands for custom role
.select(db.ref("id").as("crId").withSchema(TableName.OrgRoles))
.select(db.ref("name").as("crName").withSchema(TableName.OrgRoles))
.select(db.ref("slug").as("crSlug").withSchema(TableName.OrgRoles))
.select(db.ref("description").as("crDescription").withSchema(TableName.OrgRoles))
.select(db.ref("permissions").as("crPermission").withSchema(TableName.OrgRoles))
.select(db.ref("permissions").as("crPermission").withSchema(TableName.OrgRoles))
.select(
db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"),
db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"),
db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue")
);
if (orderBy === OrgIdentityOrderBy.Name) {
void query.orderBy("identityName", orderDirection);
}
const docs = await query;
const formattedDocs = sqlNestRelationships({
data: docs,
key: "id",
parentMapper: ({
crId,
crDescription,
crSlug,
crPermission,
crName,
identityId,
identityName,
role,
roleId,
total_count,
id,
uaId,
awsId,
gcpId,
jwtId,
kubernetesId,
oidcId,
azureId,
tokenId,
createdAt,
updatedAt
}) => ({
role,
roleId,
identityId,
id,
total_count: total_count as string,
orgId,
createdAt,
updatedAt,
customRole: roleId
? {
id: crId,
name: crName,
slug: crSlug,
permissions: crPermission,
description: crDescription
}
: undefined,
identity: {
id: identityId,
name: identityName,
authMethods: buildAuthMethods({
uaId,
awsId,
gcpId,
kubernetesId,
oidcId,
azureId,
tokenId,
jwtId
})
}
}),
childrenMapper: [
{
key: "metadataId",
label: "metadata" as const,
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
id: metadataId,
key: metadataKey,
value: metadataValue
})
}
]
});
return { docs: formattedDocs, totalCount: Number(formattedDocs?.[0]?.total_count ?? 0) };
} catch (error) {
throw new DatabaseError({ error, name: "FindByOrgId" });
}
};
const countAllOrgIdentities = async ( const countAllOrgIdentities = async (
{ search, ...filter }: Partial<TIdentityOrgMemberships> & Pick<TListOrgIdentitiesByOrgIdDTO, "search">, { search, ...filter }: Partial<TIdentityOrgMemberships> & Pick<TListOrgIdentitiesByOrgIdDTO, "search">,
tx?: Knex tx?: Knex
@@ -331,5 +543,5 @@ export const identityOrgDALFactory = (db: TDbClient) => {
} }
}; };
return { ...identityOrgOrm, find, findOne, countAllOrgIdentities }; return { ...identityOrgOrm, find, findOne, countAllOrgIdentities, searchIdentities };
}; };

View File

@@ -21,6 +21,7 @@ import {
TGetIdentityByIdDTO, TGetIdentityByIdDTO,
TListOrgIdentitiesByOrgIdDTO, TListOrgIdentitiesByOrgIdDTO,
TListProjectIdentitiesByIdentityIdDTO, TListProjectIdentitiesByIdentityIdDTO,
TSearchOrgIdentitiesByOrgIdDTO,
TUpdateIdentityDTO TUpdateIdentityDTO
} from "./identity-types"; } from "./identity-types";
@@ -288,6 +289,33 @@ export const identityServiceFactory = ({
return { identityMemberships, totalCount }; return { identityMemberships, totalCount };
}; };
const searchOrgIdentities = async ({
orgId,
actor,
actorId,
actorAuthMethod,
actorOrgId,
limit,
offset,
orderBy,
orderDirection,
searchFilter = {}
}: TSearchOrgIdentitiesByOrgIdDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
const { totalCount, docs } = await identityOrgMembershipDAL.searchIdentities({
orgId,
limit,
offset,
orderBy,
orderDirection,
searchFilter
});
return { identityMemberships: docs, totalCount };
};
const listProjectIdentitiesByIdentityId = async ({ const listProjectIdentitiesByIdentityId = async ({
identityId, identityId,
actor, actor,
@@ -317,6 +345,7 @@ export const identityServiceFactory = ({
deleteIdentity, deleteIdentity,
listOrgIdentities, listOrgIdentities,
getIdentityById, getIdentityById,
searchOrgIdentities,
listProjectIdentitiesByIdentityId listProjectIdentitiesByIdentityId
}; };
}; };

View File

@@ -1,4 +1,5 @@
import { IPType } from "@app/lib/ip"; import { IPType } from "@app/lib/ip";
import { TSearchResourceOperator } from "@app/lib/search-resource/search";
import { OrderByDirection, TOrgPermission } from "@app/lib/types"; import { OrderByDirection, TOrgPermission } from "@app/lib/types";
export type TCreateIdentityDTO = { export type TCreateIdentityDTO = {
@@ -46,3 +47,17 @@ export enum OrgIdentityOrderBy {
Name = "name" Name = "name"
// Role = "role" // Role = "role"
} }
export type TSearchOrgIdentitiesByOrgIdDAL = {
limit?: number;
offset?: number;
orderBy?: OrgIdentityOrderBy;
orderDirection?: OrderByDirection;
orgId: string;
searchFilter?: Partial<{
name: Omit<TSearchResourceOperator, "number">;
role: Omit<TSearchResourceOperator, "number">;
}>;
};
export type TSearchOrgIdentitiesByOrgIdDTO = TSearchOrgIdentitiesByOrgIdDAL & TOrgPermission;

View File

@@ -7,7 +7,8 @@ export enum SecretSync {
AzureAppConfiguration = "azure-app-configuration", AzureAppConfiguration = "azure-app-configuration",
Databricks = "databricks", Databricks = "databricks",
Humanitec = "humanitec", Humanitec = "humanitec",
Camunda = "camunda" Camunda = "camunda",
Vercel = "vercel"
} }
export enum SecretSyncInitialSyncBehavior { export enum SecretSyncInitialSyncBehavior {

View File

@@ -27,6 +27,7 @@ import { GCP_SYNC_LIST_OPTION } from "./gcp";
import { GcpSyncFns } from "./gcp/gcp-sync-fns"; import { GcpSyncFns } from "./gcp/gcp-sync-fns";
import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec"; import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec";
import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns"; import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns";
import { VERCEL_SYNC_LIST_OPTION, VercelSyncFns } from "./vercel";
const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = { const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
[SecretSync.AWSParameterStore]: AWS_PARAMETER_STORE_SYNC_LIST_OPTION, [SecretSync.AWSParameterStore]: AWS_PARAMETER_STORE_SYNC_LIST_OPTION,
@@ -37,7 +38,8 @@ const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
[SecretSync.AzureAppConfiguration]: AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION, [SecretSync.AzureAppConfiguration]: AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION,
[SecretSync.Databricks]: DATABRICKS_SYNC_LIST_OPTION, [SecretSync.Databricks]: DATABRICKS_SYNC_LIST_OPTION,
[SecretSync.Humanitec]: HUMANITEC_SYNC_LIST_OPTION, [SecretSync.Humanitec]: HUMANITEC_SYNC_LIST_OPTION,
[SecretSync.Camunda]: CAMUNDA_SYNC_LIST_OPTION [SecretSync.Camunda]: CAMUNDA_SYNC_LIST_OPTION,
[SecretSync.Vercel]: VERCEL_SYNC_LIST_OPTION
}; };
export const listSecretSyncOptions = () => { export const listSecretSyncOptions = () => {
@@ -128,6 +130,8 @@ export const SecretSyncFns = {
appConnectionDAL, appConnectionDAL,
kmsService kmsService
}).syncSecrets(secretSync, secretMap); }).syncSecrets(secretSync, secretMap);
case SecretSync.Vercel:
return VercelSyncFns.syncSecrets(secretSync, secretMap);
default: default:
throw new Error( throw new Error(
`Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` `Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
@@ -177,6 +181,8 @@ export const SecretSyncFns = {
appConnectionDAL, appConnectionDAL,
kmsService kmsService
}).getSecrets(secretSync); }).getSecrets(secretSync);
case SecretSync.Vercel:
secretMap = await VercelSyncFns.getSecrets(secretSync);
break; break;
default: default:
throw new Error( throw new Error(
@@ -225,6 +231,8 @@ export const SecretSyncFns = {
appConnectionDAL, appConnectionDAL,
kmsService kmsService
}).removeSecrets(secretSync, secretMap); }).removeSecrets(secretSync, secretMap);
case SecretSync.Vercel:
return VercelSyncFns.removeSecrets(secretSync, secretMap);
default: default:
throw new Error( throw new Error(
`Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` `Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`

View File

@@ -10,7 +10,8 @@ export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
[SecretSync.AzureAppConfiguration]: "Azure App Configuration", [SecretSync.AzureAppConfiguration]: "Azure App Configuration",
[SecretSync.Databricks]: "Databricks", [SecretSync.Databricks]: "Databricks",
[SecretSync.Humanitec]: "Humanitec", [SecretSync.Humanitec]: "Humanitec",
[SecretSync.Camunda]: "Camunda" [SecretSync.Camunda]: "Camunda",
[SecretSync.Vercel]: "Vercel"
}; };
export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = { export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
@@ -22,5 +23,6 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
[SecretSync.AzureAppConfiguration]: AppConnection.AzureAppConfiguration, [SecretSync.AzureAppConfiguration]: AppConnection.AzureAppConfiguration,
[SecretSync.Databricks]: AppConnection.Databricks, [SecretSync.Databricks]: AppConnection.Databricks,
[SecretSync.Humanitec]: AppConnection.Humanitec, [SecretSync.Humanitec]: AppConnection.Humanitec,
[SecretSync.Camunda]: AppConnection.Camunda [SecretSync.Camunda]: AppConnection.Camunda,
[SecretSync.Vercel]: AppConnection.Vercel
}; };

View File

@@ -55,6 +55,7 @@ import {
THumanitecSyncListItem, THumanitecSyncListItem,
THumanitecSyncWithCredentials THumanitecSyncWithCredentials
} from "./humanitec"; } from "./humanitec";
import { TVercelSync, TVercelSyncInput, TVercelSyncListItem, TVercelSyncWithCredentials } from "./vercel";
export type TSecretSync = export type TSecretSync =
| TAwsParameterStoreSync | TAwsParameterStoreSync
@@ -65,7 +66,8 @@ export type TSecretSync =
| TAzureAppConfigurationSync | TAzureAppConfigurationSync
| TDatabricksSync | TDatabricksSync
| THumanitecSync | THumanitecSync
| TCamundaSync; | TCamundaSync
| TVercelSync;
export type TSecretSyncWithCredentials = export type TSecretSyncWithCredentials =
| TAwsParameterStoreSyncWithCredentials | TAwsParameterStoreSyncWithCredentials
@@ -76,7 +78,8 @@ export type TSecretSyncWithCredentials =
| TAzureAppConfigurationSyncWithCredentials | TAzureAppConfigurationSyncWithCredentials
| TDatabricksSyncWithCredentials | TDatabricksSyncWithCredentials
| THumanitecSyncWithCredentials | THumanitecSyncWithCredentials
| TCamundaSyncWithCredentials; | TCamundaSyncWithCredentials
| TVercelSyncWithCredentials;
export type TSecretSyncInput = export type TSecretSyncInput =
| TAwsParameterStoreSyncInput | TAwsParameterStoreSyncInput
@@ -87,7 +90,8 @@ export type TSecretSyncInput =
| TAzureAppConfigurationSyncInput | TAzureAppConfigurationSyncInput
| TDatabricksSyncInput | TDatabricksSyncInput
| THumanitecSyncInput | THumanitecSyncInput
| TCamundaSyncInput; | TCamundaSyncInput
| TVercelSyncInput;
export type TSecretSyncListItem = export type TSecretSyncListItem =
| TAwsParameterStoreSyncListItem | TAwsParameterStoreSyncListItem
@@ -98,7 +102,8 @@ export type TSecretSyncListItem =
| TAzureAppConfigurationSyncListItem | TAzureAppConfigurationSyncListItem
| TDatabricksSyncListItem | TDatabricksSyncListItem
| THumanitecSyncListItem | THumanitecSyncListItem
| TCamundaSyncListItem; | TCamundaSyncListItem
| TVercelSyncListItem;
export type TSyncOptionsConfig = { export type TSyncOptionsConfig = {
canImportSecrets: boolean; canImportSecrets: boolean;

View File

@@ -0,0 +1,5 @@
export * from "./vercel-sync-constants";
export * from "./vercel-sync-enums";
export * from "./vercel-sync-fns";
export * from "./vercel-sync-schemas";
export * from "./vercel-sync-types";

View File

@@ -0,0 +1,10 @@
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types";
export const VERCEL_SYNC_LIST_OPTION: TSecretSyncListItem = {
name: "Vercel",
destination: SecretSync.Vercel,
connection: AppConnection.Vercel,
canImportSecrets: true
};

View File

@@ -0,0 +1,12 @@
export enum VercelSyncScope {
Application = "application",
Environment = "environment"
}
export const VercelEnvironmentType = {
Development: "development",
Preview: "preview",
Production: "production"
} as const;
export type VercelEnvironment = (typeof VercelEnvironmentType)[keyof typeof VercelEnvironmentType];

View File

@@ -0,0 +1,313 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { request } from "@app/lib/config/request";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
import { VercelEnvironmentType } from "./vercel-sync-enums";
import { DefaultVercelEnvType, TVercelSyncWithCredentials, VercelApiSecret } from "./vercel-sync-types";
function isVercelDefaultEnvType(value: string): value is DefaultVercelEnvType {
return Object.values(VercelEnvironmentType).map(String).includes(value);
}
const MAX_RETRIES = 5;
const sleep = async () =>
new Promise((resolve) => {
setTimeout(resolve, 60000);
});
const getVercelSecretsWithRetries = async (
secretSync: TVercelSyncWithCredentials,
attempt = 0
): Promise<VercelApiSecret[]> => {
const {
destinationConfig,
connection: {
credentials: { apiToken }
}
} = secretSync;
const params: { [key: string]: string } = {
decrypt: "true",
...(destinationConfig.branch ? { gitBranch: destinationConfig.branch } : {})
};
try {
const { data } = await request.get<{ envs: VercelApiSecret[] }>(
`${IntegrationUrls.VERCEL_API_URL}/v9/projects/${destinationConfig.app}/env?teamId=${destinationConfig.teamId}`,
{
params,
headers: {
Authorization: `Bearer ${apiToken}`,
"Accept-Encoding": "application/json"
}
}
);
return data.envs;
} catch (error) {
if ((error as { response: { status: number } }).response.status === 429 && attempt < MAX_RETRIES) {
await sleep();
return await getVercelSecretsWithRetries(secretSync, attempt + 1);
}
throw error;
}
};
const getDecryptedVercelSecret = async (
secretSync: TVercelSyncWithCredentials,
secret: VercelApiSecret,
attempt = 0
): Promise<VercelApiSecret> => {
const {
destinationConfig,
connection: {
credentials: { apiToken }
}
} = secretSync;
const params: { [key: string]: string } = {
decrypt: "true",
...(destinationConfig.branch ? { gitBranch: destinationConfig.branch } : {})
};
try {
const { data: decryptedSecret } = await request.get(
`${IntegrationUrls.VERCEL_API_URL}/v9/projects/${destinationConfig.app}/env/${secret.id}?teamId=${destinationConfig.teamId}`,
{
params,
headers: {
Authorization: `Bearer ${apiToken}`,
"Accept-Encoding": "application/json"
}
}
);
return decryptedSecret as VercelApiSecret;
} catch (error) {
if ((error as { response: { status: number } }).response.status === 429 && attempt < MAX_RETRIES) {
await sleep();
return await getDecryptedVercelSecret(secretSync, secret, attempt + 1);
}
throw error;
}
};
const getVercelSecrets = async (secretSync: TVercelSyncWithCredentials): Promise<VercelApiSecret[]> => {
const { destinationConfig } = secretSync;
const secrets = await getVercelSecretsWithRetries(secretSync);
const filteredSecrets = secrets.filter((secret) => {
if (!isVercelDefaultEnvType(destinationConfig.env)) {
if (secret.customEnvironmentIds?.includes(destinationConfig.env)) {
return true;
}
return false;
}
if (secret.target.includes(destinationConfig.env)) {
// If it's preview environment with a branch specified
if (
destinationConfig.env === VercelEnvironmentType.Preview &&
destinationConfig.branch &&
secret.gitBranch &&
secret.gitBranch !== destinationConfig.branch
) {
return false;
}
return true;
}
return false;
});
// For secrets of type "encrypted", we need to get their decrypted value
const secretsWithValues = await Promise.all(
filteredSecrets.map(async (secret) => {
if (secret.type === "encrypted") {
const decryptedSecret = await getDecryptedVercelSecret(secretSync, secret);
return decryptedSecret;
}
return secret;
})
);
return secretsWithValues;
};
const deleteSecret = async (
secretSync: TVercelSyncWithCredentials,
vercelSecret: VercelApiSecret,
attempt = 0
): Promise<void> => {
const {
destinationConfig,
connection: {
credentials: { apiToken }
}
} = secretSync;
try {
await request.delete(
`${IntegrationUrls.VERCEL_API_URL}/v9/projects/${destinationConfig.app}/env/${vercelSecret.id}?teamId=${destinationConfig.teamId}`,
{
headers: {
Authorization: `Bearer ${apiToken}`,
"Accept-Encoding": "application/json"
}
}
);
} catch (error) {
if ((error as { response: { status: number } }).response.status === 429 && attempt < MAX_RETRIES) {
await sleep();
return await deleteSecret(secretSync, vercelSecret, attempt + 1);
}
throw new SecretSyncError({
error,
secretKey: vercelSecret.key
});
}
};
const createSecret = async (
secretSync: TVercelSyncWithCredentials,
secretMap: TSecretMap,
key: string,
attempt = 0
): Promise<void> => {
try {
const {
destinationConfig,
connection: {
credentials: { apiToken }
}
} = secretSync;
await request.post(
`${IntegrationUrls.VERCEL_API_URL}/v10/projects/${destinationConfig.app}/env?teamId=${destinationConfig.teamId}`,
{
key,
value: secretMap[key].value,
type: "encrypted",
target: isVercelDefaultEnvType(destinationConfig.env) ? [destinationConfig.env] : [],
customEnvironmentIds: !isVercelDefaultEnvType(destinationConfig.env) ? [destinationConfig.env] : [],
...(destinationConfig.env === VercelEnvironmentType.Preview && destinationConfig.branch
? { gitBranch: destinationConfig.branch }
: {})
},
{
headers: {
Authorization: `Bearer ${apiToken}`,
"Accept-Encoding": "application/json"
}
}
);
} catch (error) {
if ((error as { response: { status: number } }).response.status === 429 && attempt < MAX_RETRIES) {
await sleep();
return await createSecret(secretSync, secretMap, key, attempt + 1);
}
throw new SecretSyncError({
error,
secretKey: key
});
}
};
const updateSecret = async (
secretSync: TVercelSyncWithCredentials,
secretMap: TSecretMap,
vercelSecret: VercelApiSecret,
attempt = 0
): Promise<void> => {
try {
const {
destinationConfig,
connection: {
credentials: { apiToken }
}
} = secretSync;
let target = [...vercelSecret.target];
if (isVercelDefaultEnvType(destinationConfig.env) && !vercelSecret.target.includes(destinationConfig.env)) {
target = [...target, destinationConfig.env];
}
let customEnvironmentIds = [...(vercelSecret.customEnvironmentIds || [])];
if (
!isVercelDefaultEnvType(destinationConfig.env) &&
!vercelSecret.customEnvironmentIds?.includes(destinationConfig.env)
) {
customEnvironmentIds = [...customEnvironmentIds, destinationConfig.env];
}
await request.patch(
`${IntegrationUrls.VERCEL_API_URL}/v9/projects/${destinationConfig.app}/env/${vercelSecret.id}?teamId=${destinationConfig.teamId}`,
{
...(vercelSecret.type !== "sensitive" && { key: vercelSecret.key }),
value: secretMap[vercelSecret.key].value,
type: vercelSecret.type,
target,
customEnvironmentIds,
...(destinationConfig.env === VercelEnvironmentType.Preview && destinationConfig.branch
? { gitBranch: destinationConfig.branch }
: {})
},
{
headers: {
Authorization: `Bearer ${apiToken}`,
"Accept-Encoding": "application/json"
}
}
);
} catch (error) {
if ((error as { response: { status: number } }).response.status === 429 && attempt < MAX_RETRIES) {
await sleep();
return await updateSecret(secretSync, secretMap, vercelSecret, attempt + 1);
}
throw new SecretSyncError({
error,
secretKey: vercelSecret.key
});
}
};
export const VercelSyncFns = {
syncSecrets: async (secretSync: TVercelSyncWithCredentials, secretMap: TSecretMap) => {
const vercelSecrets = await getVercelSecrets(secretSync);
const vercelSecretsMap = new Map(vercelSecrets.map((s) => [s.key, s]));
// Create or update secrets
for await (const key of Object.keys(secretMap)) {
const existingSecret = vercelSecretsMap.get(key);
if (!existingSecret) {
await createSecret(secretSync, secretMap, key);
} else if (existingSecret.value !== secretMap[key].value) {
await updateSecret(secretSync, secretMap, existingSecret);
}
}
// Delete secrets if disableSecretDeletion is not set
if (secretSync.syncOptions.disableSecretDeletion) return;
for await (const vercelSecret of vercelSecrets) {
if (!secretMap[vercelSecret.key]) {
await deleteSecret(secretSync, vercelSecret);
}
}
},
getSecrets: async (secretSync: TVercelSyncWithCredentials): Promise<TSecretMap> => {
const vercelSecrets = await getVercelSecrets(secretSync);
return Object.fromEntries(vercelSecrets.map((s) => [s.key, { value: s.value ?? "" }]));
},
removeSecrets: async (secretSync: TVercelSyncWithCredentials, secretMap: TSecretMap) => {
const vercelSecrets = await getVercelSecrets(secretSync);
for await (const vercelSecret of vercelSecrets) {
if (vercelSecret.key in secretMap) {
await deleteSecret(secretSync, vercelSecret);
}
}
}
};

View File

@@ -0,0 +1,49 @@
import { z } from "zod";
import { SecretSyncs } from "@app/lib/api-docs";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import {
BaseSecretSyncSchema,
GenericCreateSecretSyncFieldsSchema,
GenericUpdateSecretSyncFieldsSchema
} from "@app/services/secret-sync/secret-sync-schemas";
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
import { VercelEnvironmentType } from "./vercel-sync-enums";
const VercelSyncDestinationConfigSchema = z.object({
app: z.string().min(1, "App ID is required").describe(SecretSyncs.DESTINATION_CONFIG.VERCEL.app),
appName: z.string().min(1, "App Name is required").describe(SecretSyncs.DESTINATION_CONFIG.VERCEL.appName),
env: z.nativeEnum(VercelEnvironmentType).or(z.string()).describe(SecretSyncs.DESTINATION_CONFIG.VERCEL.env),
branch: z.string().optional().describe(SecretSyncs.DESTINATION_CONFIG.VERCEL.branch),
teamId: z.string().describe(SecretSyncs.DESTINATION_CONFIG.VERCEL.teamId)
});
const VercelSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
export const VercelSyncSchema = BaseSecretSyncSchema(SecretSync.Vercel, VercelSyncOptionsConfig).extend({
destination: z.literal(SecretSync.Vercel),
destinationConfig: VercelSyncDestinationConfigSchema
});
export const CreateVercelSyncSchema = GenericCreateSecretSyncFieldsSchema(
SecretSync.Vercel,
VercelSyncOptionsConfig
).extend({
destinationConfig: VercelSyncDestinationConfigSchema
});
export const UpdateVercelSyncSchema = GenericUpdateSecretSyncFieldsSchema(
SecretSync.Vercel,
VercelSyncOptionsConfig
).extend({
destinationConfig: VercelSyncDestinationConfigSchema.optional()
});
export const VercelSyncListItemSchema = z.object({
name: z.literal("Vercel"),
connection: z.literal(AppConnection.Vercel),
destination: z.literal(SecretSync.Vercel),
canImportSecrets: z.literal(true)
});

View File

@@ -0,0 +1,40 @@
import z from "zod";
import { TVercelConnection } from "@app/services/app-connection/vercel";
import { VercelEnvironmentType } from "./vercel-sync-enums";
import { CreateVercelSyncSchema, VercelSyncListItemSchema, VercelSyncSchema } from "./vercel-sync-schemas";
export type TVercelSyncListItem = z.infer<typeof VercelSyncListItemSchema>;
export type TVercelSync = z.infer<typeof VercelSyncSchema>;
export type TVercelSyncInput = z.infer<typeof CreateVercelSyncSchema>;
export type TVercelSyncWithCredentials = TVercelSync & {
connection: TVercelConnection;
};
export type VercelSecret = {
description: string;
is_secret: boolean;
key: string;
source: "app" | "env";
value: string;
};
export interface VercelApiSecret {
id: string;
key: string;
value: string;
type: string;
target: string[];
customEnvironmentIds?: string[];
gitBranch?: string;
createdAt?: number;
updatedAt?: number;
configurationId?: string;
system?: boolean;
}
export type DefaultVercelEnvType = (typeof VercelEnvironmentType)[keyof typeof VercelEnvironmentType];

View File

@@ -50,6 +50,7 @@ func init() {
config.INFISICAL_URL = util.AppendAPIEndpoint(config.INFISICAL_URL) config.INFISICAL_URL = util.AppendAPIEndpoint(config.INFISICAL_URL)
// util.DisplayAptInstallationChangeBanner(silent)
if !util.IsRunningInDocker() && !silent { if !util.IsRunningInDocker() && !silent {
util.CheckForUpdate() util.CheckForUpdate()
} }

View File

@@ -53,6 +53,25 @@ func CheckForUpdate() {
} }
} }
func DisplayAptInstallationChangeBanner(isSilent bool) {
if isSilent {
return
}
if runtime.GOOS == "linux" {
_, err := exec.LookPath("apt-get")
isApt := err == nil
if isApt {
yellow := color.New(color.FgYellow).SprintFunc()
msg := fmt.Sprintf("%s",
yellow("Update Required: Your current package installation script is outdated and will no longer receive updates.\nPlease update to the new installation script which can be found here https://infisical.com/docs/cli/overview#installation debian section\n"),
)
fmt.Fprintln(os.Stderr, msg)
}
}
}
func getLatestTag(repoOwner string, repoName string) (string, string, error) { func getLatestTag(repoOwner string, repoName string) (string, string, error) {
url := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", repoOwner, repoName) url := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", repoOwner, repoName)
resp, err := http.Get(url) resp, err := http.Get(url)

551
cli/scripts/setup.deb.sh Normal file
View File

@@ -0,0 +1,551 @@
#!/usr/bin/env bash
#
# The core commands execute start from the "MAIN" section below.
#
test -z "$BASH_SOURCE" && {
self="sudo -E bash"
prefix="<curl command> |"
} || {
self=$(readlink -f ${BASH_SOURCE:-$0})
prefix=""
}
tmp_log=$(mktemp .s3_setup_XXXXXXXXX)
# Environment variables that can be set
PKG_URL=${PKG_URL:-"https://artifacts-cli.infisical.com"}
PKG_PATH=${PKG_PATH:-"deb"}
PACKAGE_NAME=${PACKAGE_NAME:-"infisical"}
GPG_KEY_URL=${GPG_KEY_URL:-"${PKG_URL}/infisical.gpg"}
colours=$(tput colors 2>/dev/null || echo "256")
no_colour="\e[39;49m"
green_colour="\e[32m"
red_colour="\e[41;97m"
bold="\e[1m"
reset="\e[0m"
use_colours=$(test -n "$colours" && test $colours -ge 8 && echo "yes")
test "$use_colours" == "yes" || {
no_colour=""
green_colour=""
red_colour=""
bold=""
reset=""
}
example_name="Ubuntu/Focal (20.04)"
example_distro="ubuntu"
example_codename="focal"
example_version="20.04"
function echo_helptext {
local help_text="$*"
echo " ^^^^: ... $help_text"
}
function die {
local text="$@"
test ! -z "$text" && {
echo_helptext "$text" 1>&2
}
local prefix="${red_colour} !!!!${no_colour}"
echo -e "$prefix: Oh no, your setup failed! :-( ... But we might be able to help. :-)"
echo -e "$prefix: "
echo -e "$prefix: ${bold}Please check your S3 bucket configuration and try again.${reset}"
echo -e "$prefix: "
test -f "$tmp_log" && {
local n=20
echo -e "$prefix: Last $n log lines from $tmp_log (might not be errors, nor even relevant):"
echo -e "$prefix:"
check_tool_silent "xargs" && {
check_tool_silent "fmt" && {
tail -n $n $tmp_log | fmt -t | xargs -Ilog echo -e "$prefix: > log"
} || {
tail -n $n $tmp_log | xargs -Ilog echo -e "$prefix: > log"
}
} || {
echo
tail -n $n $tmp_log
}
}
exit 1
}
function echo_colour {
local colour="${1:-"no"}_colour"; shift
echo -e "${!colour}$@${no_colour}"
}
function echo_green_or_red {
local rc="$1"
local good="${2:-YES}"
local bad="${3:-NO}"
test "$rc" -eq 0 && {
echo_colour "green" "$good"
} || {
echo_colour "red" "$bad"
}
return $rc
}
function echo_clearline {
local rc="$?"
echo -e -n "\033[1K\r"
return $rc
}
function echo_status {
local rc="$1"
local good="$2"
local bad="$3"
local text="$4"
local help_text="$5"
local newline=$(test "$6" != "no" && echo "\n" || echo "")
local status_text=$(echo_green_or_red "$rc" "$good" "$bad")
echo_clearline
local width=$(test "$use_colours" == "yes" && echo "16" || echo "5")
printf "%${width}s %s${newline}" "${status_text}:" "$text"
test $rc -ne 0 && test ! -z "$help_text" && {
echo_helptext "$help_text"
echo
}
return $rc
}
function echo_running {
local rc=$?
local text="$1"
echo_status 0 " RUN" " RUN" "$text" "" "no"
return $rc
}
function echo_okfail_rc {
local rc=$1
local text="$2"
local help_text="$3"
echo_clearline
echo_status $rc " OK" " NOPE" "$text" "$help_text"
return $rc
}
function echo_okfail {
echo_okfail_rc $? "$@"
return $?
}
function check_tool_silent {
local tool=${1}
command -v $tool &>/dev/null || which $tool &>/dev/null
return $?
}
function check_tool {
local tool=${1}
local optional=${2:-false}
local required_text="optional"
if ! $optional; then required_text="required"; fi
local text="Checking for $required_text executable '$tool' ..."
echo_running "$text"
check_tool_silent "$tool"
echo_okfail "$text" || {
if ! $optional; then
die "$tool is not installed, but is required by this script."
fi
return 1
}
return 0
}
function cleanup {
echo
rm -rf $tmp_log
}
function shutdown {
echo_colour "red" " !!!!: Operation cancelled by user!"
exit 2
}
function check_os {
test ! -z "$distro" && test ! -z "${version}${codename}"
return $?
}
function detect_os_system {
check_os && return 0
echo_running "$text"
local text="Detecting your OS distribution and release using system methods ..."
local tool_rc=1
test -f '/etc/os-release' && {
. /etc/os-release
distro=${distro:-$ID}
codename=${codename:-$VERSION_CODENAME}
codename=${codename:-$(echo $VERSION | cut -d '(' -f 2 | cut -d ')' -f 1)}
version=${version:-$VERSION_ID}
test -z "${version}${codename}" && test -f '/etc/debian_version' && {
# Workaround for Debian unstable releases; get the codename from debian_version
codename=$(cat /etc/debian_version | cut -d '/' -f1)
}
tool_rc=0
}
check_os
local rc=$?
echo_okfail_rc $rc "$text"
test $tool_rc -eq 0 && {
report_os_expanded
}
return $rc
}
function report_os_attribute {
local name=$1
local value=$2
local coloured=""
echo -n "$name="
test -z "$value" && {
echo -e -n "${red_colour}<empty>${no_colour} "
} || {
echo -e -n "${green_colour}${value}${no_colour} "
}
}
function report_os_expanded {
echo_helptext "Detected/provided for your OS/distribution, version and architecture:"
echo " >>>>:"
report_os_values
}
function report_os_values {
echo -n " >>>>: ... "
report_os_attribute "distro" $distro
report_os_attribute "codename" "stable (fixed)"
report_os_attribute "arch" $arch
echo
echo " >>>>:"
}
function detect_os_legacy_python {
check_os && return 0
local text="Detecting your OS distribution and release using legacy python ..."
echo_running "$text"
IFS='' read -r -d '' script <<-'EOF'
from __future__ import unicode_literals, print_function
import platform;
info = platform.linux_distribution() or ('', '', '');
for key, value in zip(('distro', 'version', 'codename'), info):
print("local guess_%s=\"%s\"\n" % (key, value.lower().replace(' ', '')));
EOF
local tool_rc=1
check_tool_silent "python" && {
eval $(python -c "$script")
distro=${distro:-$guess_distro}
codename=${codename:-$guess_codename}
version=${version:-$guess_version}
tool_rc=$?
}
check_os
local rc=$?
echo_okfail_rc $rc "$text"
check_tool_silent "python" || {
echo_helptext "Python isn't available, so skipping detection method (hint: install python)"
}
test $tool_rc -eq 0 && {
report_os
}
return $rc
}
function detect_os_modern_python {
check_os && return 0
check_tool_silent "python" && {
local text="Ensuring python-pip is installed ..."
echo_running "$text"
check_tool_silent "pip"
echo_okfail "$text" || {
local text="Checking if pip can be bootstrapped without get-pip ..."
echo_running "$text"
python -m ensurepip --default-pip &>$tmp_log
echo_okfail "$text" || {
local text="Installing pip via get-pip bootstrap ..."
echo_running "$text"
curl -1sLf https://bootstrap.pypa.io/get-pip.py 2>$tmp/log | python &>$tmp_log
echo_okfail "$text" || die "Failed to install pip!"
}
}
local text="Installing 'distro' python library ..."
echo_running "$text"
python -c 'import distro' &>$tmp_log || python -m pip install distro &>$tmp_log
echo_okfail "$text" || die "Failed to install required 'distro' python library!"
}
IFS='' read -r -d '' script <<-'EOF'
from __future__ import unicode_literals, print_function
import distro;
info = distro.linux_distribution(full_distribution_name=False) or ('', '', '');
for key, value in zip(('distro', 'version', 'codename'), info):
print("local guess_%s=\"%s\"\n" % (key, value.lower().replace(' ', '')));
EOF
local text="Detecting your OS distribution and release using modern python ..."
echo_running "$text"
local tool_rc=1
check_tool_silent "python" && {
eval $(python -c "$script")
distro=${distro:-$guess_distro}
codename=${codename:-$guess_codename}
version=${version:-$guess_version}
tool_rc=$?
}
check_os
local rc=$?
echo_okfail_rc $rc "$text"
check_tool_silent "python" || {
echo_helptext "Python isn't available, so skipping detection method (hint: install python)"
}
test $tool_rc -eq 0 && {
report_os_expanded
}
return $rc
}
function detect_os {
# Backwards compat for old distribution parameter names
distro=${distro:-$os}
# Always use "stable" as the codename
codename="stable"
arch=${arch:-$(arch || uname -m)}
# Only detect OS if not manually specified
if [ -z "$distro" ]; then
detect_os_system ||
detect_os_legacy_python ||
detect_os_modern_python
fi
# Always ensure we have a distro
(test -z "$distro") && {
echo_okfail_rc "1" "Unable to detect your OS distribution!"
cat <<EOF
>>>>:
>>>>: The 'distro' value is required. Without it, the install script
>>>>: cannot retrieve the correct configuration for this system.
>>>>:
>>>>: You can force this script to use a particular value by specifying distro
>>>>: via environment variable. E.g., to specify a distro
>>>>: such as $example_name, use the following:
>>>>:
>>>>: $prefix distro=$example_distro $self
>>>>:
EOF
die
}
}
function create_repo_config {
if [ -z "$PKG_PATH" ]; then
repo_url="${PKG_URL}"
else
repo_url="${PKG_URL}/${PKG_PATH}"
fi
# Create configuration with GPG key verification
local gpg_keyring_path="/usr/share/keyrings/${PACKAGE_NAME}-archive-keyring.gpg"
local apt_conf=$(cat <<EOF
deb [arch=$(dpkg --print-architecture) signed-by=${gpg_keyring_path}] ${repo_url} stable main
EOF
)
echo "$apt_conf"
return 0
}
function check_gpg_key {
local text="Checking if GPG key is accessible at ${GPG_KEY_URL} ..."
echo_running "$text"
local code="$(curl -1IsL -w "%{http_code}\\n" "$GPG_KEY_URL" -o /dev/null --connect-timeout 15 --max-time 60)"
test "$code" == "200" && {
echo_okfail_rc 0 "$text"
return 0
} || {
echo_okfail_rc 1 "$text"
echo_helptext "Failed to access the GPG key. Please check that it exists in your S3 bucket."
cat <<EOF
>>>>:
>>>>: It looks like we can't access the GPG key at ${GPG_KEY_URL}
>>>>:
EOF
die
}
}
function check_dpkg_tool {
local tool=${1}
local required=${2:-true}
local install=${3:-true}
local text="Checking for apt dependency '$tool' ..."
echo_running "$text"
dpkg -l | grep "$tool\>" &>$tmp_log
echo_okfail "$text" || {
if $install; then
test "$apt_updated" == "yes" || update_apt
local text="Attempting to install '$tool' ..."
echo_running "$text"
apt-get install -y "$tool" &>$tmp_log
echo_okfail "$text" || {
if $required; then
die "Could not install '$tool', check your permissions, etc."
fi
}
else {
if $required; then
die "$tool is not installed, but is required by this script."
fi
}
fi
}
return 0
}
function update_apt {
local text="Updating apt repository metadata cache ..."
local tmp_log=$(mktemp .s3_deb_output_XXXXXXXXX.log)
echo_running "$text"
apt-get update &>$tmp_log
echo_okfail "$text" || {
echo_colour "red" "Failed to update via apt-get update"
cat $tmp_log
rm -rf $tmp_log
die "Failed to update via apt-get update - Context above (maybe no packages?)."
}
rm -rf $tmp_log
apt_updated="yes"
}
function install_apt_prereqs {
# Debian-archive-keyring has to be installed for apt-transport-https.
test "${distro}" == "debian" && {
check_dpkg_tool "debian-keyring"
check_dpkg_tool "debian-archive-keyring"
}
check_dpkg_tool "apt-transport-https"
check_dpkg_tool "ca-certificates" false
check_dpkg_tool "gnupg"
}
function import_gpg_key {
local text="Importing '$PACKAGE_NAME' repository GPG key from S3 ..."
echo_running "$text"
local gpg_keyring_path="/usr/share/keyrings/${PACKAGE_NAME}-archive-keyring.gpg"
# Check if GPG key is accessible
check_gpg_key
# Download and import GPG key
curl -1sLf "${GPG_KEY_URL}" | gpg --dearmor > $gpg_keyring_path
chmod 644 $gpg_keyring_path
# Check for older apt versions that don't support signed-by
local signed_by_version="1.1"
local detected_version=$(dpkg -s apt | grep Version | cut -d' ' -f2)
if [ "$(printf "%s\n" $detected_version $signed_by_version | sort -V | head -n 1)" != "$signed_by_version" ]; then
echo_helptext "Detected older apt version without signed-by support. Copying key to trusted.gpg.d."
cp ${gpg_keyring_path} /etc/apt/trusted.gpg.d/${PACKAGE_NAME}.gpg
chmod 644 /etc/apt/trusted.gpg.d/${PACKAGE_NAME}.gpg
fi
echo_okfail "$text" || die "Could not import the GPG key for this repository"
}
function setup_repository {
local repo_path="/etc/apt/sources.list.d/${PACKAGE_NAME}.list"
local text="Installing '$PACKAGE_NAME' repository via apt ..."
echo_running "$text"
create_repo_config > "$repo_path"
chmod 644 $repo_path
echo_okfail "$text" || die "Could not install the repository, do you have permissions?"
}
function usage () {
cat <<EOF
Usage: $self [opts]
-h Displays this usage text.
-i Ignore repository setup errors during setup and
continue with install. This will leave the repository config
in place rather than removing it upon errors.
-p Package name to use for repository setup (default: ${PACKAGE_NAME})
-k GPG key URL (default: ${GPG_KEY_URL})
EOF
exit 0
}
trap cleanup EXIT
trap shutdown INT
ignore_errors=1
apt_updated="no"
while getopts ":ihp:b:s:k:" OPT; do
case $OPT in
i) ignore_errors=0 ;;
h) usage ;;
p) PACKAGE_NAME=$OPTARG ;;
b) PKG_URL=$OPTARG ;;
s) PKG_PATH=$OPTARG ;;
k) GPG_KEY_URL=$OPTARG ;;
\?) usage ;;
esac
done
shift $(($OPTIND - 1))
#
# MAIN
#
echo "Executing the setup script for the '$PACKAGE_NAME' S3 repository ..."
echo
check_tool "curl"
check_tool "apt-get"
detect_os
install_apt_prereqs
import_gpg_key
setup_repository
update_apt
echo_okfail_rc "0" "The repository has been installed successfully - You're ready to rock!"
echo
echo "You can now install the package with: apt install $PACKAGE_NAME"

View File

@@ -4,12 +4,18 @@ for i in *.apk; do
cloudsmith push alpine --republish infisical/infisical-cli/alpine/any-version $i cloudsmith push alpine --republish infisical/infisical-cli/alpine/any-version $i
done done
# for i in *.deb; do
# [ -f "$i" ] || break
# cloudsmith push deb --republish infisical/infisical-cli/any-distro/any-version $i
# done
for i in *.deb; do for i in *.deb; do
[ -f "$i" ] || break [ -f "$i" ] || break
cloudsmith push deb --republish infisical/infisical-cli/any-distro/any-version $i deb-s3 upload --bucket=$INFISICAL_CLI_S3_BUCKET --prefix=deb --visibility=private --sign=$INFISICAL_CLI_REPO_SIGNING_KEY_ID --preserve-versions $i
done done
for i in *.rpm; do for i in *.rpm; do
[ -f "$i" ] || break [ -f "$i" ] || break
cloudsmith push rpm --republish infisical/infisical-cli/any-distro/any-version $i cloudsmith push rpm --republish infisical/infisical-cli/any-distro/any-version $i
done done

View File

@@ -0,0 +1,4 @@
---
title: "Available"
openapi: "GET /api/v1/app-connections/vercel/available"
---

View File

@@ -0,0 +1,9 @@
---
title: "Create"
openapi: "POST /api/v1/app-connections/vercel"
---
<Note>
Check out the configuration docs for [Vercel Connections](/integrations/app-connections/vercel) to learn how to obtain
the required credentials.
</Note>

View File

@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/app-connections/vercel/{connectionId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v1/app-connections/vercel/{connectionId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v1/app-connections/vercel/connection-name/{connectionName}"
---

View File

@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v1/app-connections/vercel"
---

View File

@@ -0,0 +1,9 @@
---
title: "Update"
openapi: "PATCH /api/v1/app-connections/vercel/{connectionId}"
---
<Note>
Check out the configuration docs for [Vercel Connections](/integrations/app-connections/vercel) to learn how to obtain
the required credentials.
</Note>

View File

@@ -0,0 +1,4 @@
---
title: "Search"
openapi: "POST /api/v1/identities/search"
---

View File

@@ -0,0 +1,4 @@
---
title: "Create"
openapi: "POST /api/v1/secret-syncs/vercel"
---

View File

@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/secret-syncs/vercel/{syncId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v1/secret-syncs/vercel/{syncId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v1/secret-syncs/vercel/sync-name/{syncName}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Import Secrets"
openapi: "POST /api/v1/secret-syncs/vercel/{syncId}/import-secrets"
---

View File

@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v1/secret-syncs/vercel"
---

View File

@@ -0,0 +1,4 @@
---
title: "Remove Secrets"
openapi: "POST /api/v1/secret-syncs/vercel/{syncId}/remove-secrets"
---

View File

@@ -0,0 +1,4 @@
---
title: "Sync Secrets"
openapi: "POST /api/v1/secret-syncs/vercel/{syncId}/sync-secrets"
---

View File

@@ -0,0 +1,4 @@
---
title: "Update"
openapi: "PATCH /api/v1/secret-syncs/vercel/{syncId}"
---

View File

@@ -4,6 +4,34 @@ title: "Changelog"
The changelog below reflects new product developments and updates on a monthly basis. The changelog below reflects new product developments and updates on a monthly basis.
## March 2025
- Released [Infisical Gateway](https://infisical.com/docs/documentation/platform/gateways/overview) for secure access to private resources without needing direct inbound connections to private networks.
- Enhanced [Terraform](https://infisical.com/docs/integrations/frameworks/terraform#terraform) capabilities with token authentication, ability to import existing Infisical secrets as resources, and support for project templates.
- Self-hosted improvements: Usage and billing visibility for enabled features, ability to delete users, and support for multiple super admins.
- UI and UX updates: Improved secret import interface on the overview page, password reset without backup PDF.
- CLI enhancements: Various improvements including multiline secret support and ability to pass headers.
- Kubernetes operator updates: Auto-reloading for DaemonSets and StatefulSets (previously only Deployments), added support for ConfigMaps.
- Implemented powerful [Access Control](https://infisical.com/docs/documentation/platform/access-controls/overview#access-controls) updates including \"**Grant Privileges**\" feature for designating specific users for policy management, **Access Tree** visualization for simulating permissions, and ability to restrict scope of secret sharing within organizations.
- Released new **Secret Requests** feature under Secret Share, added support for reminders with webhook triggers and implementing password policies for dynamic secrets.
- Enhanced secret version history to show who made changes.
- New integrations and syncs: **Crossplane** provider, **Humanitec** secret sync, **Airflow** system integration
- Performed significant performance optimizations including a 50% reduction in database usage and optimized client secret handling for universal auth.
- Enhanced security features with ability to add custom instance banners (useful for regulated industries), short-lived tokens for Kubernetes auth, and OIDC claim passing from machine identity login to permissions.
- [Golang SDK](https://infisical.com/docs/sdks/languages/go#infisical-go-sdk): New API added for enhanced functionality
- Added capability to programmatically configure an Infisical instance from start to finish without UI interaction.
## February 2025
- Released [KMIP integration](https://infisical.com/docs/documentation/platform/kms/kmip) with PKI structure, auth model integration with machine identities, complete set of client operations, and client certificate authentication flow.
- Added new [AWS App Connection](https://infisical.com/docs/integrations/app-connections/aws) and [Secret Sync](https://infisical.com/docs/integrations/secret-syncs/aws-secrets-manager) functionality for enhanced AWS integration.
- Released new [Azure Key Vault App Connection](https://infisical.com/docs/integrations/app-connections/azure-key-vault) and [Secret Sync](https://infisical.com/docs/integrations/secret-syncs/azure-key-vault), plus Terraform provider support.
- Introduced more comprehensive logging with detailed records for secret sharing and metadata in audit logs.
- Introduced new [permission types](https://infisical.com/docs/internals/permissions/project-permissions#subject-secrets): \"View Value\" vs \"Describe Value\" for more granular access control over secrets.
- Updated encryption logic with unified approach for all platform data, ensuring consistency across the system.
- Added support for [OIDC group mapping](https://infisical.com/docs/documentation/platform/sso/general-oidc) to automatically map groups to Infisical for role-based access control.
- Added [Terraform Cloud support for OIDC](https://infisical.com/docs/documentation/platform/identities/oidc-auth/terraform-cloud#terraform-cloud).
## January 2025 ## January 2025
- Released new integration architecture with decoupled authentication, replacing native integrations with [App Connections](https://infisical.com/docs/integrations/app-connections/overview) and [Secret Syncs](https://infisical.com/docs/integrations/secret-syncs/overview). Initial support for AWS Parameter Store, GitHub, and GCP Secret Manager with improved API and Terraform integration capabilities. - Released new integration architecture with decoupled authentication, replacing native integrations with [App Connections](https://infisical.com/docs/integrations/app-connections/overview) and [Secret Syncs](https://infisical.com/docs/integrations/secret-syncs/overview). Initial support for AWS Parameter Store, GitHub, and GCP Secret Manager with improved API and Terraform integration capabilities.
@@ -15,7 +43,6 @@ The changelog below reflects new product developments and updates on a monthly b
- Implemented secret Access Visibility allowing users to view all entities with access to specific secrets in the secret side panel. - Implemented secret Access Visibility allowing users to view all entities with access to specific secrets in the secret side panel.
- Added secret filtering by metadata and SSH assigned certificates (Version 1). - Added secret filtering by metadata and SSH assigned certificates (Version 1).
## December 2024 ## December 2024
- Added [GCP KMS](https://infisical.com/docs/documentation/platform/kms/overview) integration support. - Added [GCP KMS](https://infisical.com/docs/documentation/platform/kms/overview) integration support.
- Added support for [K8s CSI integration](https://infisical.com/docs/integrations/platforms/kubernetes-csi) and ability to point K8s operator to specific secret versions. - Added support for [K8s CSI integration](https://infisical.com/docs/integrations/platforms/kubernetes-csi) and ability to point K8s operator to specific secret versions.

View File

@@ -8,6 +8,11 @@ You can use it across various environments, whether it's local development, CI/C
## Installation ## Installation
<Warning>
As of 04/08/25, all future releases for Debian/Ubuntu will be distributed via the official Infisical repository at https://artifacts-cli.infisical.com.
No new releases will be published for Debian/Ubuntu on Cloudsmith going forward.
</Warning>
<Tabs> <Tabs>
<Tab title="MacOS"> <Tab title="MacOS">
Use [brew](https://brew.sh/) package manager Use [brew](https://brew.sh/) package manager
@@ -93,11 +98,12 @@ You can use it across various environments, whether it's local development, CI/C
</Tip> </Tip>
</Tab> </Tab>
<Tab title="Debian/Ubuntu"> <Tab title="Debian/Ubuntu">
Add Infisical repository Add Infisical repository
```bash ```bash
curl -1sLf \ curl -1sLf \
'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' \ 'https://artifacts-cli.infisical.com/setup.deb.sh' \
| sudo -E bash | sudo -E bash
``` ```

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

View File

@@ -0,0 +1,97 @@
---
title: "Vercel Connection"
description: "Learn how to configure a Vercel Connection for Infisical."
---
Infisical supports connecting to Vercel using an API Token to securely sync your secrets to Vercel.
## Setup Vercel Connection in Infisical
<Steps>
<Step title="Move to API Tokens on Vercel">
Navigate to the Vercel **Account Settings** page by clicking on your profile icon in the top-right corner.
![Vercel API Tokens Tab](/images/app-connections/vercel/vercel-main-page.png)
</Step>
<Step title="Open API Tokens Tab">
Select the **API Tokens** tab from the left sidebar navigation menu.
![Vercel API Tokens Tab](/images/app-connections/vercel/vercel-settings-page.png)
</Step>
<Step title="Create the API Token">
Click the **Create** button and provide a name for your token (e.g., "Infisical Integration").
Choose appropriate scope permissions based on your requirements.
<Note>
If you configure an expiry date for your API token, you will need to manually rotate to a new token prior to expiration to avoid integration downtime. Consider setting a calendar reminder for this task.
</Note>
![Vercel Create API Token](/images/app-connections/vercel/vercel-create-token.png)
</Step>
<Step title="Copy the API Token">
After creation, a modal with the API token will be displayed. Copy this token immediately and store it securely, as you won't be able to view it again after closing this dialog.
![Vercel Copy API Token](/images/app-connections/vercel/vercel-copy-token.png)
</Step>
<Step title="Token Created">
You should now see your newly created token in the list of API tokens on the Vercel dashboard.
![Vercel Connection Created](/images/app-connections/vercel/vercel-token-created.png)
</Step>
<Step title="Setup Vercel Connection in Infisical">
<Tabs>
<Tab title="Infisical UI">
1. Navigate to App Connections
In your Infisical dashboard, go to **Organization Settings** and select the **App Connections** tab.
![App Connections Tab](/images/app-connections/general/add-connection.png)
2. Add Connection
Click the **+ Add Connection** button and select the **Vercel Connection** option from the available integrations.
![Select Vercel Connection](/images/app-connections/vercel/vercel-app-connection-option.png)
3. Fill the Vercel Connection Modal
Complete the Vercel Connection form by entering:
- A descriptive name for the connection
- The API Token you generated in steps 3-4
- An optional description for future reference
![Vercel Connection Modal](/images/app-connections/vercel/vercel-app-connection-modal.png)
4. Connection Created
After clicking Create, your **Vercel Connection** is established and ready to use with your Infisical projects.
![Vercel Connection Created](/images/app-connections/vercel/vercel-app-connection-created.png)
</Tab>
<Tab title="API">
To create a Vercel Connection, make an API request to the [Create Vercel
Connection](/api-reference/endpoints/app-connections/vercel/create) API endpoint.
### Sample request
```bash Request
curl --request POST \
--url https://app.infisical.com/api/v1/app-connections/vercel \
--header 'Content-Type: application/json' \
--data '{
"name": "my-vercel-connection",
"method": "api-token",
"credentials": {
"apiToken": "...",
}
}'
```
### Sample response
```bash Response
{
"appConnection": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "my-vercel-connection",
"version": 123,
"orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"createdAt": "2025-04-01T05:31:56Z",
"updatedAt": "2025-04-01T05:31:56Z",
"app": "vercel",
"method": "api-token",
"credentials": {}
}
}
```
</Tab>
</Tabs>
</Step>
</Steps>

View File

@@ -264,6 +264,7 @@ The available authentication methods are `universalAuth`, `kubernetesAuth`, `aws
- `credentialsRef.secretName`: The name of the Kubernetes secret. - `credentialsRef.secretName`: The name of the Kubernetes secret.
- `credentialsRef.secretNamespace`: The namespace of the Kubernetes secret. - `credentialsRef.secretNamespace`: The namespace of the Kubernetes secret.
Example: Example:
```yaml ```yaml
@@ -296,6 +297,9 @@ The available authentication methods are `universalAuth`, `kubernetesAuth`, `aws
- `serviceAccountRef`: The name and namespace of the service account that will be used to authenticate with Infisical. - `serviceAccountRef`: The name and namespace of the service account that will be used to authenticate with Infisical.
- `serviceAccountRef.name`: The name of the service account. - `serviceAccountRef.name`: The name of the service account.
- `serviceAccountRef.namespace`: The namespace of the service account. - `serviceAccountRef.namespace`: The namespace of the service account.
- `autoCreateServiceAccountToken`: If set to `true`, the operator will automatically create a short-lived service account token on-demand for the service account. Defaults to `false`.
- `serviceAccountTokenAudiences`: Optionally specify audience for the service account token. This field is only relevant if you have set `autoCreateServiceAccountToken` to `true`. No audience is specified by default.
Example: Example:
@@ -303,6 +307,9 @@ The available authentication methods are `universalAuth`, `kubernetesAuth`, `aws
spec: spec:
kubernetesAuth: kubernetesAuth:
identityId: <machine-identity-id> identityId: <machine-identity-id>
autoCreateServiceAccountToken: true # Automatically creates short-lived service account tokens for the service account.
serviceAccountTokenAudiences:
- <audience> # Optionally specify audience for the service account token. No audience is specified by default.
serviceAccountRef: serviceAccountRef:
name: <secret-name> name: <secret-name>
namespace: <secret-namespace> namespace: <secret-namespace>

View File

@@ -291,6 +291,8 @@ After applying the InfisicalPushSecret CRD, you should notice that the secrets y
- `serviceAccountRef`: The name and namespace of the service account that will be used to authenticate with Infisical. - `serviceAccountRef`: The name and namespace of the service account that will be used to authenticate with Infisical.
- `serviceAccountRef.name`: The name of the service account. - `serviceAccountRef.name`: The name of the service account.
- `serviceAccountRef.namespace`: The namespace of the service account. - `serviceAccountRef.namespace`: The namespace of the service account.
- `autoCreateServiceAccountToken`: If set to `true`, the operator will automatically create a short-lived service account token on-demand for the service account. Defaults to `false`.
- `serviceAccountTokenAudiences`: Optionally specify audience for the service account token. This field is only relevant if you have set `autoCreateServiceAccountToken` to `true`. No audience is specified by default.
Example: Example:
@@ -298,6 +300,9 @@ After applying the InfisicalPushSecret CRD, you should notice that the secrets y
spec: spec:
kubernetesAuth: kubernetesAuth:
identityId: <machine-identity-id> identityId: <machine-identity-id>
autoCreateServiceAccountToken: true # Automatically creates short-lived service account tokens for the service account.
serviceAccountTokenAudiences:
- <audience> # Optionally specify audience for the service account token. No audience is specified by default.
serviceAccountRef: serviceAccountRef:
name: <secret-name> name: <secret-name>
namespace: <secret-namespace> namespace: <secret-namespace>

View File

@@ -156,157 +156,420 @@ spec:
<Accordion title="authentication.kubernetesAuth"> <Accordion title="authentication.kubernetesAuth">
The Kubernetes machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used within a Kubernetes environment. The Kubernetes machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used within a Kubernetes environment.
<Steps> <Tabs>
<Step title="Obtaining the token reviewer JWT for Infisical"> <Tab title="Short-lived service account tokens (Recommended)">
1.1. Start by creating a service account in your Kubernetes cluster that will be used by Infisical to authenticate with the Kubernetes API Server. Short-lived service account tokens are automatically created by the operator and are valid only for a short period of time. This is the recommended approach for using Kubernetes auth in the Infisical Secrets Operator.
```yaml infisical-service-account.yaml <Steps>
apiVersion: v1 <Step title="Obtaining the token reviewer JWT for Infisical">
kind: ServiceAccount **1.1.** Start by creating a reviewer service account in your Kubernetes cluster that will be used by Infisical to authenticate with the Kubernetes API Server.
metadata:
name: infisical-auth
namespace: default
``` ```yaml infisical-reviewer-service-account.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: infisical-token-reviewer
namespace: default
``` ```
kubectl apply -f infisical-service-account.yaml
```
1.2. Bind the service account to the `system:auth-delegator` cluster role. As described [here](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#other-component-roles), this role allows delegated authentication and authorization checks, specifically for Infisical to access the [TokenReview API](https://kubernetes.io/docs/reference/kubernetes-api/authentication-resources/token-review-v1/). You can apply the following configuration file: ```bash
kubectl apply -f infisical-reviewer-service-account.yaml
```
```yaml cluster-role-binding.yaml **1.2.** Bind the reviewer service account to the `system:auth-delegator` cluster role. As described [here](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#other-component-roles), this role allows delegated authentication and authorization checks, specifically for Infisical to access the [TokenReview API](https://kubernetes.io/docs/reference/kubernetes-api/authentication-resources/token-review-v1/). You can apply the following configuration file:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: role-tokenreview-binding
namespace: default
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:auth-delegator
subjects:
- kind: ServiceAccount
name: infisical-auth
namespace: default
```
``` ```yaml infisical-reviewer-cluster-role-binding.yaml
kubectl apply -f cluster-role-binding.yaml apiVersion: rbac.authorization.k8s.io/v1
``` kind: ClusterRoleBinding
metadata:
name: infisical-token-reviewer-role-binding
namespace: default
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:auth-delegator
subjects:
- kind: ServiceAccount
name: infisical-token-reviewer
namespace: default
```
1.3. Next, create a long-lived service account JWT token (i.e. the token reviewer JWT token) for the service account using this configuration file for a new `Secret` resource: ```bash
kubectl apply -f infisical-reviewer-cluster-role-binding.yaml
```
```yaml service-account-token.yaml **1.3.** Next, create a long-lived service account JWT token (i.e. the token reviewer JWT token) for the service account using this configuration file for a new `Secret` resource:
apiVersion: v1
kind: Secret ```yaml service-account-reviewer-token.yaml
type: kubernetes.io/service-account-token apiVersion: v1
metadata: kind: Secret
name: infisical-auth-token type: kubernetes.io/service-account-token
annotations: metadata:
kubernetes.io/service-account.name: "infisical-auth" name: infisical-token-reviewer-token
``` annotations:
kubernetes.io/service-account.name: "infisical-token-reviewer"
```
``` ```bash
kubectl apply -f service-account-token.yaml kubectl apply -f service-account-reviewer-token.yaml
``` ```
1.4. Link the secret in step 1.3 to the service account in step 1.1: **1.4.** Link the secret in step 1.3 to the service account in step 1.1:
```bash ```bash
kubectl patch serviceaccount infisical-auth -p '{"secrets": [{"name": "infisical-auth-token"}]}' -n default kubectl patch serviceaccount infisical-token-reviewer -p '{"secrets": [{"name": "infisical-token-reviewer-token"}]}' -n default
``` ```
1.5. Finally, retrieve the token reviewer JWT token from the secret. **1.5.** Finally, retrieve the token reviewer JWT token from the secret.
```bash ```bash
kubectl get secret infisical-auth-token -n default -o=jsonpath='{.data.token}' | base64 --decode kubectl get secret infisical-token-reviewer-token -n default -o=jsonpath='{.data.token}' | base64 --decode
``` ```
Keep this JWT token handy as you will need it for the **Token Reviewer JWT** field when configuring the Kubernetes Auth authentication method for the identity in step 2. Keep this JWT token handy as you will need it for the **Token Reviewer JWT** field when configuring the Kubernetes Auth authentication method for the identity in step 2.
</Step>
</Step> <Step title="Creating an identity">
To create an identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**.
<Step title="Creating an identity"> ![identities organization](/images/platform/identities/identities-org.png)
To create an identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**.
![identities organization](/images/platform/identities/identities-org.png) When creating an identity, you specify an organization level [role](/documentation/platform/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles.
When creating an identity, you specify an organization level [role](/documentation/platform/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles. ![identities organization create](/images/platform/identities/identities-org-create.png)
![identities organization create](/images/platform/identities/identities-org-create.png) Now input a few details for your new identity. Here's some guidance for each field:
Now input a few details for your new identity. Here's some guidance for each field: - Name (required): A friendly name for the identity.
- Role (required): A role from the **Organization Roles** tab for the identity to assume. The organization role assigned will determine what organization level resources this identity can have access to.
- Name (required): A friendly name for the identity. Once you've created an identity, you'll be prompted to configure the authentication method for it. Here, select **Kubernetes Auth**.
- Role (required): A role from the **Organization Roles** tab for the identity to assume. The organization role assigned will determine what organization level resources this identity can have access to.
Once you've created an identity, you'll be prompted to configure the authentication method for it. Here, select **Kubernetes Auth**. <Info>
To learn more about each field of the Kubernetes native authentication method, see step 2 of [guide](/documentation/platform/identities/kubernetes-auth#guide).
</Info>
<Info> ![identities organization create auth method](/images/platform/identities/identities-org-create-kubernetes-auth-method.png)
To learn more about each field of the Kubernetes native authentication method, see step 2 of [guide](/documentation/platform/identities/kubernetes-auth#guide).
</Info>
![identities organization create auth method](/images/platform/identities/identities-org-create-kubernetes-auth-method.png)
</Step> </Step>
<Step title="Adding an identity to a project"> <Step title="Adding an identity to a project">
To allow the operator to use the given identity to access secrets, you will need to add the identity to project(s) that you would like to grant it access to. To allow the operator to use the given identity to access secrets, you will need to add the identity to project(s) that you would like to grant it access to.
To do this, head over to the project you want to add the identity to and go to Project Settings > Access Control > Machine Identities and press **Add identity**. To do this, head over to the project you want to add the identity to and go to Project Settings > Access Control > Machine Identities and press **Add identity**.
Next, select the identity you want to add to the project and the project level role you want to allow it to assume. The project role assigned will determine what project level resources this identity can have access to. Next, select the identity you want to add to the project and the project level role you want to allow it to assume. The project role assigned will determine what project level resources this identity can have access to.
![identities project](/images/platform/identities/identities-project.png) ![identities project](/images/platform/identities/identities-project.png)
![identities project create](/images/platform/identities/identities-project-create.png) ![identities project create](/images/platform/identities/identities-project-create.png)
</Step> </Step>
<Step title="Add your identity ID & service account to your InfisicalSecret resource">
Once you have created your machine identity and added it to your project(s), you will need to add the identity ID to your InfisicalSecret resource.
In the `authentication.kubernetesAuth.identityId` field, add the identity ID of the machine identity you created.
See the example below for more details.
</Step>
<Step title="Add your Kubernetes service account token to the InfisicalSecret resource">
Add the service account details from the previous steps under `authentication.kubernetesAuth.serviceAccountRef`.
Here you will need to enter the name and namespace of the service account.
The example below shows a complete InfisicalSecret resource with all required fields defined.
</Step>
</Steps> <Step title="Create a new Kubernetes service account to authenticate with Infisical">
You have already created the reviewer service account in step **1.1**. Now, create a new Kubernetes service account that will be used to authenticate with Infisical.
This service account will create short-lived tokens that will be used to authenticate with Infisical. The operator itself will handle the creation of these tokens automatically.
<Info> ```yaml infisical-service-account.yaml
Make sure to also populate the `secretsScope` field with the project slug kind: ServiceAccount
_`projectSlug`_, environment slug _`envSlug`_, and secrets path apiVersion: v1
_`secretsPath`_ that you want to fetch secrets from. Please see the example metadata:
below. name: infisical-service-account
</Info> ```
## Example ```bash
kubectl apply -f infisical-service-account.yaml -n default
```
```yaml example-kubernetes-auth.yaml </Step>
apiVersion: secrets.infisical.com/v1alpha1
kind: InfisicalSecret
metadata:
name: infisicalsecret-sample-crd
spec:
authentication:
kubernetesAuth:
identityId: <machine-identity-id>
serviceAccountRef:
name: <service-account-name>
namespace: <service-account-namespace>
# secretsScope is identical to the secrets scope in the universalAuth field in this sample. <Step title="Add your identity ID & service account to your InfisicalSecret resource">
secretsScope: Once you have created your machine identity and added it to your project(s), you will need to add the identity ID to your InfisicalSecret resource.
projectSlug: your-project-slug In the `authentication.kubernetesAuth.identityId` field, add the identity ID of the machine identity you created.
envSlug: prod See the example below for more details.
secretsPath: "/path" </Step>
recursive: true <Step title="Add your Kubernetes service account token to the InfisicalSecret resource">
... Add the service account details from the previous steps under `authentication.kubernetesAuth.serviceAccountRef`.
``` Here you will need to enter the name and namespace of the service account.
The example below shows a complete InfisicalSecret resource with all required fields defined.
Make sure you set `authentication.kubernetesAuth.autoCreateServiceAccountToken` to `true` to automatically create short-lived service account tokens for the service account.
</Step>
</Steps>
<Info>
Make sure to also populate the `secretsScope` field with the project slug
_`projectSlug`_, environment slug _`envSlug`_, and secrets path
_`secretsPath`_ that you want to fetch secrets from. Please see the example
below.
</Info>
## Example
```yaml example-kubernetes-auth.yaml
apiVersion: secrets.infisical.com/v1alpha1
kind: InfisicalSecret
metadata:
name: infisicalsecret-sample-crd
spec:
authentication:
kubernetesAuth:
identityId: <machine-identity-id>
autoCreateServiceAccountToken: true # Automatically creates short-lived service account tokens for the service account.
serviceAccountTokenAudiences:
- <audience> # Optionally specify audience for the service account token. No audience is specified by default.
serviceAccountRef:
name: infisical-service-account # The service account we just created in the previous step.
namespace: <service-account-namespace>
# secretsScope is identical to the secrets scope in the universalAuth field in this sample.
secretsScope:
projectSlug: your-project-slug
envSlug: prod
secretsPath: "/path"
recursive: true
...
```
</Tab>
<Tab title="Manual long-lived service account tokens">
Manual long-lived service account tokens are manually created by the user and are valid indefinitely unless deleted or rotated. In most cases, you should be using the automatic short-lived service account tokens as they are more secure and easier to use.
<Steps>
<Step title="Obtaining the token reviewer JWT for Infisical">
**1.1.** Start by creating a reviewer service account in your Kubernetes cluster that will be used by Infisical to authenticate with the Kubernetes API Server.
```yaml infisical-reviewer-service-account.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: infisical-token-reviewer
namespace: default
```
```bash
kubectl apply -f infisical-reviewer-service-account.yaml
```
**1.2.** Bind the reviewer service account to the `system:auth-delegator` cluster role. As described [here](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#other-component-roles), this role allows delegated authentication and authorization checks, specifically for Infisical to access the [TokenReview API](https://kubernetes.io/docs/reference/kubernetes-api/authentication-resources/token-review-v1/). You can apply the following configuration file:
```yaml infisical-reviewer-cluster-role-binding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: infisical-token-reviewer-role-binding
namespace: default
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:auth-delegator
subjects:
- kind: ServiceAccount
name: infisical-token-reviewer
namespace: default
```
```bash
kubectl apply -f infisical-reviewer-cluster-role-binding.yaml
```
**1.3.** Next, create a long-lived service account JWT token (i.e. the token reviewer JWT token) for the service account using this configuration file for a new `Secret` resource:
```yaml service-account-reviewer-token.yaml
apiVersion: v1
kind: Secret
type: kubernetes.io/service-account-token
metadata:
name: infisical-token-reviewer-token
annotations:
kubernetes.io/service-account.name: "infisical-token-reviewer"
```
```bash
kubectl apply -f service-account-reviewer-token.yaml
```
**1.4.** Link the secret in step 1.3 to the service account in step 1.1:
```bash
kubectl patch serviceaccount infisical-token-reviewer -p '{"secrets": [{"name": "infisical-token-reviewer-token"}]}' -n default
```
**1.5.** Finally, retrieve the token reviewer JWT token from the secret.
```bash
kubectl get secret infisical-token-reviewer-token -n default -o=jsonpath='{.data.token}' | base64 --decode
```
Keep this JWT token handy as you will need it for the **Token Reviewer JWT** field when configuring the Kubernetes Auth authentication method for the identity in step 2.
</Step>
<Step title="Creating an identity">
To create an identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**.
![identities organization](/images/platform/identities/identities-org.png)
When creating an identity, you specify an organization level [role](/documentation/platform/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles.
![identities organization create](/images/platform/identities/identities-org-create.png)
Now input a few details for your new identity. Here's some guidance for each field:
- Name (required): A friendly name for the identity.
- Role (required): A role from the **Organization Roles** tab for the identity to assume. The organization role assigned will determine what organization level resources this identity can have access to.
Once you've created an identity, you'll be prompted to configure the authentication method for it. Here, select **Kubernetes Auth**.
<Info>
To learn more about each field of the Kubernetes native authentication method, see step 2 of [guide](/documentation/platform/identities/kubernetes-auth#guide).
</Info>
![identities organization create auth method](/images/platform/identities/identities-org-create-kubernetes-auth-method.png)
</Step>
<Step title="Adding an identity to a project">
To allow the operator to use the given identity to access secrets, you will need to add the identity to project(s) that you would like to grant it access to.
To do this, head over to the project you want to add the identity to and go to Project Settings > Access Control > Machine Identities and press **Add identity**.
Next, select the identity you want to add to the project and the project level role you want to allow it to assume. The project role assigned will determine what project level resources this identity can have access to.
![identities project](/images/platform/identities/identities-project.png)
![identities project create](/images/platform/identities/identities-project-create.png)
</Step>
<Step title="Create a new Kubernetes service account to authenticate with Infisical">
You have already created the reviewer service account in step **1.1**. Now, create a new Kubernetes service account that will be used to authenticate with Infisical.
```yaml infisical-service-account.yaml
kind: ServiceAccount
apiVersion: v1
metadata:
name: infisical-service-account
```
```bash
kubectl apply -f infisical-service-account.yaml -n default
```
</Step>
<Step title="Create a service account token for the Kubernetes service account">
Create a service account token for the newly created Kubernetes service account from the previous step.
```yaml infisical-service-account-token.yaml
apiVersion: v1
kind: Secret
type: kubernetes.io/service-account-token
metadata:
name: infisical-service-account-token
annotations:
kubernetes.io/service-account.name: "infisical-service-account"
```
```bash
kubectl apply -f infisical-service-account-token.yaml -n default
```
Patch the service account with the newly created service account token.
```bash
kubectl patch serviceaccount infisical-service-account -p '{"secrets": [{"name": "infisical-service-account-token"}]}' -n default
```
</Step>
<Step title="Add your identity ID & service account to your InfisicalSecret resource">
Once you have created your machine identity and added it to your project(s), you will need to add the identity ID to your InfisicalSecret resource.
In the `authentication.kubernetesAuth.identityId` field, add the identity ID of the machine identity you created.
See the example below for more details.
</Step>
<Step title="Add your Kubernetes service account token to the InfisicalSecret resource">
Add the service account details from the previous steps under `authentication.kubernetesAuth.serviceAccountRef`.
Here you will need to enter the name and namespace of the service account.
The example below shows a complete InfisicalSecret resource with all required fields defined.
</Step>
</Steps>
<Info>
Make sure to also populate the `secretsScope` field with the project slug
_`projectSlug`_, environment slug _`envSlug`_, and secrets path
_`secretsPath`_ that you want to fetch secrets from. Please see the example
below.
</Info>
## Example
```yaml example-kubernetes-auth.yaml
apiVersion: secrets.infisical.com/v1alpha1
kind: InfisicalSecret
metadata:
name: infisicalsecret-sample-crd
spec:
authentication:
kubernetesAuth:
identityId: <machine-identity-id>
serviceAccountRef:
name: infisical-service-account # The service account we just created in the previous step. (*not* the reviewer service account)
namespace: <service-account-namespace>
# secretsScope is identical to the secrets scope in the universalAuth field in this sample.
secretsScope:
projectSlug: your-project-slug
envSlug: prod
secretsPath: "/path"
recursive: true
...
```
</Tab>
</Tabs>
</Accordion> </Accordion>

View File

@@ -0,0 +1,148 @@
---
title: "Vercel Sync"
description: "Learn how to configure a Vercel Sync for Infisical."
---
**Prerequisites:**
- Set up and add secrets to [Infisical Cloud](https://app.infisical.com)
- Create a [Vercel Connection](/integrations/app-connections/vercel)
<Tabs>
<Tab title="Infisical UI">
1. Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button.
![Secret Syncs Tab](/images/secret-syncs/general/secret-sync-tab.png)
2. Select the **Vercel** option.
![Select Vercel](/images/secret-syncs/vercel/select-vercel-option.png)
3. Configure the **Source** from where secrets should be retrieved, then click **Next**.
![Configure Source](/images/secret-syncs/vercel/vercel-source.png)
- **Environment**: The project environment to retrieve secrets from.
- **Secret Path**: The folder path to retrieve secrets from.
<Tip>
If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports).
</Tip>
4. Configure the **Destination** to where secrets should be deployed, then click **Next**.
![Configure Destination](/images/secret-syncs/vercel/vercel-destination.png)
- **Vercel Connection**: The Vercel Connection to authenticate with.
- **Vercel App**: The application to deploy secrets to.
- **Vercel App Environment**: The environment to deploy secrets to.
- **Vercel Preview Branch (Optional)**: Specify a branch for preview deployments if needed.
After configuring these parameters, click the **Next** button to continue to the Sync Options step.
5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**.
![Configure Options](/images/secret-syncs/vercel/vercel-options.png)
- **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync.
- **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical.
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Vercel when keys conflict.
- **Import Secrets (Prioritize Vercel)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Vercel over Infisical when keys conflict.
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
- **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical.
6. Configure the **Details** of your Vercel Sync, then click **Next**.
![Configure Details](/images/secret-syncs/vercel/vercel-details.png)
- **Name**: The name of your sync. Must be slug-friendly.
- **Description**: An optional description for your sync.
7. Review your Vercel Sync configuration, then click **Create Sync**.
![Confirm Configuration](/images/secret-syncs/vercel/vercel-review.png)
8. If enabled, your Vercel Sync will begin syncing your secrets to the destination endpoint.
![Sync Secrets](/images/secret-syncs/vercel/vercel-created.png)
</Tab>
<Tab title="API">
To create an **Vercel Sync**, make an API request to the [Create Vercel Sync](/api-reference/endpoints/secret-syncs/vercel/create) API endpoint.
### Sample request
```bash Request
curl --request POST \
--url https://app.infisical.com/api/v1/secret-syncs/vercel \
--header 'Content-Type: application/json' \
--data '{
"name": "my-vercel-sync",
"projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"description": "an example sync",
"connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"environment": "dev",
"secretPath": "/my-secrets",
"isEnabled": true,
"syncOptions": {
"initialSyncBehavior": "overwrite-destination"
},
"destinationConfig": {
"app": "prj_bz7zgHvQETPvJWc5tmIr0tGRH9kE",
"env": "preview",
"branch": "test",
"appName": "nextjs-boilerplate",
"teamId": "team_0d444b5088888dd257"
}
}'
```
### Sample response
```bash Response
{
"secretSync": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "my-vercel-sync",
"description": "an example sync",
"isEnabled": true,
"version": 1,
"folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"createdAt": "2023-11-07T05:31:56Z",
"updatedAt": "2023-11-07T05:31:56Z",
"syncStatus": "succeeded",
"lastSyncJobId": "123",
"lastSyncMessage": null,
"lastSyncedAt": "2023-11-07T05:31:56Z",
"importStatus": null,
"lastImportJobId": null,
"lastImportMessage": null,
"lastImportedAt": null,
"removeStatus": null,
"lastRemoveJobId": null,
"lastRemoveMessage": null,
"lastRemovedAt": null,
"syncOptions": {
"initialSyncBehavior": "overwrite-destination"
},
"projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"connection": {
"app": "vercel",
"name": "my-vercel-connection",
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
},
"environment": {
"slug": "dev",
"name": "Development",
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
},
"folder": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"path": "/my-secrets"
},
"destination": "vercel",
"destinationConfig": {
"app": "prj_bz7zgHvQETPvJWc5tmIr0tGRH9kE",
"env": "preview",
"branch": "test",
"appName": "nextjs-boilerplate",
"teamId": "team_0d444b5088888dd257"
}
}
}
```
</Tab>
</Tabs>

View File

@@ -422,6 +422,7 @@
"integrations/app-connections/gcp", "integrations/app-connections/gcp",
"integrations/app-connections/github", "integrations/app-connections/github",
"integrations/app-connections/humanitec", "integrations/app-connections/humanitec",
"integrations/app-connections/vercel",
"integrations/app-connections/mssql", "integrations/app-connections/mssql",
"integrations/app-connections/postgres" "integrations/app-connections/postgres"
] ]
@@ -443,7 +444,8 @@
"integrations/secret-syncs/databricks", "integrations/secret-syncs/databricks",
"integrations/secret-syncs/gcp-secret-manager", "integrations/secret-syncs/gcp-secret-manager",
"integrations/secret-syncs/github", "integrations/secret-syncs/github",
"integrations/secret-syncs/humanitec" "integrations/secret-syncs/humanitec",
"integrations/secret-syncs/vercel"
] ]
} }
] ]
@@ -584,7 +586,8 @@
"api-reference/endpoints/identities/update", "api-reference/endpoints/identities/update",
"api-reference/endpoints/identities/delete", "api-reference/endpoints/identities/delete",
"api-reference/endpoints/identities/get-by-id", "api-reference/endpoints/identities/get-by-id",
"api-reference/endpoints/identities/list" "api-reference/endpoints/identities/list",
"api-reference/endpoints/identities/search"
] ]
}, },
{ {
@@ -973,6 +976,18 @@
"api-reference/endpoints/app-connections/humanitec/delete" "api-reference/endpoints/app-connections/humanitec/delete"
] ]
}, },
{
"group": "Vercel",
"pages": [
"api-reference/endpoints/app-connections/vercel/list",
"api-reference/endpoints/app-connections/vercel/available",
"api-reference/endpoints/app-connections/vercel/get-by-id",
"api-reference/endpoints/app-connections/vercel/get-by-name",
"api-reference/endpoints/app-connections/vercel/create",
"api-reference/endpoints/app-connections/vercel/update",
"api-reference/endpoints/app-connections/vercel/delete"
]
},
{ {
"group": "Microsoft SQL Server", "group": "Microsoft SQL Server",
"pages": [ "pages": [
@@ -1125,6 +1140,20 @@
"api-reference/endpoints/secret-syncs/humanitec/sync-secrets", "api-reference/endpoints/secret-syncs/humanitec/sync-secrets",
"api-reference/endpoints/secret-syncs/humanitec/remove-secrets" "api-reference/endpoints/secret-syncs/humanitec/remove-secrets"
] ]
},
{
"group": "Vercel",
"pages": [
"api-reference/endpoints/secret-syncs/vercel/list",
"api-reference/endpoints/secret-syncs/vercel/get-by-id",
"api-reference/endpoints/secret-syncs/vercel/get-by-name",
"api-reference/endpoints/secret-syncs/vercel/create",
"api-reference/endpoints/secret-syncs/vercel/update",
"api-reference/endpoints/secret-syncs/vercel/delete",
"api-reference/endpoints/secret-syncs/vercel/sync-secrets",
"api-reference/endpoints/secret-syncs/vercel/remove-secrets",
"api-reference/endpoints/secret-syncs/vercel/import-secrets"
]
} }
] ]
}, },

View File

@@ -23,6 +23,7 @@
"@hcaptcha/react-hcaptcha": "^1.11.0", "@hcaptcha/react-hcaptcha": "^1.11.0",
"@headlessui/react": "^1.7.19", "@headlessui/react": "^1.7.19",
"@hookform/resolvers": "^3.9.1", "@hookform/resolvers": "^3.9.1",
"@lexical/react": "^0.29.0",
"@lottiefiles/dotlottie-react": "^0.12.0", "@lottiefiles/dotlottie-react": "^0.12.0",
"@octokit/rest": "^21.0.2", "@octokit/rest": "^21.0.2",
"@peculiar/x509": "^1.12.3", "@peculiar/x509": "^1.12.3",
@@ -66,6 +67,7 @@
"jspdf": "^2.5.2", "jspdf": "^2.5.2",
"jsrp": "^0.2.4", "jsrp": "^0.2.4",
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"lexical": "^0.29.0",
"ms": "^2.1.3", "ms": "^2.1.3",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"picomatch": "^4.0.2", "picomatch": "^4.0.2",
@@ -1570,6 +1572,260 @@
} }
} }
}, },
"node_modules/@lexical/clipboard": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@lexical/clipboard/-/clipboard-0.29.0.tgz",
"integrity": "sha512-llxZosYCwH13p2GfPfhAinukdvAZYxWuwf5md107X80hsE8TQJj25unjqTwRKQ+w/wD+hpmBMziU8+K/WTitWQ==",
"license": "MIT",
"dependencies": {
"@lexical/html": "0.29.0",
"@lexical/list": "0.29.0",
"@lexical/selection": "0.29.0",
"@lexical/utils": "0.29.0",
"lexical": "0.29.0"
}
},
"node_modules/@lexical/code": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@lexical/code/-/code-0.29.0.tgz",
"integrity": "sha512-yKGzoKpyIO39Xf7OKLPpoCE5V8mTDCM3l3CDHZR3X1gM/VZQzf4jAiO3b06y9YkQ2fM8kqwchYu87wGvs8/iIQ==",
"license": "MIT",
"dependencies": {
"@lexical/utils": "0.29.0",
"lexical": "0.29.0",
"prismjs": "^1.30.0"
}
},
"node_modules/@lexical/devtools-core": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@lexical/devtools-core/-/devtools-core-0.29.0.tgz",
"integrity": "sha512-uUq0m9ql/7mthp7Ho1vnG7Id6imQ5kD5mxUhX2lmgHretS+yAHGsGsGiPIVHdPWeVmUb2n4IVDJ+cJbUsUjQJw==",
"license": "MIT",
"dependencies": {
"@lexical/html": "0.29.0",
"@lexical/link": "0.29.0",
"@lexical/mark": "0.29.0",
"@lexical/table": "0.29.0",
"@lexical/utils": "0.29.0",
"lexical": "0.29.0"
},
"peerDependencies": {
"react": ">=17.x",
"react-dom": ">=17.x"
}
},
"node_modules/@lexical/dragon": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@lexical/dragon/-/dragon-0.29.0.tgz",
"integrity": "sha512-Zaky2jd/Pp1blAZqPeGNdyhxnVL4lwVjbWPxhfS1gbW4Q5CBQ3aD3B0T4ljiKfmRNJm004LJ9q7KjhlRbREvZA==",
"license": "MIT",
"dependencies": {
"lexical": "0.29.0"
}
},
"node_modules/@lexical/hashtag": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@lexical/hashtag/-/hashtag-0.29.0.tgz",
"integrity": "sha512-fa7s0Yi2RKz/GvgT5XU9fborx6VPU3VtvvEPaIXgyd6zXZRiOhD9rGypwB3oj4fMK1ndx2dX0m7SwhMJo48D8w==",
"license": "MIT",
"dependencies": {
"@lexical/utils": "0.29.0",
"lexical": "0.29.0"
}
},
"node_modules/@lexical/history": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@lexical/history/-/history-0.29.0.tgz",
"integrity": "sha512-OrCwZycp/yaq63mw511NutkwAB+W6WSchG1xTxlLh6nbc8jnbvKhCf4CGbnrvlhD7hTuzxJ8FI9/2M/2zv/mNQ==",
"license": "MIT",
"dependencies": {
"@lexical/utils": "0.29.0",
"lexical": "0.29.0"
}
},
"node_modules/@lexical/html": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@lexical/html/-/html-0.29.0.tgz",
"integrity": "sha512-+jV6ijppOpxpUGeXkGssXJbsAmFALfeLrgbM0xuZbxZ7RgYZ+5Atn00WjSno7+JV5EOuRkYmCNtS1tiHtXMY1g==",
"license": "MIT",
"dependencies": {
"@lexical/selection": "0.29.0",
"@lexical/utils": "0.29.0",
"lexical": "0.29.0"
}
},
"node_modules/@lexical/link": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@lexical/link/-/link-0.29.0.tgz",
"integrity": "sha512-wGbKRF0x/6ZQHuCfr8m8qD1J0R1kFmWINBG2A1hUXPDf7UY5qm/nS2oKNDGpjiDMGwkVZ7n7WfzeBGO+KRe/Lg==",
"license": "MIT",
"dependencies": {
"@lexical/utils": "0.29.0",
"lexical": "0.29.0"
}
},
"node_modules/@lexical/list": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@lexical/list/-/list-0.29.0.tgz",
"integrity": "sha512-sWiof+i2ff8rL7KxJ3dxHLwyJfX423e1EVLmAdQEOPhyZJiNbeLTSNhNGsZ8FjFoBwvTTEDwuQZm3iT3hliKOg==",
"license": "MIT",
"dependencies": {
"@lexical/selection": "0.29.0",
"@lexical/utils": "0.29.0",
"lexical": "0.29.0"
}
},
"node_modules/@lexical/mark": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@lexical/mark/-/mark-0.29.0.tgz",
"integrity": "sha512-UB3x6pyUdpZHRqF4tiajLnC1+Umvt7x8Rkkdi29aNNvzIWniVwGkBOlmvFus7x+4dOV1D1fydwiP4m38nGgLDw==",
"license": "MIT",
"dependencies": {
"@lexical/utils": "0.29.0",
"lexical": "0.29.0"
}
},
"node_modules/@lexical/markdown": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@lexical/markdown/-/markdown-0.29.0.tgz",
"integrity": "sha512-4Od8WoDoviv9DxJZVgrIORTIAzyoGOpztbGbIBXguGmwvy7NnHQDh9fZYIYRrdI1Awp1VVGdJ3ku/7KTgSOoRw==",
"license": "MIT",
"dependencies": {
"@lexical/code": "0.29.0",
"@lexical/link": "0.29.0",
"@lexical/list": "0.29.0",
"@lexical/rich-text": "0.29.0",
"@lexical/text": "0.29.0",
"@lexical/utils": "0.29.0",
"lexical": "0.29.0"
}
},
"node_modules/@lexical/offset": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@lexical/offset/-/offset-0.29.0.tgz",
"integrity": "sha512-VyD2Ff3rBJpo++Fxvi3MNYmDELa+9nA0EgXqGRNb3MvRehRjHbaDbymtLMMHIwvbkF5lnra+ubStcTRQmoQxXw==",
"license": "MIT",
"dependencies": {
"lexical": "0.29.0"
}
},
"node_modules/@lexical/overflow": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@lexical/overflow/-/overflow-0.29.0.tgz",
"integrity": "sha512-IzH3M652Ej2gB2sK65N3yTgyiQAa3I3tqKbSnBRiXu/+isxHoCy/qRr9/kL63uy7zhGvgV+EYsoffQCawIFt8Q==",
"license": "MIT",
"dependencies": {
"lexical": "0.29.0"
}
},
"node_modules/@lexical/plain-text": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@lexical/plain-text/-/plain-text-0.29.0.tgz",
"integrity": "sha512-F5C3meDb2HmO0NmKJBVRkjmX9PNln6O1jXU/APJuSFBdvfcIWSY58ncHR4zy2M5LF1Q5PQMWyIay9p+SqOtY5A==",
"license": "MIT",
"dependencies": {
"@lexical/clipboard": "0.29.0",
"@lexical/selection": "0.29.0",
"@lexical/utils": "0.29.0",
"lexical": "0.29.0"
}
},
"node_modules/@lexical/react": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@lexical/react/-/react-0.29.0.tgz",
"integrity": "sha512-YMlnljW/jxmwSzsRv5UPatfOoMZXqxFmRIEltTUIQfrOFdqn+ssUtCpjE6xRD1oxD6KpSIekakzLs+y/8+7CuQ==",
"license": "MIT",
"dependencies": {
"@lexical/devtools-core": "0.29.0",
"@lexical/dragon": "0.29.0",
"@lexical/hashtag": "0.29.0",
"@lexical/history": "0.29.0",
"@lexical/link": "0.29.0",
"@lexical/list": "0.29.0",
"@lexical/mark": "0.29.0",
"@lexical/markdown": "0.29.0",
"@lexical/overflow": "0.29.0",
"@lexical/plain-text": "0.29.0",
"@lexical/rich-text": "0.29.0",
"@lexical/table": "0.29.0",
"@lexical/text": "0.29.0",
"@lexical/utils": "0.29.0",
"@lexical/yjs": "0.29.0",
"lexical": "0.29.0",
"react-error-boundary": "^3.1.4"
},
"peerDependencies": {
"react": ">=17.x",
"react-dom": ">=17.x"
}
},
"node_modules/@lexical/rich-text": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@lexical/rich-text/-/rich-text-0.29.0.tgz",
"integrity": "sha512-fSKgXGxJUOWo7dwSTUYFVBNNk4pPN8norsZfdmKM1kGDS1/GKuVzlzHLKZ7rQb8RLD5a43p4ifEL+28P+q0Qqg==",
"license": "MIT",
"dependencies": {
"@lexical/clipboard": "0.29.0",
"@lexical/selection": "0.29.0",
"@lexical/utils": "0.29.0",
"lexical": "0.29.0"
}
},
"node_modules/@lexical/selection": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@lexical/selection/-/selection-0.29.0.tgz",
"integrity": "sha512-lX9CRrXgKte65cozTHFXwUJ2fvZD92OEtos+YU+U40GJjf3NdheGeKDxDfOpF4AXrYRSszY7E0CzmIvuEs0p4A==",
"license": "MIT",
"dependencies": {
"lexical": "0.29.0"
}
},
"node_modules/@lexical/table": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@lexical/table/-/table-0.29.0.tgz",
"integrity": "sha512-Jdj32kBDeJh/0dGaZB14JggnEIS956/cN7grnLr7cmhhVzDicvLMBENSXQVEJAQVcSIU4G9EvxC7GJZ9VgqDnA==",
"license": "MIT",
"dependencies": {
"@lexical/clipboard": "0.29.0",
"@lexical/utils": "0.29.0",
"lexical": "0.29.0"
}
},
"node_modules/@lexical/text": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@lexical/text/-/text-0.29.0.tgz",
"integrity": "sha512-QnNGr6ickTLk76o3PdxJjPwt//dpuh8idVfR73WdCIoAwkhiEPUxxTZERoMsudXj6O/lJ+/HhI61wVjLckYr3A==",
"license": "MIT",
"dependencies": {
"lexical": "0.29.0"
}
},
"node_modules/@lexical/utils": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@lexical/utils/-/utils-0.29.0.tgz",
"integrity": "sha512-y2hhWQDjcXdplsAaQMuZx6ht9u1I4BV5NynA+WKoQ3h8vKxzeDnpCxVOK/zxU1R5dhM/nilnFu7uhvrSeEn+TQ==",
"license": "MIT",
"dependencies": {
"@lexical/list": "0.29.0",
"@lexical/selection": "0.29.0",
"@lexical/table": "0.29.0",
"lexical": "0.29.0"
}
},
"node_modules/@lexical/yjs": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@lexical/yjs/-/yjs-0.29.0.tgz",
"integrity": "sha512-6IXWWlGkVJEzWP/+LcuKYJ9jmcFp8k7TT/jmz4V5gBD9Ut3swOGsIA/sQCtB9y7jad10csaDVmFdFzGNWKVH9A==",
"license": "MIT",
"dependencies": {
"@lexical/offset": "0.29.0",
"@lexical/selection": "0.29.0",
"lexical": "0.29.0"
},
"peerDependencies": {
"yjs": ">=13.5.22"
}
},
"node_modules/@lottiefiles/dotlottie-react": { "node_modules/@lottiefiles/dotlottie-react": {
"version": "0.12.0", "version": "0.12.0",
"resolved": "https://registry.npmjs.org/@lottiefiles/dotlottie-react/-/dotlottie-react-0.12.0.tgz", "resolved": "https://registry.npmjs.org/@lottiefiles/dotlottie-react/-/dotlottie-react-0.12.0.tgz",
@@ -8871,6 +9127,17 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/isomorphic.js": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz",
"integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==",
"license": "MIT",
"peer": true,
"funding": {
"type": "GitHub Sponsors ❤",
"url": "https://github.com/sponsors/dmonad"
}
},
"node_modules/iterator.prototype": { "node_modules/iterator.prototype": {
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.4.tgz", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.4.tgz",
@@ -9100,6 +9367,34 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/lexical": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/lexical/-/lexical-0.29.0.tgz",
"integrity": "sha512-eoBHUEn0LmExKeK6x2cFKU0FPaMk2Bc5HgiCzTiv5ymKtwWw7LeKcxaNPmLxRRdQpcWV1IMKjayAbw7Lt/Gu7w==",
"license": "MIT"
},
"node_modules/lib0": {
"version": "0.2.102",
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.102.tgz",
"integrity": "sha512-g70kydI0I1sZU0ChO8mBbhw0oUW/8U0GHzygpvEIx8k+jgOpqnTSb/E+70toYVqHxBhrERD21TwD5QcZJQ40ZQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"isomorphic.js": "^0.2.4"
},
"bin": {
"0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js",
"0gentesthtml": "bin/gentesthtml.js",
"0serve": "bin/0serve.js"
},
"engines": {
"node": ">=16"
},
"funding": {
"type": "GitHub Sponsors ❤",
"url": "https://github.com/sponsors/dmonad"
}
},
"node_modules/lilconfig": { "node_modules/lilconfig": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
@@ -10857,6 +11152,15 @@
} }
} }
}, },
"node_modules/prismjs": {
"version": "1.30.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
"integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/process": { "node_modules/process": {
"version": "0.11.10", "version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
@@ -11142,6 +11446,22 @@
"react": "^18.3.1" "react": "^18.3.1"
} }
}, },
"node_modules/react-error-boundary": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz",
"integrity": "sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.5"
},
"engines": {
"node": ">=10",
"npm": ">=6"
},
"peerDependencies": {
"react": ">=16.13.1"
}
},
"node_modules/react-fast-compare": { "node_modules/react-fast-compare": {
"version": "3.2.2", "version": "3.2.2",
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
@@ -13587,9 +13907,9 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "5.4.14", "version": "5.4.16",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.16.tgz",
"integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==", "integrity": "sha512-Y5gnfp4NemVfgOTDQAunSD4346fal44L9mszGGY/e+qxsRT5y1sMlS/8tiQ8AFAp+MFgYNSINdfEchJiPm41vQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -14131,6 +14451,24 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/yjs": {
"version": "13.6.24",
"resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.24.tgz",
"integrity": "sha512-xn/pYLTZa3uD1uDG8lpxfLRo5SR/rp0frdASOl2a71aYNvUXdWcLtVL91s2y7j+Q8ppmjZ9H3jsGVgoFMbT2VA==",
"license": "MIT",
"peer": true,
"dependencies": {
"lib0": "^0.2.99"
},
"engines": {
"node": ">=16.0.0",
"npm": ">=8.0.0"
},
"funding": {
"type": "GitHub Sponsors ❤",
"url": "https://github.com/sponsors/dmonad"
}
},
"node_modules/yocto-queue": { "node_modules/yocto-queue": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",

View File

@@ -27,6 +27,7 @@
"@hcaptcha/react-hcaptcha": "^1.11.0", "@hcaptcha/react-hcaptcha": "^1.11.0",
"@headlessui/react": "^1.7.19", "@headlessui/react": "^1.7.19",
"@hookform/resolvers": "^3.9.1", "@hookform/resolvers": "^3.9.1",
"@lexical/react": "^0.29.0",
"@lottiefiles/dotlottie-react": "^0.12.0", "@lottiefiles/dotlottie-react": "^0.12.0",
"@octokit/rest": "^21.0.2", "@octokit/rest": "^21.0.2",
"@peculiar/x509": "^1.12.3", "@peculiar/x509": "^1.12.3",
@@ -70,6 +71,7 @@
"jspdf": "^2.5.2", "jspdf": "^2.5.2",
"jsrp": "^0.2.4", "jsrp": "^0.2.4",
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"lexical": "^0.29.0",
"ms": "^2.1.3", "ms": "^2.1.3",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"picomatch": "^4.0.2", "picomatch": "^4.0.2",

View File

@@ -12,6 +12,7 @@ import { DatabricksSyncFields } from "./DatabricksSyncFields";
import { GcpSyncFields } from "./GcpSyncFields"; import { GcpSyncFields } from "./GcpSyncFields";
import { GitHubSyncFields } from "./GitHubSyncFields"; import { GitHubSyncFields } from "./GitHubSyncFields";
import { HumanitecSyncFields } from "./HumanitecSyncFields"; import { HumanitecSyncFields } from "./HumanitecSyncFields";
import { VercelSyncFields } from "./VercelSyncFields";
export const SecretSyncDestinationFields = () => { export const SecretSyncDestinationFields = () => {
const { watch } = useFormContext<TSecretSyncForm>(); const { watch } = useFormContext<TSecretSyncForm>();
@@ -37,6 +38,8 @@ export const SecretSyncDestinationFields = () => {
return <HumanitecSyncFields />; return <HumanitecSyncFields />;
case SecretSync.Camunda: case SecretSync.Camunda:
return <CamundaSyncFields />; return <CamundaSyncFields />;
case SecretSync.Vercel:
return <VercelSyncFields />;
default: default:
throw new Error(`Unhandled Destination Config Field: ${destination}`); throw new Error(`Unhandled Destination Config Field: ${destination}`);
} }

View File

@@ -0,0 +1,195 @@
import { useMemo } from "react";
import { Controller, useFormContext, useWatch } from "react-hook-form";
import { SingleValue } from "react-select";
import { faCircleInfo } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
import { FilterableSelect, FormControl, Tooltip } from "@app/components/v2";
import {
TVercelConnectionApp,
useVercelConnectionListOrganizations
} from "@app/hooks/api/appConnections/vercel";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TSecretSyncForm } from "../schemas";
const vercelEnvironments = [
{ name: "Development", slug: "development" },
{ name: "Preview", slug: "preview" },
{ name: "Production", slug: "production" }
];
export const VercelSyncFields = () => {
const { control, watch, setValue } = useFormContext<
TSecretSyncForm & { destination: SecretSync.Vercel }
>();
const connectionId = useWatch({ name: "connection.id", control });
const currentApp = watch("destinationConfig.app");
const currentEnv = watch("destinationConfig.env");
const { data: projects, isLoading: isProjectsLoading } = useVercelConnectionListOrganizations(
connectionId,
{
enabled: Boolean(connectionId)
}
);
const selectedProject = projects
?.find((project) => project.apps.some((app) => app.id === currentApp))
?.apps.find((app) => app.id === currentApp);
const allApps =
projects?.flatMap((project) =>
project.apps.map((app) => ({ ...app, project: project.name, projectId: project.id }))
) || [];
const environmentOptions = useMemo(() => {
return vercelEnvironments
.map((env) => ({
key: env.slug,
type: env.slug,
name: env.name
}))
.concat(
selectedProject?.envs?.map((env) => ({
key: env.id,
type: env.type,
name: env.slug
})) || []
);
}, [currentApp]);
const previewBranchOptions =
selectedProject?.previewBranches?.map((branch) => ({
id: branch,
name: branch
})) || [];
const isPreviewEnvironment = currentEnv === "preview";
return (
<>
<SecretSyncConnectionField
onChange={() => {
setValue("destinationConfig.app", "");
setValue("destinationConfig.appName", "");
setValue("destinationConfig.env", "production");
setValue("destinationConfig.branch", "");
}}
/>
<Controller
name="destinationConfig.app"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Vercel App"
helperText={
<Tooltip
className="max-w-md"
content="Ensure the project exists and the API token scope for this connection includes the desired project."
>
<div>
<span>Don&#39;t see the project you&#39;re looking for?</span>{" "}
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
</div>
</Tooltip>
}
>
<FilterableSelect
menuPlacement="top"
isLoading={isProjectsLoading && Boolean(connectionId)}
isDisabled={!connectionId}
value={allApps.find((app) => app.id === value) ?? null}
onChange={(option) => {
const appId = (option as SingleValue<TVercelConnectionApp>)?.id ?? null;
onChange(appId);
setValue("destinationConfig.branch", "");
setValue(
"destinationConfig.teamId",
(option as SingleValue<TVercelConnectionApp>)?.projectId || ""
);
setValue(
"destinationConfig.appName",
(option as SingleValue<TVercelConnectionApp>)?.name || ""
);
}}
options={allApps}
placeholder="Select a project..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id.toString()}
groupBy="project"
/>
</FormControl>
)}
/>
<Controller
name="destinationConfig.env"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Vercel App Environment"
>
<FilterableSelect
menuPlacement="top"
isDisabled={!connectionId || !currentApp}
value={
value
? {
key: environmentOptions.find((env) => env.key === value)?.key,
type: environmentOptions.find((env) => env.key === value)?.type,
name: environmentOptions.find((env) => env.key === value)?.name
}
: null
}
onChange={(option) => {
const envKey = (option as any)?.key ?? null;
onChange(envKey);
setValue("destinationConfig.branch", "");
}}
options={environmentOptions}
placeholder="Select an environment..."
getOptionLabel={(option) => option.name || option.key || ""}
getOptionValue={(option) => option.key || ""}
/>
</FormControl>
)}
/>
{isPreviewEnvironment && (
<Controller
name="destinationConfig.branch"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Vercel Preview Branch (Optional)"
>
<FilterableSelect
menuPlacement="top"
isLoading={isProjectsLoading && Boolean(connectionId) && Boolean(currentApp)}
isDisabled={!connectionId || !currentApp}
value={previewBranchOptions.find((branch) => branch.id === value) ?? null}
onChange={(option) => onChange((option as SingleValue<{ id: string }>)?.id || "")}
options={previewBranchOptions}
placeholder="Select a branch..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option?.id || ""}
isClearable
/>
</FormControl>
)}
/>
)}
</>
);
};

View File

@@ -40,6 +40,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
case SecretSync.Databricks: case SecretSync.Databricks:
case SecretSync.Humanitec: case SecretSync.Humanitec:
case SecretSync.Camunda: case SecretSync.Camunda:
case SecretSync.Vercel:
AdditionalSyncOptionsFieldsComponent = null; AdditionalSyncOptionsFieldsComponent = null;
break; break;
default: default:

View File

@@ -22,6 +22,7 @@ import { DatabricksSyncReviewFields } from "./DatabricksSyncReviewFields";
import { GcpSyncReviewFields } from "./GcpSyncReviewFields"; import { GcpSyncReviewFields } from "./GcpSyncReviewFields";
import { GitHubSyncReviewFields } from "./GitHubSyncReviewFields"; import { GitHubSyncReviewFields } from "./GitHubSyncReviewFields";
import { HumanitecSyncReviewFields } from "./HumanitecSyncReviewFields"; import { HumanitecSyncReviewFields } from "./HumanitecSyncReviewFields";
import { VercelSyncReviewFields } from "./VercelSyncReviewFields";
export const SecretSyncReviewFields = () => { export const SecretSyncReviewFields = () => {
const { watch } = useFormContext<TSecretSyncForm>(); const { watch } = useFormContext<TSecretSyncForm>();
@@ -76,6 +77,9 @@ export const SecretSyncReviewFields = () => {
case SecretSync.Camunda: case SecretSync.Camunda:
DestinationFieldsComponent = <CamundaSyncReviewFields />; DestinationFieldsComponent = <CamundaSyncReviewFields />;
break; break;
case SecretSync.Vercel:
DestinationFieldsComponent = <VercelSyncReviewFields />;
break;
default: default:
throw new Error(`Unhandled Destination Review Fields: ${destination}`); throw new Error(`Unhandled Destination Review Fields: ${destination}`);
} }

View File

@@ -0,0 +1,23 @@
import { useFormContext } from "react-hook-form";
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
import { GenericFieldLabel } from "@app/components/v2";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { VercelEnvironmentType } from "@app/hooks/api/secretSyncs/types/vercel-sync";
export const VercelSyncReviewFields = () => {
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.Vercel }>();
const envId = watch("destinationConfig.env");
const branchId = watch("destinationConfig.branch");
const appName = watch("destinationConfig.appName");
return (
<>
<GenericFieldLabel label="Vercel App">{appName}</GenericFieldLabel>
<GenericFieldLabel label="Environment">{envId}</GenericFieldLabel>
{envId === VercelEnvironmentType.Preview && branchId && (
<GenericFieldLabel label="Preview Branch">{branchId}</GenericFieldLabel>
)}
</>
);
};

View File

@@ -10,6 +10,7 @@ import { AzureKeyVaultSyncDestinationSchema } from "./azure-key-vault-sync-desti
import { CamundaSyncDestinationSchema } from "./camunda-sync-destination-schema"; import { CamundaSyncDestinationSchema } from "./camunda-sync-destination-schema";
import { GcpSyncDestinationSchema } from "./gcp-sync-destination-schema"; import { GcpSyncDestinationSchema } from "./gcp-sync-destination-schema";
import { HumanitecSyncDestinationSchema } from "./humanitec-sync-destination-schema"; import { HumanitecSyncDestinationSchema } from "./humanitec-sync-destination-schema";
import { VercelSyncDestinationSchema } from "./vercel-sync-destination-schema";
const SecretSyncUnionSchema = z.discriminatedUnion("destination", [ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
AwsParameterStoreSyncDestinationSchema, AwsParameterStoreSyncDestinationSchema,
@@ -20,7 +21,8 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
AzureAppConfigurationSyncDestinationSchema, AzureAppConfigurationSyncDestinationSchema,
DatabricksSyncDestinationSchema, DatabricksSyncDestinationSchema,
HumanitecSyncDestinationSchema, HumanitecSyncDestinationSchema,
CamundaSyncDestinationSchema CamundaSyncDestinationSchema,
VercelSyncDestinationSchema
]); ]);
export const SecretSyncFormSchema = SecretSyncUnionSchema; export const SecretSyncFormSchema = SecretSyncUnionSchema;

View File

@@ -0,0 +1,18 @@
import { z } from "zod";
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { VercelEnvironmentType } from "@app/hooks/api/secretSyncs/types/vercel-sync";
export const VercelSyncDestinationSchema = BaseSecretSyncSchema().merge(
z.object({
destination: z.literal(SecretSync.Vercel),
destinationConfig: z.object({
app: z.string().trim().min(1, "Project required"),
appName: z.string().trim().min(1, "Project required"),
env: z.nativeEnum(VercelEnvironmentType).or(z.string()),
branch: z.string().trim().optional(),
teamId: z.string().trim()
})
})
);

View File

@@ -0,0 +1,159 @@
/* eslint-disable no-underscore-dangle */
import { forwardRef, InputHTMLAttributes } from "react";
import { InitialConfigType, LexicalComposer } from "@lexical/react/LexicalComposer";
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";
import { OnChangePlugin } from "@lexical/react/LexicalOnChangePlugin";
import { PlainTextPlugin } from "@lexical/react/LexicalPlainTextPlugin";
import { ReactNode } from "@tanstack/react-router";
import { cva, VariantProps } from "cva";
import { EditorState, LexicalEditor } from "lexical";
import { twMerge } from "tailwind-merge";
import { HighlightNode } from "./EditorHighlight";
import { EditorPlaceholderPlugin } from "./EditorPlaceholderPlugin";
// Catch any errors that occur during Lexical updates and log them
// or throw them as needed. If you don't throw them, Lexical will
// try to recover gracefully without losing user data.
function onError(error: Error) {
console.error(error);
}
const inputVariants = cva(
"input w-full py-[0.375rem] text-gray-400 placeholder:text-sm placeholder-gray-500 placeholder-opacity-50 outline-none focus:ring-2 hover:ring-bunker-400/60 duration-100",
{
variants: {
size: {
xs: ["text-xs"],
sm: ["text-sm"],
md: ["text-md"],
lg: ["text-lg"]
},
isRounded: {
true: ["rounded-md"],
false: ""
},
variant: {
filled: ["bg-mineshaft-900", "text-gray-400"],
outline: ["bg-transparent"],
plain: "bg-transparent outline-none"
},
isError: {
true: "focus:ring-red/50 placeholder-red-300",
false: "focus:ring-primary-400/50 focus:ring-1"
}
},
compoundVariants: []
}
);
const inputParentContainerVariants = cva("inline-flex font-inter items-center border relative", {
variants: {
isRounded: {
true: ["rounded-md"],
false: ""
},
isError: {
true: "border-red",
false: "border-mineshaft-500"
},
isFullWidth: {
true: "w-full",
false: ""
},
variant: {
filled: ["bg-bunker-800", "text-gray-400"],
outline: ["bg-transparent"],
plain: "border-none"
}
}
});
type Props = Omit<
InputHTMLAttributes<HTMLDivElement>,
"size" | "onChange" | "placeholder" | "aria-placeholder"
> &
VariantProps<typeof inputVariants> & {
children?: ReactNode;
namespace?: string;
placeholder?: string;
isFullWidth?: boolean;
isRequired?: boolean;
leftIcon?: ReactNode;
rightIcon?: ReactNode;
isDisabled?: boolean;
isReadOnly?: boolean;
containerClassName?: string;
onChange: (editorState: EditorState, editor: LexicalEditor, tags: Set<string>) => void;
initialValue?: string;
};
export const Editor = forwardRef<HTMLDivElement, Props>(
(
{
children,
namespace = "infisical-editor",
className,
containerClassName,
isRounded = true,
isFullWidth = true,
isDisabled,
isError = false,
isRequired,
leftIcon,
rightIcon,
variant = "filled",
size = "md",
isReadOnly,
placeholder,
onChange,
...props
},
ref
) => {
const initialConfig: InitialConfigType = {
namespace,
onError,
nodes: [HighlightNode]
};
return (
<div
className={inputParentContainerVariants({
isRounded,
isError,
isFullWidth,
variant,
className: containerClassName
})}
>
{leftIcon && <span className="absolute left-0 ml-3 text-sm">{leftIcon}</span>}
<LexicalComposer initialConfig={initialConfig}>
<PlainTextPlugin
contentEditable={
<ContentEditable
ref={ref}
aria-required={isRequired}
readOnly={isReadOnly}
disabled={isDisabled}
className={twMerge(
leftIcon ? "pl-10" : "pl-2.5",
rightIcon ? "pr-10" : "pr-2.5",
inputVariants({ className, isError, size, isRounded, variant })
)}
{...props}
placeholder={null}
/>
}
ErrorBoundary={LexicalErrorBoundary}
/>
<OnChangePlugin onChange={onChange} />
<EditorPlaceholderPlugin placeholder={placeholder} />
{children}
</LexicalComposer>
{rightIcon && <span className="absolute right-0 mr-3">{rightIcon}</span>}
</div>
);
}
);

View File

@@ -0,0 +1,127 @@
/* eslint-disable no-underscore-dangle,@typescript-eslint/class-methods-use-this */
import { useCallback, useEffect } from "react";
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
import { useLexicalTextEntity } from "@lexical/react/useLexicalTextEntity";
import {
$applyNodeReplacement,
EditorConfig,
LexicalNode,
SerializedTextNode,
Spread,
TextNode
} from "lexical";
type HighlightTheme = { contentClassName: string };
type Trigger = { startTrigger: string; endTrigger: string };
export type SerializedHighlightNode = Spread<
{
__highlightTheme: HighlightTheme;
__trigger: Trigger;
},
SerializedTextNode
>;
export class HighlightNode extends TextNode {
__highlightTheme: HighlightTheme;
__trigger: Trigger;
constructor(
text: string,
highlightTheme: HighlightTheme = {
contentClassName: "ph-no-capture text-yellow-200/80"
},
trigger: Trigger = { startTrigger: "${", endTrigger: "}" },
key?: string
) {
super(text, key);
this.__highlightTheme = highlightTheme;
this.__trigger = trigger;
}
static getType(): string {
return "highlight";
}
static clone(node: HighlightNode): HighlightNode {
return new HighlightNode(node.__text, node.__highlightTheme, node.__trigger, node.__key);
}
static importJSON(serializedNode: SerializedHighlightNode): HighlightNode {
return $applyNodeReplacement(new HighlightNode("")).updateFromJSON(serializedNode);
}
createDOM(config: EditorConfig): HTMLElement {
const dom = super.createDOM(config);
dom.style.cursor = "default";
dom.className = this.__highlightTheme.contentClassName;
return dom;
}
canInsertTextBefore(): boolean {
return false;
}
canInsertTextAfter(): boolean {
return false;
}
isTextEntity(): true {
return true;
}
}
export function $createKeywordNode(keyword: string = ""): HighlightNode {
return $applyNodeReplacement(new HighlightNode(keyword));
}
export function $isKeywordNode(node: LexicalNode | null | undefined): boolean {
return node instanceof HighlightNode;
}
type Props = {
contentClassName?: string;
startTrigger?: string;
endTrigger?: string;
};
export const EditorHighlightPlugin = ({
endTrigger = "}",
startTrigger = "${",
contentClassName = "ph-no-capture text-yellow-200/80"
}: Props) => {
const [editor] = useLexicalComposerContext();
useEffect(() => {
if (!editor.hasNodes([HighlightNode])) {
throw new Error("HighlightsPlugin: HighlightsNode not registered on editor");
}
}, [editor]);
const createKeywordNode = useCallback((textNode: TextNode): HighlightNode => {
return $applyNodeReplacement(
new HighlightNode(
textNode.getTextContent(),
{ contentClassName },
{ startTrigger, endTrigger }
)
);
}, []);
const getKeywordMatch = useCallback((text: string) => {
for (let i = 0; i < text.length; i += 1) {
if (text.slice(i, i + 2) === startTrigger) {
const closingBracketIndex = text.indexOf(endTrigger, i + 2);
if (closingBracketIndex !== -1) {
return { start: i, end: closingBracketIndex + 1 };
}
return null;
}
}
return null;
}, []);
useLexicalTextEntity<HighlightNode>(getKeywordMatch, HighlightNode, createKeywordNode);
return null;
};

Some files were not shown because too many files have changed in this diff Show More