Merge remote-tracking branch 'origin/main' into feat/camunda-app-connection-and-secret-sync
102
.github/workflows/codeql.yml
vendored
@@ -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}}"
|
|
||||||
253
.github/workflows/release_build_infisical_cli.yml
vendored
@@ -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 }}
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -594,6 +594,7 @@ export const scimServiceFactory = ({
|
|||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|
||||||
await orgMembershipDAL.updateById(
|
await orgMembershipDAL.updateById(
|
||||||
membership.id,
|
membership.id,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
141
backend/src/lib/search-resource/db.ts
Normal 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]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
43
backend/src/lib/search-resource/search.ts
Normal 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();
|
||||||
|
};
|
||||||
@@ -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(",")}` });
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.");
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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
|
||||||
|
});
|
||||||
@@ -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 };
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
4
backend/src/services/app-connection/vercel/index.ts
Normal 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";
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export enum VercelConnectionMethod {
|
||||||
|
ApiToken = "api-token"
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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()
|
||||||
|
});
|
||||||
@@ -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
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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}`
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
5
backend/src/services/secret-sync/vercel/index.ts
Normal 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";
|
||||||
@@ -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
|
||||||
|
};
|
||||||
12
backend/src/services/secret-sync/vercel/vercel-sync-enums.ts
Normal 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];
|
||||||
313
backend/src/services/secret-sync/vercel/vercel-sync-fns.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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)
|
||||||
|
});
|
||||||
40
backend/src/services/secret-sync/vercel/vercel-sync-types.ts
Normal 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];
|
||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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"
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Available"
|
||||||
|
openapi: "GET /api/v1/app-connections/vercel/available"
|
||||||
|
---
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Delete"
|
||||||
|
openapi: "DELETE /api/v1/app-connections/vercel/{connectionId}"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Get by ID"
|
||||||
|
openapi: "GET /api/v1/app-connections/vercel/{connectionId}"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Get by Name"
|
||||||
|
openapi: "GET /api/v1/app-connections/vercel/connection-name/{connectionName}"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "List"
|
||||||
|
openapi: "GET /api/v1/app-connections/vercel"
|
||||||
|
---
|
||||||
@@ -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>
|
||||||
4
docs/api-reference/endpoints/identities/search.mdx
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Search"
|
||||||
|
openapi: "POST /api/v1/identities/search"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Create"
|
||||||
|
openapi: "POST /api/v1/secret-syncs/vercel"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Delete"
|
||||||
|
openapi: "DELETE /api/v1/secret-syncs/vercel/{syncId}"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Get by ID"
|
||||||
|
openapi: "GET /api/v1/secret-syncs/vercel/{syncId}"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Get by Name"
|
||||||
|
openapi: "GET /api/v1/secret-syncs/vercel/sync-name/{syncName}"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Import Secrets"
|
||||||
|
openapi: "POST /api/v1/secret-syncs/vercel/{syncId}/import-secrets"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "List"
|
||||||
|
openapi: "GET /api/v1/secret-syncs/vercel"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Remove Secrets"
|
||||||
|
openapi: "POST /api/v1/secret-syncs/vercel/{syncId}/remove-secrets"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Sync Secrets"
|
||||||
|
openapi: "POST /api/v1/secret-syncs/vercel/{syncId}/sync-secrets"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Update"
|
||||||
|
openapi: "PATCH /api/v1/secret-syncs/vercel/{syncId}"
|
||||||
|
---
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
|
After Width: | Height: | Size: 335 KiB |
|
After Width: | Height: | Size: 233 KiB |
|
After Width: | Height: | Size: 243 KiB |
BIN
docs/images/app-connections/vercel/vercel-copy-token.png
Normal file
|
After Width: | Height: | Size: 116 KiB |
BIN
docs/images/app-connections/vercel/vercel-create-token.png
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
docs/images/app-connections/vercel/vercel-main-page.png
Normal file
|
After Width: | Height: | Size: 110 KiB |
BIN
docs/images/app-connections/vercel/vercel-settings-page.png
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
docs/images/app-connections/vercel/vercel-token-created.png
Normal file
|
After Width: | Height: | Size: 123 KiB |
BIN
docs/images/secret-syncs/vercel/select-vercel-option.png
Normal file
|
After Width: | Height: | Size: 260 KiB |
BIN
docs/images/secret-syncs/vercel/vercel-created.png
Normal file
|
After Width: | Height: | Size: 372 KiB |
BIN
docs/images/secret-syncs/vercel/vercel-destination.png
Normal file
|
After Width: | Height: | Size: 249 KiB |
BIN
docs/images/secret-syncs/vercel/vercel-details.png
Normal file
|
After Width: | Height: | Size: 244 KiB |
BIN
docs/images/secret-syncs/vercel/vercel-options.png
Normal file
|
After Width: | Height: | Size: 270 KiB |
BIN
docs/images/secret-syncs/vercel/vercel-review.png
Normal file
|
After Width: | Height: | Size: 257 KiB |
BIN
docs/images/secret-syncs/vercel/vercel-source.png
Normal file
|
After Width: | Height: | Size: 241 KiB |
97
docs/integrations/app-connections/vercel.mdx
Normal 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.
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Open API Tokens Tab">
|
||||||
|
Select the **API Tokens** tab from the left sidebar navigation menu.
|
||||||
|

|
||||||
|
</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>
|
||||||
|

|
||||||
|
</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.
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Token Created">
|
||||||
|
You should now see your newly created token in the list of API tokens on the Vercel dashboard.
|
||||||
|

|
||||||
|
</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.
|
||||||
|

|
||||||
|
2. Add Connection
|
||||||
|
|
||||||
|
Click the **+ Add Connection** button and select the **Vercel Connection** option from the available integrations.
|
||||||
|

|
||||||
|
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
|
||||||
|

|
||||||
|
4. Connection Created
|
||||||
|
|
||||||
|
After clicking Create, your **Vercel Connection** is established and ready to use with your Infisical projects.
|
||||||
|

|
||||||
|
</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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|

|
||||||
To create an identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**.
|
|
||||||
|
|
||||||

|
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.
|

|
||||||
|
|
||||||

|
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>
|

|
||||||
To learn more about each field of the Kubernetes native authentication method, see step 2 of [guide](/documentation/platform/identities/kubernetes-auth#guide).
|
|
||||||
</Info>
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
</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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
</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**.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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>
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
</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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</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>
|
||||||
|
|
||||||
|
|||||||
148
docs/integrations/secret-syncs/vercel.mdx
Normal 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.
|
||||||
|

|
||||||
|
|
||||||
|
2. Select the **Vercel** option.
|
||||||
|

|
||||||
|
|
||||||
|
3. Configure the **Source** from where secrets should be retrieved, then click **Next**.
|
||||||
|

|
||||||
|
|
||||||
|
- **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**.
|
||||||
|

|
||||||
|
|
||||||
|
- **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**.
|
||||||
|

|
||||||
|
|
||||||
|
- **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**.
|
||||||
|

|
||||||
|
|
||||||
|
- **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**.
|
||||||
|

|
||||||
|
|
||||||
|
8. If enabled, your Vercel Sync will begin syncing your secrets to the destination endpoint.
|
||||||
|

|
||||||
|
|
||||||
|
</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>
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
344
frontend/package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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't see the project you'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>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -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}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
);
|
||||||
159
frontend/src/components/v2/Editor/Editor.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
127
frontend/src/components/v2/Editor/EditorHighlight.tsx
Normal 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;
|
||||||
|
};
|
||||||