diff --git a/.env.example b/.env.example index 2025622dc..169e6db0d 100644 --- a/.env.example +++ b/.env.example @@ -1,21 +1,19 @@ # Keys -# Required keys for platform encryption/decryption ops -PRIVATE_KEY=replace_with_nacl_sk -PUBLIC_KEY=replace_with_nacl_pk -ENCRYPTION_KEY=replace_with_lengthy_secure_hex +# Required key for platform encryption/decryption ops +ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218 # JWT # Required secrets to sign JWT tokens -JWT_SIGNUP_SECRET=replace_with_lengthy_secure_hex -JWT_REFRESH_SECRET=replace_with_lengthy_secure_hex -JWT_AUTH_SECRET=replace_with_lengthy_secure_hex +JWT_SIGNUP_SECRET=3679e04ca949f914c03332aaaeba805a +JWT_REFRESH_SECRET=5f2f3c8f0159068dc2bbb3a652a716ff +JWT_AUTH_SECRET=4be6ba5602e0fa0ac6ac05c3cd4d247f +JWT_SERVICE_SECRET=f32f716d70a42c5703f4656015e76200 # JWT lifetime # Optional lifetimes for JWT tokens expressed in seconds or a string # describing a time span (e.g. 60, "2 days", "10h", "7d") JWT_AUTH_LIFETIME= JWT_REFRESH_LIFETIME= -JWT_SERVICE_SECRET= JWT_SIGNUP_LIFETIME= # Optional lifetimes for OTP expressed in seconds @@ -33,21 +31,28 @@ MONGO_PASSWORD=example # Website URL # Required - SITE_URL=http://localhost:8080 # Mail/SMTP -# Required to send emails -# By default, SMTP_HOST is set to smtp.gmail.com -SMTP_HOST=smtp.gmail.com -SMTP_NAME=Team -SMTP_USERNAME=team@infisical.com -SMTP_PASSWORD= +SMTP_HOST= # required +SMTP_USERNAME= # required +SMTP_PASSWORD= # required +SMTP_PORT=587 +SMTP_SECURE=false +SMTP_FROM_ADDRESS= # required +SMTP_FROM_NAME=Infisical # Integration # Optional only if integration is used -OAUTH_CLIENT_SECRET_HEROKU= -OAUTH_TOKEN_URL_HEROKU= +CLIENT_ID_HEROKU= +CLIENT_ID_VERCEL= +CLIENT_ID_NETLIFY= +CLIENT_ID_GITHUB= +CLIENT_SECRET_HEROKU= +CLIENT_SECRET_VERCEL= +CLIENT_SECRET_NETLIFY= +CLIENT_SECRET_GITHUB= +CLIENT_SLUG_VERCEL= # Sentry (optional) for monitoring errors SENTRY_DSN= diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 000000000..7a3d61558 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,3 @@ +node_modules +built +healthcheck.js diff --git a/.github/images/star-infisical.gif b/.github/images/star-infisical.gif index bb0752cb7..6d0789969 100644 Binary files a/.github/images/star-infisical.gif and b/.github/images/star-infisical.gif differ diff --git a/.github/resources/docker-compose.be-test.yml b/.github/resources/docker-compose.be-test.yml new file mode 100644 index 000000000..6efdd87f6 --- /dev/null +++ b/.github/resources/docker-compose.be-test.yml @@ -0,0 +1,30 @@ +version: '3' + +services: + backend: + container_name: infisical-backend-test + restart: unless-stopped + depends_on: + - mongo + image: infisical/backend:test + command: npm run start + environment: + - NODE_ENV=production + - MONGO_URL=mongodb://test:example@mongo:27017/?authSource=admin + - MONGO_USERNAME=test + - MONGO_PASSWORD=example + networks: + - infisical-test + + mongo: + container_name: infisical-mongo-test + image: mongo + restart: always + environment: + - MONGO_INITDB_ROOT_USERNAME=test + - MONGO_INITDB_ROOT_PASSWORD=example + networks: + - infisical-test + +networks: + infisical-test: diff --git a/.github/resources/healthcheck.sh b/.github/resources/healthcheck.sh new file mode 100755 index 000000000..bc28e3607 --- /dev/null +++ b/.github/resources/healthcheck.sh @@ -0,0 +1,26 @@ +# Name of the target container to check +container_name="$1" +# Timeout in seconds. Default: 60 +timeout=$((${2:-60})); + +if [ -z $container_name ]; then + echo "No container name specified"; + exit 1; +fi + +echo "Container: $container_name"; +echo "Timeout: $timeout sec"; + +try=0; +is_healthy="false"; +while [ $is_healthy != "true" ]; +do + try=$(($try + 1)); + printf "โ– "; + is_healthy=$(docker inspect --format='{{json .State.Health}}' $container_name | jq '.Status == "healthy"'); + sleep 1; + if [[ $try -eq $timeout ]]; then + echo " Container was not ready within timeout"; + exit 1; + fi +done diff --git a/.github/workflows/be-test-report.yml b/.github/workflows/be-test-report.yml new file mode 100644 index 000000000..bd57b377e --- /dev/null +++ b/.github/workflows/be-test-report.yml @@ -0,0 +1,41 @@ +name: "Backend Test Report" + +on: + workflow_run: + workflows: ["Check Backend Pull Request"] + types: + - completed + +jobs: + be-report: + name: Backend test report + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: ๐Ÿ“ Download test results + id: download-artifact + uses: dawidd6/action-download-artifact@v2 + with: + name: be-test-results + path: backend + workflow: check-be-pull-request.yml + workflow_conclusion: success + - name: ๐Ÿ“‹ Publish test results + uses: dorny/test-reporter@v1 + with: + name: Test Results + path: reports/jest-*.xml + reporter: jest-junit + working-directory: backend + - name: ๐Ÿ“‹ Publish coverage + uses: ArtiomTr/jest-coverage-report-action@v2 + id: coverage + with: + output: comment, report-markdown + coverage-file: coverage/report.json + github-token: ${{ secrets.GITHUB_TOKEN }} + working-directory: backend + - uses: marocchino/sticky-pull-request-comment@v2 + with: + message: ${{ steps.coverage.outputs.report }} diff --git a/.github/workflows/check-be-pull-request.yml b/.github/workflows/check-be-pull-request.yml new file mode 100644 index 000000000..8022a25bc --- /dev/null +++ b/.github/workflows/check-be-pull-request.yml @@ -0,0 +1,42 @@ +name: "Check Backend Pull Request" + +on: + pull_request: + types: [opened, synchronize] + paths: + - "backend/**" + - "!backend/README.md" + - "!backend/.*" + - "backend/.eslintrc.js" + +jobs: + check-be-pr: + name: Check + runs-on: ubuntu-latest + + steps: + - name: โ˜๏ธ Checkout source + uses: actions/checkout@v3 + - name: ๐Ÿ”ง Setup Node 16 + uses: actions/setup-node@v3 + with: + node-version: "16" + cache: "npm" + cache-dependency-path: backend/package-lock.json + - name: ๐Ÿ“ฆ Install dependencies + run: npm ci --only-production --ignore-scripts + working-directory: backend + - name: ๐Ÿงช Run tests + run: npm run test:ci + working-directory: backend + - name: ๐Ÿ“ Upload test results + uses: actions/upload-artifact@v3 + if: always() + with: + name: be-test-results + path: | + ./backend/reports + ./backend/coverage + - name: ๐Ÿ—๏ธ Run build + run: npm run build + working-directory: backend diff --git a/.github/workflows/check-fe-pull-request.yml b/.github/workflows/check-fe-pull-request.yml new file mode 100644 index 000000000..b91e6f060 --- /dev/null +++ b/.github/workflows/check-fe-pull-request.yml @@ -0,0 +1,41 @@ +name: Check Frontend Pull Request + +on: + pull_request: + types: [ opened, synchronize ] + paths: + - 'frontend/**' + - '!frontend/README.md' + - '!frontend/.*' + - 'frontend/.eslintrc.js' + + +jobs: + + check-fe-pr: + name: Check + runs-on: ubuntu-latest + + steps: + - + name: โ˜๏ธ Checkout source + uses: actions/checkout@v3 + - + name: ๐Ÿ”ง Setup Node 16 + uses: actions/setup-node@v3 + with: + node-version: '16' + cache: 'npm' + cache-dependency-path: frontend/package-lock.json + - + name: ๐Ÿ“ฆ Install dependencies + run: npm ci --only-production --ignore-scripts + working-directory: frontend + # - + # name: ๐Ÿงช Run tests + # run: npm run test:ci + # working-directory: frontend + - + name: ๐Ÿ—๏ธ Run build + run: npm run build + working-directory: frontend diff --git a/.github/workflows/close_inactive_issues.yml b/.github/workflows/close_inactive_issues.yml deleted file mode 100644 index 315c9e929..000000000 --- a/.github/workflows/close_inactive_issues.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Close inactive issues -on: - schedule: - - cron: "30 1 * * *" - -jobs: - close-issues: - runs-on: ubuntu-latest - permissions: - issues: write - pull-requests: write - steps: - - uses: actions/stale@v4 - with: - days-before-issue-stale: 30 - days-before-issue-close: 14 - stale-issue-label: "stale" - stale-issue-message: "This issue is stale because it has been open for 30 days with no activity." - close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale." - days-before-pr-stale: -1 - days-before-pr-close: -1 - repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 770fa7e83..1e836a178 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -3,35 +3,84 @@ name: Push to Docker Hub on: [workflow_dispatch] jobs: - docker: + backend-image: + name: Build backend image runs-on: ubuntu-latest + steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Set up QEMU + - name: โ˜๏ธ Checkout source + uses: actions/checkout@v3 + - name: ๐Ÿ”ง Set up QEMU uses: docker/setup-qemu-action@v2 - - - name: Set up Docker Buildx + - name: ๐Ÿ”ง Set up Docker Buildx uses: docker/setup-buildx-action@v2 - - - name: Login to Docker Hub + - name: ๐Ÿ‹ Login to Docker Hub uses: docker/login-action@v2 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Build and push backend + - name: ๐Ÿ“ฆ Build backend and export to Docker + uses: docker/build-push-action@v3 + with: + load: true + context: backend + tags: infisical/backend:test + - name: โป Spawn backend container and dependencies + run: | + docker compose -f .github/resources/docker-compose.be-test.yml up --wait --quiet-pull + - name: ๐Ÿงช Test backend image + run: | + ./.github/resources/healthcheck.sh infisical-backend-test + - name: โป Shut down backend container and dependencies + run: | + docker compose -f .github/resources/docker-compose.be-test.yml down + - name: ๐Ÿ—๏ธ Build backend and push uses: docker/build-push-action@v3 with: push: true context: backend - tags: infisical/backend:test - - - name: Build and push frontend + tags: infisical/backend:latest + platforms: linux/amd64,linux/arm64 + + frontend-image: + name: Build frontend image + runs-on: ubuntu-latest + + steps: + - name: โ˜๏ธ Checkout source + uses: actions/checkout@v3 + - name: ๐Ÿ”ง Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: ๐Ÿ”ง Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: ๐Ÿ‹ Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: ๐Ÿ“ฆ Build frontend and export to Docker + uses: docker/build-push-action@v3 + with: + load: true + context: frontend + tags: infisical/frontend:test + build-args: | + POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} + - name: โป Spawn frontend container + run: | + docker run -d --rm --name infisical-frontend-test infisical/frontend:test + - name: ๐Ÿงช Test frontend image + run: | + ./.github/resources/healthcheck.sh infisical-frontend-test + - name: โป Shut down frontend container + run: | + docker stop infisical-frontend-test + - name: ๐Ÿ—๏ธ Build frontend and push uses: docker/build-push-action@v3 with: push: true - file: frontend/Dockerfile.dev context: frontend - tags: infisical/frontend:test + tags: infisical/frontend:latest + platforms: linux/amd64,linux/arm64 + build-args: | + POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} diff --git a/.github/workflows/release_build.yml b/.github/workflows/release_build.yml index 695b0ea24..c5fd9034f 100644 --- a/.github/workflows/release_build.yml +++ b/.github/workflows/release_build.yml @@ -13,7 +13,7 @@ permissions: jobs: goreleaser: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v3 with: @@ -24,6 +24,15 @@ jobs: go-version: '>=1.19.3' cache: true cache-dependency-path: cli/go.sum + - name: libssl1.1 => libssl1.0-dev for OSXCross + run: | + echo 'deb http://security.ubuntu.com/ubuntu bionic-security main' | sudo tee -a /etc/apt/sources.list + sudo apt update && apt-cache policy libssl1.0-dev + sudo apt-get install libssl1.0-dev + - name: OSXCross for CGO Support + run: | + mkdir ../../osxcross + git clone https://github.com/plentico/osxcross-target.git ../../osxcross/target - uses: goreleaser/goreleaser-action@v2 with: distribution: goreleaser diff --git a/.github/workflows/release_docker_k8_operator.yaml b/.github/workflows/release_docker_k8_operator.yaml new file mode 100644 index 000000000..788d414b6 --- /dev/null +++ b/.github/workflows/release_docker_k8_operator.yaml @@ -0,0 +1,29 @@ +name: Release Docker image for K8 operator +on: [workflow_dispatch] + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: ๐Ÿ”ง Set up QEMU + uses: docker/setup-qemu-action@v1 + + - name: ๐Ÿ”ง Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: ๐Ÿ‹ Login to Docker Hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push + id: docker_build + uses: docker/build-push-action@v2 + with: + context: k8-operator + push: true + platforms: linux/amd64,linux/arm64 + tags: infisical/kubernetes-operator:latest \ No newline at end of file diff --git a/.gitignore b/.gitignore index f32a51384..6c4414313 100644 --- a/.gitignore +++ b/.gitignore @@ -25,7 +25,9 @@ node_modules .env # testing -/coverage +coverage +reports +junit.xml # next.js /.next/ diff --git a/.goreleaser.yaml b/.goreleaser.yaml index e83973397..5e94e0de6 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -7,12 +7,23 @@ # # you may remove this if you don't need go generate # - cd cli && go generate ./... builds: - - env: - - CGO_ENABLED=0 + - id: darwin-build binary: infisical - id: infisical + env: + - CGO_ENABLED=1 + - CC=/home/runner/work/osxcross/target/bin/o64-clang + - CXX=/home/runner/work/osxcross/target/bin/o64-clang++ goos: - darwin + ignore: + - goos: darwin + goarch: "386" + dir: ./cli + - id: all-other-builds + env: + - CGO_ENABLED=0 + binary: infisical + goos: - freebsd - linux - netbsd @@ -27,8 +38,6 @@ builds: - 6 - 7 ignore: - - goos: darwin - goarch: "386" - goos: windows goarch: "386" - goos: freebsd @@ -71,7 +80,7 @@ nfpms: - id: infisical package_name: infisical builds: - - infisical + - all-other-builds vendor: Infisical, Inc homepage: https://infisical.com/ maintainer: Infisical, Inc @@ -81,6 +90,7 @@ nfpms: - rpm - deb - apk + - archlinux bindir: /usr/bin scoop: bucket: diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 000000000..0b3d59a18 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,5 @@ + +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +npx lint-staged diff --git a/README.md b/README.md index 1b2d43c5f..3851b3c53 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- ifnisical + infisical infisical

@@ -27,6 +27,9 @@ Slack community channel + + Infisical Twitter + Dashboard @@ -52,9 +55,15 @@ And more. To quickly get started, visit our [get started guide](https://infisical.com/docs/getting-started/introduction). +

+ + + +

+ ## ๐Ÿ”ฅ What's cool about this? -Infisical makes secret management simple and end-to-end encrypted by default. We're on a mission to make it more accessible to all developers, not just security teams. +Infisical makes secret management simple and end-to-end encrypted by default. We're on a mission to make it more accessible to all developers, not just security teams. According to a [report](https://www.ekransystem.com/en/blog/secrets-management) in 2019, only 10% of organizations use secret management solutions despite all using digital secrets to some extent. @@ -66,14 +75,17 @@ We are currently working hard to make Infisical more extensive. Need any integra Whether it's big or small, we love contributions โค๏ธ Check out our guide to see how to [get started](https://infisical.com/docs/contributing/overview). -Not sure where to get started? [Book a free, non-pressure pairing sessions with one of our teammates](mailto:tony@infisical.com?subject=Pairing%20session&body=I'd%20like%20to%20do%20a%20pairing%20session!)! +Not sure where to get started? You can: + +- [Book a free, non-pressure pairing sessions with one of our teammates](mailto:tony@infisical.com?subject=Pairing%20session&body=I'd%20like%20to%20do%20a%20pairing%20session!)! +- Join our Slack, and ask us any questions there. ## ๐Ÿ’š Community & Support - [Slack](https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g) (For live discussion with the community and the Infisical team) - [GitHub Discussions](https://github.com/Infisical/infisical/discussions) (For help with building and deeper conversations about features) - [GitHub Issues](https://github.com/Infisical/infisical-cli/issues) (For any bugs and errors you encounter using Infisical) -- [Twitter](https://twitter.com/infisical) (Get news fast) +- [Twitter](https://twitter.com/infisical) (Get news fast) ## ๐Ÿฅ Status @@ -83,12 +95,6 @@ Not sure where to get started? [Book a free, non-pressure pairing sessions with We're currently in Public Alpha. -## ๐Ÿšจ Stay Up-to-Date - -Infisical officially launched as v.1.0 on November 21st, 2022. However, a lot of new features are coming very quickly. Watch **releases** of this repository to be notified about future updates: - -![infisical-star-github](https://github.com/Infisical/infisical/blob/main/.github/images/star-infisical.gif?raw=true) - ## ๐Ÿ”Œ Integrations We're currently setting the foundation and building [integrations](https://infisical.com/docs/integrations/overview) so secrets can be synced everywhere. Any help is welcome! :) @@ -122,10 +128,14 @@ We're currently setting the foundation and building [integrations](https://infis - ๐Ÿ”œ Vercel (https://github.com/Infisical/infisical/issues/60) + + โœ”๏ธ Vercel + - ๐Ÿ”œ GitLab CI/CD + + โœ”๏ธ Kubernetes + ๐Ÿ”œ Fly.io @@ -136,7 +146,9 @@ We're currently setting the foundation and building [integrations](https://infis ๐Ÿ”œ AWS - ๐Ÿ”œ GitHub Actions (https://github.com/Infisical/infisical/issues/54) + + โœ”๏ธ GitHub Actions + ๐Ÿ”œ Railway @@ -147,10 +159,10 @@ We're currently setting the foundation and building [integrations](https://infis ๐Ÿ”œ GCP - ๐Ÿ”œ Kubernetes + ๐Ÿ”œ GitLab CI/CD (https://github.com/Infisical/infisical/issues/134) - ๐Ÿ”œ CircleCI + ๐Ÿ”œ CircleCI (https://github.com/Infisical/infisical/issues/91) @@ -169,7 +181,23 @@ We're currently setting the foundation and building [integrations](https://infis ๐Ÿ”œ TravisCI - ๐Ÿ”œ Netlify (https://github.com/Infisical/infisical/issues/55) + + โœ”๏ธ Netlify + + + + ๐Ÿ”œ Railway + + + + + ๐Ÿ”œ Bitbucket + + + ๐Ÿ”œ Supabase + + + ๐Ÿ”œ Render (https://github.com/Infisical/infisical/issues/132) @@ -178,7 +206,6 @@ We're currently setting the foundation and building [integrations](https://infis - @@ -253,6 +280,18 @@ We're currently setting the foundation and building [integrations](https://infis + + + +
+ + โœ”๏ธ Fiber + + + + โœ”๏ธ Nuxt + +
@@ -260,7 +299,6 @@ We're currently setting the foundation and building [integrations](https://infis - ## ๐Ÿ˜ Open-source vs. paid This repo is entirely MIT licensed, with the exception of the `ee` directory which will contain premium enterprise features requiring a Infisical license in the future. We're currently focused on developing non-enterprise offerings first that should suit most use-cases. @@ -269,6 +307,12 @@ This repo is entirely MIT licensed, with the exception of the `ee` directory whi Looking to report a security vulnerability? Please don't post about it in GitHub issue. Instead, refer to our [SECURITY.md](./SECURITY.md) file. +## ๐Ÿšจ Stay Up-to-Date + +Infisical officially launched as v.1.0 on November 21st, 2022. However, a lot of new features are coming very quickly. Watch **releases** of this repository to be notified about future updates: + +![infisical-star-github](https://github.com/Infisical/infisical/blob/main/.github/images/star-infisical.gif?raw=true) + ## ๐Ÿฆธ Contributors [//]: contributor-faces @@ -277,4 +321,4 @@ Looking to report a security vulnerability? Please don't post about it in GitHub - + diff --git a/backend/.eslintrc b/backend/.eslintrc index a0c373ecb..c1ca1a1eb 100644 --- a/backend/.eslintrc +++ b/backend/.eslintrc @@ -1,18 +1,12 @@ { - "root": true, - "parser": "@typescript-eslint/parser", - "plugins": [ - "@typescript-eslint", - "prettier" - ], - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended", - "prettier" - ], - "rules": { - "no-console": 2, - "prettier/prettier": 2 - } -} \ No newline at end of file + "parser": "@typescript-eslint/parser", + "plugins": ["@typescript-eslint"], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" + ], + "rules": { + "no-console": 2 + } +} diff --git a/backend/.prettierrc b/backend/.prettierrc deleted file mode 100644 index a5a98113e..000000000 --- a/backend/.prettierrc +++ /dev/null @@ -1,7 +0,0 @@ -{ - "semi": true, - "trailingComma": "none", - "singleQuote": true, - "printWidth": 80, - "useTabs": true - } diff --git a/backend/Dockerfile b/backend/Dockerfile index 2cc270947..85b7204fe 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -2,11 +2,14 @@ FROM node:16-bullseye-slim WORKDIR /app -COPY package*.json . +COPY package.json package-lock.json ./ -RUN npm install +RUN npm ci --only-production --ignore-scripts COPY . . -CMD ["npm", "run", "start"] +HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \ + CMD node healthcheck.js + +CMD ["npm", "run", "start"] diff --git a/backend/__tests__/healthcheck.test.ts b/backend/__tests__/healthcheck.test.ts new file mode 100644 index 000000000..234d2d8eb --- /dev/null +++ b/backend/__tests__/healthcheck.test.ts @@ -0,0 +1,19 @@ +import { server } from '../src/app'; +import { describe, expect, it, beforeAll, afterAll } from '@jest/globals'; +import supertest from 'supertest'; +import { setUpHealthEndpoint } from '../src/services/health'; + +const requestWithSupertest = supertest(server); +describe('Healthcheck endpoint', () => { + beforeAll(async () => { + setUpHealthEndpoint(server); + }); + afterAll(async () => { + server.close(); + }); + + it('GET /healthcheck should return OK', async () => { + const res = await requestWithSupertest.get('/healthcheck'); + expect(res.status).toEqual(200); + }); +}); diff --git a/backend/environment.d.ts b/backend/environment.d.ts index 33827fb2b..291a33550 100644 --- a/backend/environment.d.ts +++ b/backend/environment.d.ts @@ -14,12 +14,16 @@ declare global { JWT_SIGNUP_SECRET: string; MONGO_URL: string; NODE_ENV: 'development' | 'staging' | 'testing' | 'production'; - OAUTH_CLIENT_SECRET_HEROKU: string; - OAUTH_TOKEN_URL_HEROKU: string; + VERBOSE_ERROR_OUTPUT: string; + LOKI_HOST: string; + CLIENT_ID_HEROKU: string; + CLIENT_ID_VERCEL: string; + CLIENT_ID_NETLIFY: string; + CLIENT_SECRET_HEROKU: string; + CLIENT_SECRET_VERCEL: string; + CLIENT_SECRET_NETLIFY: string; POSTHOG_HOST: string; POSTHOG_PROJECT_API_KEY: string; - PRIVATE_KEY: string; - PUBLIC_KEY: string; SENTRY_DSN: string; SITE_URL: string; SMTP_HOST: string; diff --git a/backend/healthcheck.js b/backend/healthcheck.js new file mode 100644 index 000000000..8cb3dfcaa --- /dev/null +++ b/backend/healthcheck.js @@ -0,0 +1,24 @@ +const http = require('http'); +const PORT = process.env.PORT || 4000; +const options = { + host: 'localhost', + port: PORT, + timeout: 2000, + path: '/healthcheck' +}; + +const healthCheck = http.request(options, (res) => { + console.log(`HEALTHCHECK STATUS: ${res.statusCode}`); + if (res.statusCode == 200) { + process.exit(0); + } else { + process.exit(1); + } +}); + +healthCheck.on('error', function (err) { + console.error(`HEALTH CHECK ERROR: ${err}`); + process.exit(1); +}); + +healthCheck.end(); diff --git a/backend/package-lock.json b/backend/package-lock.json index 63a3a3060..3f555b8de 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -9,9 +9,12 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@godaddy/terminus": "^4.11.2", + "@octokit/rest": "^19.0.5", "@sentry/node": "^7.14.0", - "@sentry/tracing": "^7.14.0", + "@sentry/tracing": "^7.19.0", "@types/crypto-js": "^4.1.1", + "@types/libsodium-wrappers": "^0.7.10", "axios": "^1.1.3", "bigint-conversion": "^2.2.2", "cookie-parser": "^1.4.6", @@ -19,45 +22,51 @@ "crypto-js": "^4.1.1", "dotenv": "^16.0.1", "express": "^4.18.1", - "express-rate-limit": "^6.5.1", + "express-rate-limit": "^6.7.0", "express-validator": "^6.14.2", "handlebars": "^4.7.7", "helmet": "^5.1.1", - "jsonwebtoken": "^8.5.1", + "jsonwebtoken": "^9.0.0", "jsrp": "^0.2.4", - "mongoose": "^6.7.1", + "libsodium-wrappers": "^0.7.10", + "mongoose": "^6.7.2", "nodemailer": "^6.8.0", - "posthog-node": "^2.1.0", - "query-string": "^7.1.1", + "posthog-node": "^2.2.2", + "query-string": "^7.1.3", "rimraf": "^3.0.2", "stripe": "^10.7.0", "swagger-jsdoc": "^6.2.5", "swagger-ui-express": "^4.6.0", "tweetnacl": "^1.0.3", "tweetnacl-util": "^0.15.1", - "typescript": "^4.8.4" + "typescript": "^4.9.3", + "winston": "^3.8.2", + "winston-loki": "^6.0.6" }, "devDependencies": { + "@jest/globals": "^29.3.1", "@posthog/plugin-scaffold": "^1.3.4", "@types/cookie-parser": "^1.4.3", "@types/cors": "^2.8.12", "@types/express": "^4.17.14", + "@types/jest": "^29.2.4", "@types/jsonwebtoken": "^8.5.9", "@types/node": "^18.11.3", "@types/nodemailer": "^6.4.6", + "@types/supertest": "^2.0.12", "@types/swagger-jsdoc": "^6.0.1", "@types/swagger-ui-express": "^4.1.3", "@typescript-eslint/eslint-plugin": "^5.40.1", "@typescript-eslint/parser": "^5.40.1", + "cross-env": "^7.0.3", "eslint": "^8.26.0", - "eslint-config-prettier": "^8.5.0", - "eslint-plugin-prettier": "^4.2.1", - "husky": "^8.0.1", "install": "^0.13.0", "jest": "^29.3.1", + "jest-junit": "^15.0.0", "nodemon": "^2.0.19", "npm": "^8.19.3", - "prettier": "^2.7.1", + "supertest": "^6.3.3", + "ts-jest": "^29.0.3", "ts-node": "^10.9.1" } }, @@ -1118,6 +1127,7 @@ "version": "3.188.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-base64-browser/-/util-base64-browser-3.188.0.tgz", "integrity": "sha512-qlH+5NZBLiyKziL335BEPedYxX6j+p7KFRWXvDQox9S+s+gLCayednpK+fteOhBenCcR9fUZOVuAPScy1I8qCg==", + "deprecated": "The package @aws-sdk/util-base64-browser has been renamed to @aws-sdk/util-base64. Please install the renamed package.", "optional": true, "dependencies": { "tslib": "^2.3.1" @@ -1133,6 +1143,7 @@ "version": "3.201.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-base64-node/-/util-base64-node-3.201.0.tgz", "integrity": "sha512-ydZqNpB3l5kiicInpPDExPb5xHI7uyVIa1vMupnuIrJ412iNb0F2+K8LlFynzw6fSJShVKnqFcWOYRA96z1iIw==", + "deprecated": "The package @aws-sdk/util-base64-node has been renamed to @aws-sdk/util-base64. Please install the renamed package.", "optional": true, "dependencies": { "@aws-sdk/util-buffer-from": "3.201.0", @@ -2036,6 +2047,14 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -2048,6 +2067,16 @@ "node": ">=12" } }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, "node_modules/@eslint/eslintrc": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.3.tgz", @@ -2071,6 +2100,14 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@godaddy/terminus": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/@godaddy/terminus/-/terminus-4.11.2.tgz", + "integrity": "sha512-e/kbOWpGKME42eltM/wXM3RxSUOrfureZxEd6Dt6NXyFoJ7E8lnmm7znXydJsL3B7ky4HRFZI+eHrep54NZbeQ==", + "dependencies": { + "stoppable": "^1.1.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.7", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz", @@ -2597,6 +2634,21 @@ "maxmind": "^4.2.0" } }, + "node_modules/@napi-rs/snappy-darwin-arm64": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-darwin-arm64/-/snappy-darwin-arm64-7.1.1.tgz", + "integrity": "sha512-3LZyoAw3Qa5F7sCCTkSkhmGlydwUKU6L3Jl46eKHO2Ctm8Gcjxww6T7MfwlwGZ6JqAM6d1d++WLzUZPCGXVmag==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2632,6 +2684,153 @@ "node": ">= 8" } }, + "node_modules/@octokit/auth-token": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-3.0.2.tgz", + "integrity": "sha512-pq7CwIMV1kmzkFTimdwjAINCXKTajZErLB4wMLYapR2nuB/Jpr66+05wOTZMSCBXP6n4DdDWT2W19Bm17vU69Q==", + "dependencies": { + "@octokit/types": "^8.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@octokit/core": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-4.1.0.tgz", + "integrity": "sha512-Czz/59VefU+kKDy+ZfDwtOIYIkFjExOKf+HA92aiTZJ6EfWpFzYQWw0l54ji8bVmyhc+mGaLUbSUmXazG7z5OQ==", + "dependencies": { + "@octokit/auth-token": "^3.0.0", + "@octokit/graphql": "^5.0.0", + "@octokit/request": "^6.0.0", + "@octokit/request-error": "^3.0.0", + "@octokit/types": "^8.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@octokit/endpoint": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-7.0.3.tgz", + "integrity": "sha512-57gRlb28bwTsdNXq+O3JTQ7ERmBTuik9+LelgcLIVfYwf235VHbN9QNo4kXExtp/h8T423cR5iJThKtFYxC7Lw==", + "dependencies": { + "@octokit/types": "^8.0.0", + "is-plain-object": "^5.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@octokit/graphql": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-5.0.4.tgz", + "integrity": "sha512-amO1M5QUQgYQo09aStR/XO7KAl13xpigcy/kI8/N1PnZYSS69fgte+xA4+c2DISKqUZfsh0wwjc2FaCt99L41A==", + "dependencies": { + "@octokit/request": "^6.0.0", + "@octokit/types": "^8.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-14.0.0.tgz", + "integrity": "sha512-HNWisMYlR8VCnNurDU6os2ikx0s0VyEjDYHNS/h4cgb8DeOxQ0n72HyinUtdDVxJhFy3FWLGl0DJhfEWk3P5Iw==" + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-5.0.1.tgz", + "integrity": "sha512-7A+rEkS70pH36Z6JivSlR7Zqepz3KVucEFVDnSrgHXzG7WLAzYwcHZbKdfTXHwuTHbkT1vKvz7dHl1+HNf6Qyw==", + "dependencies": { + "@octokit/types": "^8.0.0" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "@octokit/core": ">=4" + } + }, + "node_modules/@octokit/plugin-request-log": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz", + "integrity": "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==", + "peerDependencies": { + "@octokit/core": ">=3" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-6.7.0.tgz", + "integrity": "sha512-orxQ0fAHA7IpYhG2flD2AygztPlGYNAdlzYz8yrD8NDgelPfOYoRPROfEyIe035PlxvbYrgkfUZIhSBKju/Cvw==", + "dependencies": { + "@octokit/types": "^8.0.0", + "deprecation": "^2.3.1" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "@octokit/core": ">=3" + } + }, + "node_modules/@octokit/request": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-6.2.2.tgz", + "integrity": "sha512-6VDqgj0HMc2FUX2awIs+sM6OwLgwHvAi4KCK3mT2H2IKRt6oH9d0fej5LluF5mck1lRR/rFWN0YIDSYXYSylbw==", + "dependencies": { + "@octokit/endpoint": "^7.0.0", + "@octokit/request-error": "^3.0.0", + "@octokit/types": "^8.0.0", + "is-plain-object": "^5.0.0", + "node-fetch": "^2.6.7", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@octokit/request-error": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-3.0.2.tgz", + "integrity": "sha512-WMNOFYrSaX8zXWoJg9u/pKgWPo94JXilMLb2VManNOby9EZxrQaBe/QSC4a1TzpAlpxofg2X/jMnCyZgL6y7eg==", + "dependencies": { + "@octokit/types": "^8.0.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@octokit/rest": { + "version": "19.0.5", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-19.0.5.tgz", + "integrity": "sha512-+4qdrUFq2lk7Va+Qff3ofREQWGBeoTKNqlJO+FGjFP35ZahP+nBenhZiGdu8USSgmq4Ky3IJ/i4u0xbLqHaeow==", + "dependencies": { + "@octokit/core": "^4.1.0", + "@octokit/plugin-paginate-rest": "^5.0.0", + "@octokit/plugin-request-log": "^1.0.4", + "@octokit/plugin-rest-endpoint-methods": "^6.7.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@octokit/types": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-8.0.0.tgz", + "integrity": "sha512-65/TPpOJP1i3K4lBJMnWqPUJ6zuOtzhtagDvydAWbEXpbFYA0oMKKyLb95NFZZP0lSh/4b6K+DQlzvYQJQQePg==", + "dependencies": { + "@octokit/openapi-types": "^14.0.0" + } + }, "node_modules/@posthog/plugin-scaffold": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/@posthog/plugin-scaffold/-/plugin-scaffold-1.3.4.tgz", @@ -2641,27 +2840,68 @@ "@maxmind/geoip2-node": "^3.4.0" } }, - "node_modules/@sentry/core": { - "version": "7.17.4", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.17.4.tgz", - "integrity": "sha512-U3ABSJBKGK8dJ01nEG2+qNOb6Wv7U3VqoajiZxfV4lpPWNFGCoEhiTytxBlFTOCmdUH8209zSZiWJZaDLy+TSA==", + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", "dependencies": { - "@sentry/types": "7.17.4", - "@sentry/utils": "7.17.4", - "tslib": "^1.9.3" - }, - "engines": { - "node": ">=8" + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" } }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, "node_modules/@sentry/node": { - "version": "7.17.4", - "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.17.4.tgz", - "integrity": "sha512-cR+Gsir9c/tzFWxvk4zXkMQy6tNRHEYixHrb88XIjZVYDqDS9l2/bKs5nJusdmaUeLtmPp5Et2o7RJyS7gvKTQ==", + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.19.0.tgz", + "integrity": "sha512-yG7Tx32WqOkEHVotFLrumCcT9qlaSDTkFNZ+yLSvZXx74ifsE781DzBA9W7K7bBdYO3op+p2YdsOKzf3nPpAyQ==", "dependencies": { - "@sentry/core": "7.17.4", - "@sentry/types": "7.17.4", - "@sentry/utils": "7.17.4", + "@sentry/core": "7.19.0", + "@sentry/types": "7.19.0", + "@sentry/utils": "7.19.0", "cookie": "^0.4.1", "https-proxy-agent": "^5.0.0", "lru_map": "^0.3.3", @@ -2671,34 +2911,80 @@ "node": ">=8" } }, - "node_modules/@sentry/tracing": { - "version": "7.17.4", - "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-7.17.4.tgz", - "integrity": "sha512-9Fz6DI16ddnd970mlB5MiCNRSmSXp4SVZ1Yv3L22oS3kQeNxjBTE+htYNwJzSPrQp9aL/LqTYwlnrCy24u9XQA==", + "node_modules/@sentry/node/node_modules/@sentry/core": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.19.0.tgz", + "integrity": "sha512-YF9cTBcAnO4R44092BJi5Wa2/EO02xn2ziCtmNgAVTN2LD31a/YVGxGBt/FDr4Y6yeuVehaqijVVvtpSmXrGJw==", "dependencies": { - "@sentry/core": "7.17.4", - "@sentry/types": "7.17.4", - "@sentry/utils": "7.17.4", + "@sentry/types": "7.19.0", + "@sentry/utils": "7.19.0", "tslib": "^1.9.3" }, "engines": { "node": ">=8" } }, - "node_modules/@sentry/types": { - "version": "7.17.4", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.17.4.tgz", - "integrity": "sha512-QJj8vO4AtxuzQfJIzDnECSmoxwnS+WJsm1Ta2Cwdy+TUCBJyWpW7aIJJGta76zb9gNPGb3UcAbeEjhMJBJeRMQ==", + "node_modules/@sentry/node/node_modules/@sentry/types": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.19.0.tgz", + "integrity": "sha512-oGRAT6lfzoKrxO1mvxiSj0XHxWPd6Gd1wpPGuu6iJo03xgWDS+MIlD1h2unqL4N5fAzLjzmbC2D2lUw50Kn2pA==", "engines": { "node": ">=8" } }, - "node_modules/@sentry/utils": { - "version": "7.17.4", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.17.4.tgz", - "integrity": "sha512-ioG0ANy8uiWzig82/e7cc+6C9UOxkyBzJDi1luoQVDH6P0/PvM8GzVU+1iUVUipf8+OL1Jh09GrWnd5wLm3XNQ==", + "node_modules/@sentry/node/node_modules/@sentry/utils": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.19.0.tgz", + "integrity": "sha512-2L6lq+c9Ol2uiRxQDdcgoapmHJp24MhMN0gIkn2alSfMJ+ls6bGXzQHx6JAIdoOiwFQXRZHKL9ecfAc8O+vItA==", "dependencies": { - "@sentry/types": "7.17.4", + "@sentry/types": "7.19.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/tracing": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-7.19.0.tgz", + "integrity": "sha512-SWY17M3TsgBePaGowUcSqBwaT0TJQzuNexVnLojuU0k6F57L9hubvP9zaoosoCfARXQ/3NypAFWnlJyf570rFQ==", + "dependencies": { + "@sentry/core": "7.19.0", + "@sentry/types": "7.19.0", + "@sentry/utils": "7.19.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/tracing/node_modules/@sentry/core": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.19.0.tgz", + "integrity": "sha512-YF9cTBcAnO4R44092BJi5Wa2/EO02xn2ziCtmNgAVTN2LD31a/YVGxGBt/FDr4Y6yeuVehaqijVVvtpSmXrGJw==", + "dependencies": { + "@sentry/types": "7.19.0", + "@sentry/utils": "7.19.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/tracing/node_modules/@sentry/types": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.19.0.tgz", + "integrity": "sha512-oGRAT6lfzoKrxO1mvxiSj0XHxWPd6Gd1wpPGuu6iJo03xgWDS+MIlD1h2unqL4N5fAzLjzmbC2D2lUw50Kn2pA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/tracing/node_modules/@sentry/utils": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.19.0.tgz", + "integrity": "sha512-2L6lq+c9Ol2uiRxQDdcgoapmHJp24MhMN0gIkn2alSfMJ+ls6bGXzQHx6JAIdoOiwFQXRZHKL9ecfAc8O+vItA==", + "dependencies": { + "@sentry/types": "7.19.0", "tslib": "^1.9.3" }, "engines": { @@ -2822,6 +3108,12 @@ "@types/express": "*" } }, + "node_modules/@types/cookiejar": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.2.tgz", + "integrity": "sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==", + "dev": true + }, "node_modules/@types/cors": { "version": "2.8.12", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", @@ -2889,6 +3181,16 @@ "@types/istanbul-lib-report": "*" } }, + "node_modules/@types/jest": { + "version": "29.2.4", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.2.4.tgz", + "integrity": "sha512-PipFB04k2qTRPePduVLTRiPzQfvMeLwUN3Z21hsAKaB/W9IIzgB2pizCL466ftJlcyZqnHoC9ZHpxLGl3fS86A==", + "dev": true, + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, "node_modules/@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", @@ -2903,6 +3205,16 @@ "@types/node": "*" } }, + "node_modules/@types/libsodium-wrappers": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/@types/libsodium-wrappers/-/libsodium-wrappers-0.7.10.tgz", + "integrity": "sha512-BqI9B92u+cM3ccp8mpHf+HzJ8fBlRwdmyd6+fz3p99m3V6ifT5O3zmOMi612PGkpeFeG/G6loxUnzlDNhfjPSA==" + }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" + }, "node_modules/@types/mime": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", @@ -2924,9 +3236,9 @@ } }, "node_modules/@types/prettier": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.1.tgz", - "integrity": "sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow==", + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz", + "integrity": "sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg==", "dev": true }, "node_modules/@types/qs": { @@ -2963,6 +3275,25 @@ "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", "dev": true }, + "node_modules/@types/superagent": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-4.1.16.tgz", + "integrity": "sha512-tLfnlJf6A5mB6ddqF159GqcDizfzbMUB1/DeT59/wBNqzRTNNKsaw79A/1TZ84X+f/EwWH8FeuSkjlCLyqS/zQ==", + "dev": true, + "dependencies": { + "@types/cookiejar": "*", + "@types/node": "*" + } + }, + "node_modules/@types/supertest": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-2.0.12.tgz", + "integrity": "sha512-X3HPWTwXRerBZS7Mo1k6vMVR1Z6zmJcDVn5O/31whe0tnjE4te6ZJSJGq1RiqHPjzPdMTfjCFogDJmwng9xHaQ==", + "dev": true, + "dependencies": { + "@types/superagent": "*" + } + }, "node_modules/@types/swagger-jsdoc": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/@types/swagger-jsdoc/-/swagger-jsdoc-6.0.1.tgz", @@ -3359,6 +3690,12 @@ "node": ">=8" } }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true + }, "node_modules/assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", @@ -3368,6 +3705,19 @@ "node": ">=0.8" } }, + "node_modules/async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" + }, + "node_modules/async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -3498,6 +3848,11 @@ } ] }, + "node_modules/before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==" + }, "node_modules/bigint-conversion": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/bigint-conversion/-/bigint-conversion-2.2.2.tgz", @@ -3606,6 +3961,18 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/bser": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", @@ -3626,6 +3993,17 @@ "node": ">=6.9.0" } }, + "node_modules/btoa": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", + "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==", + "bin": { + "btoa": "bin/btoa.js" + }, + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -3855,6 +4233,15 @@ "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", "dev": true }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3870,8 +4257,38 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } }, "node_modules/combined-stream": { "version": "1.0.8", @@ -3884,13 +4301,11 @@ "node": ">= 0.8" } }, - "node_modules/commander": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", - "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==", - "engines": { - "node": ">= 6" - } + "node_modules/component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true }, "node_modules/concat-map": { "version": "0.0.1", @@ -3955,6 +4370,12 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "node_modules/cookiejar": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.3.tgz", + "integrity": "sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ==", + "dev": true + }, "node_modules/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -3991,6 +4412,24 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -4027,9 +4466,9 @@ } }, "node_modules/decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", "engines": { "node": ">=0.10" } @@ -4079,6 +4518,11 @@ "node": ">= 0.8" } }, + "node_modules/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" + }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -4097,6 +4541,16 @@ "node": ">=8" } }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -4183,6 +4637,11 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" + }, "node_modules/encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -4282,39 +4741,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint-config-prettier": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz", - "integrity": "sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==", - "dev": true, - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/eslint-plugin-prettier": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz", - "integrity": "sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==", - "dev": true, - "dependencies": { - "prettier-linter-helpers": "^1.0.0" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "eslint": ">=7.28.0", - "prettier": ">=2.0.0" - }, - "peerDependenciesMeta": { - "eslint-config-prettier": { - "optional": true - } - } - }, "node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -4573,9 +4999,9 @@ } }, "node_modules/express-rate-limit": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.6.0.tgz", - "integrity": "sha512-HFN2+4ZGdkQOS8Qli4z6knmJFnw6lZed67o6b7RGplWeb1Z0s8VXaj3dUgPIdm9hrhZXTRpCTHXA0/2Eqex0vA==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.7.0.tgz", + "integrity": "sha512-vhwIdRoqcYB/72TK3tRZI+0ttS8Ytrk24GfmsxDXK9o9IhHNO5bXRiXQSExPQ4GbaE5tvIS7j1SGrxsuWs+sGA==", "engines": { "node": ">= 12.9.0" }, @@ -4631,12 +5057,6 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, - "node_modules/fast-diff": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", - "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", - "dev": true - }, "node_modules/fast-glob": { "version": "3.2.12", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", @@ -4677,6 +5097,12 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true + }, "node_modules/fast-xml-parser": { "version": "4.0.11", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.0.11.tgz", @@ -4711,6 +5137,11 @@ "bser": "2.1.1" } }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -4808,6 +5239,11 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" + }, "node_modules/follow-redirects": { "version": "1.15.2", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", @@ -4840,6 +5276,21 @@ "node": ">= 6" } }, + "node_modules/formidable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.1.tgz", + "integrity": "sha512-0EcS9wCFEzLvfiks7omJ+SiYJAiD+TzK4Pcw1UlUoGnhUxDcMKjt0P7x8wEb0u6OHu8Nb98WG3nxtlF5C7bvUQ==", + "dev": true, + "dependencies": { + "dezalgo": "^1.0.4", + "hexoid": "^1.0.0", + "once": "^1.4.0", + "qs": "^6.11.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -5094,6 +5545,15 @@ "node": ">=12.0.0" } }, + "node_modules/hexoid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", + "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -5136,21 +5596,6 @@ "node": ">=10.17.0" } }, - "node_modules/husky": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.1.tgz", - "integrity": "sha512-xs7/chUH/CKdOCs7Zy0Aev9e/dKOMZf3K1Az1nar3tzlv0jfqnYtu235bstsWTmXOR0EfINrPa97yy4Lz6RiKw==", - "dev": true, - "bin": { - "husky": "lib/bin.js" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/typicode" - } - }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -5373,11 +5818,18 @@ "node": ">=8" } }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, "engines": { "node": ">=8" }, @@ -5708,6 +6160,21 @@ "fsevents": "^2.3.2" } }, + "node_modules/jest-junit": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/jest-junit/-/jest-junit-15.0.0.tgz", + "integrity": "sha512-Z5sVX0Ag3HZdMUnD5DFlG+1gciIFSy7yIVPhOdGUi8YJaI9iLvvBb530gtQL2CHmv0JJeiwRZenr0VrSR7frvg==", + "dev": true, + "dependencies": { + "mkdirp": "^1.0.4", + "strip-ansi": "^6.0.1", + "uuid": "^8.3.2", + "xml": "^1.0.1" + }, + "engines": { + "node": ">=10.12.0" + } + }, "node_modules/jest-leak-detector": { "version": "29.3.1", "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.3.1.tgz", @@ -6089,32 +6556,18 @@ } }, "node_modules/jsonwebtoken": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", - "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", + "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", "dependencies": { "jws": "^3.2.2", - "lodash.includes": "^4.3.0", - "lodash.isboolean": "^3.0.3", - "lodash.isinteger": "^4.0.4", - "lodash.isnumber": "^3.0.3", - "lodash.isplainobject": "^4.0.6", - "lodash.isstring": "^4.0.1", - "lodash.once": "^4.0.0", + "lodash": "^4.17.21", "ms": "^2.1.1", - "semver": "^5.6.0" + "semver": "^7.3.8" }, "engines": { - "node": ">=4", - "npm": ">=1.4.28" - } - }, - "node_modules/jsonwebtoken/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "bin": { - "semver": "bin/semver" + "node": ">=12", + "npm": ">=6" } }, "node_modules/jsprim": { @@ -6175,6 +6628,11 @@ "node": ">=6" } }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -6197,6 +6655,19 @@ "node": ">= 0.8.0" } }, + "node_modules/libsodium": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.10.tgz", + "integrity": "sha512-eY+z7hDrDKxkAK+QKZVNv92A5KYkxfvIshtBJkmg5TSiCnYqZP3i9OO9whE79Pwgm4jGaoHgkM4ao/b9Cyu4zQ==" + }, + "node_modules/libsodium-wrappers": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.7.10.tgz", + "integrity": "sha512-pO3F1Q9NPLB/MWIhehim42b/Fwb30JNScCNh8TcQ/kIc+qGLQch8ag8wb0keK3EP5kbGakk1H8Wwo7v+36rNQg==", + "dependencies": { + "libsodium": "^0.7.0" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -6223,45 +6694,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, - "node_modules/lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" - }, - "node_modules/lodash.includes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" - }, - "node_modules/lodash.isboolean": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" - }, - "node_modules/lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" - }, - "node_modules/lodash.isinteger": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", - "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" - }, - "node_modules/lodash.isnumber": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", - "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" - }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" - }, - "node_modules/lodash.isstring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true }, "node_modules/lodash.merge": { "version": "4.6.2", @@ -6269,15 +6706,22 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, - "node_modules/lodash.mergewith": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", - "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==" + "node_modules/logform": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.4.2.tgz", + "integrity": "sha512-W4c9himeAwXEdZ05dQNerhFz2XG80P9Oj0loPUMV23VC2it0orMHQhJm4hdnnor3rd1HsGf6a2lPwBM1zeXHGw==", + "dependencies": { + "@colors/colors": "1.5.0", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + } }, - "node_modules/lodash.once": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + "node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" }, "node_modules/lru_map": { "version": "0.3.3", @@ -6288,7 +6732,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -6484,6 +6927,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/mmdb-lib": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/mmdb-lib/-/mmdb-lib-2.0.2.tgz", @@ -6522,9 +6977,9 @@ } }, "node_modules/mongoose": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.7.1.tgz", - "integrity": "sha512-qbagtqSyvIhUz4EWzXC00EA0DJHFrQwlzTlNGX5DjiESoJiPKqkEga1k9hviFKRFgBna+OlW54mkdi+0+AqxCw==", + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.7.2.tgz", + "integrity": "sha512-lrP2V5U1qhaf+z33fiIn7aYAZZ1fVDly+TkFRjTujNBF/FIHESATj2RbgAOSlWqv32fsZXkXejXzeVfjbv35Ow==", "dependencies": { "bson": "^4.7.0", "kareem": "2.4.1", @@ -6596,6 +7051,44 @@ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, + "node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -9320,6 +9813,14 @@ "wrappy": "1" } }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "dependencies": { + "fn.name": "1.x.x" + } + }, "node_modules/onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -9573,9 +10074,9 @@ } }, "node_modules/posthog-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-2.1.0.tgz", - "integrity": "sha512-xr56mZRQo7rnL2YdwbipcxTZeyi5dcI6IM4++wIN7JLYwinrJYcQv01nan4gU4kMy33Qz5qT6boWMQRwpKZJVQ==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-2.2.3.tgz", + "integrity": "sha512-dYlLZhrDus+uRov/Hh+EiRlMoMhRKchNjNa7mNE2iWmKg/ryOTipf0XYKS9UKdki7aU1NzWFhnLe11HF615XuA==", "dependencies": { "axios": "^0.27.0" }, @@ -9601,33 +10102,6 @@ "node": ">= 0.8.0" } }, - "node_modules/prettier": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz", - "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==", - "dev": true, - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/prettier-linter-helpers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", - "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", - "dev": true, - "dependencies": { - "fast-diff": "^1.1.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/pretty-format": { "version": "29.3.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.3.1.tgz", @@ -9667,6 +10141,31 @@ "node": ">= 6" } }, + "node_modules/protobufjs": { + "version": "6.11.3", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.3.tgz", + "integrity": "sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg==", + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.1", + "@types/node": ">=13.7.0", + "long": "^4.0.0" + }, + "bin": { + "pbjs": "bin/pbjs", + "pbts": "bin/pbts" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -9713,11 +10212,11 @@ } }, "node_modules/query-string": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.1.tgz", - "integrity": "sha512-MplouLRDHBZSG9z7fpuAAcI7aAYjDLhtsiVZsevsfaHWDS2IDdORKbSd1kWUA+V4zyva/HZoSfpwnYMMQDhb0w==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", + "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", "dependencies": { - "decode-uri-component": "^0.2.0", + "decode-uri-component": "^0.2.2", "filter-obj": "^1.1.0", "split-on-first": "^1.0.0", "strict-uri-encode": "^2.0.0" @@ -9974,6 +10473,14 @@ } ] }, + "node_modules/safe-stable-stringify": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.1.tgz", + "integrity": "sha512-dVHE6bMtS/bnL2mwualjc6IxEv1F+OCUpA46pKUj6F8uDbUM0jCCulPqRNPSnWwGNKx5etqMjZYdXtrm5KJZGA==", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -9995,7 +10502,6 @@ "version": "7.3.8", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", - "dev": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -10123,6 +10629,19 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + }, "node_modules/simple-update-notifier": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.0.7.tgz", @@ -10168,6 +10687,34 @@ "npm": ">= 3.0.0" } }, + "node_modules/snappy": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/snappy/-/snappy-7.1.1.tgz", + "integrity": "sha512-mL7GGPJ+WdsaFT5aR/uEqCq8cPg2VbhyifDEP7AeqIVDsAC8LBGYbZP1Qzoa2Ym84OW7JEQXqIpwqFp1EQw5BA==", + "optional": true, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/snappy-android-arm-eabi": "7.1.1", + "@napi-rs/snappy-android-arm64": "7.1.1", + "@napi-rs/snappy-darwin-arm64": "7.1.1", + "@napi-rs/snappy-darwin-x64": "7.1.1", + "@napi-rs/snappy-freebsd-x64": "7.1.1", + "@napi-rs/snappy-linux-arm-gnueabihf": "7.1.1", + "@napi-rs/snappy-linux-arm64-gnu": "7.1.1", + "@napi-rs/snappy-linux-arm64-musl": "7.1.1", + "@napi-rs/snappy-linux-x64-gnu": "7.1.1", + "@napi-rs/snappy-linux-x64-musl": "7.1.1", + "@napi-rs/snappy-win32-arm64-msvc": "7.1.1", + "@napi-rs/snappy-win32-ia32-msvc": "7.1.1", + "@napi-rs/snappy-win32-x64-msvc": "7.1.1" + } + }, "node_modules/socks": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", @@ -10222,6 +10769,14 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "engines": { + "node": "*" + } + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -10251,6 +10806,15 @@ "node": ">= 0.8" } }, + "node_modules/stoppable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", + "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", + "engines": { + "node": ">=4", + "npm": ">=6" + } + }, "node_modules/strict-uri-encode": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", @@ -10354,6 +10918,52 @@ "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", "optional": true }, + "node_modules/superagent": { + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.0.6.tgz", + "integrity": "sha512-HqSe6DSIh3hEn6cJvCkaM1BLi466f1LHi4yubR0tpewlMpk4RUFFy35bKz8SsPBwYfIIJy5eclp+3tCYAuX0bw==", + "dev": true, + "dependencies": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.3", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^2.1.1", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.0", + "semver": "^7.3.8" + }, + "engines": { + "node": ">=6.4.0 <13 || >=14" + } + }, + "node_modules/superagent/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/supertest": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.3.3.tgz", + "integrity": "sha512-EMCG6G8gDu5qEqRQ3JjjPs6+FYT1a7Hv5ApHvtSghmOFJYtsU5S+pSb6Y2EUeCEY3CmEL3mmQ8YWlPOzQomabA==", + "dev": true, + "dependencies": { + "methods": "^1.1.2", + "superagent": "^8.0.5" + }, + "engines": { + "node": ">=6.4.0" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -10460,6 +11070,11 @@ "node": ">=8" } }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -10533,6 +11148,54 @@ "node": ">=12" } }, + "node_modules/triple-beam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", + "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" + }, + "node_modules/ts-jest": { + "version": "29.0.3", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.0.3.tgz", + "integrity": "sha512-Ibygvmuyq1qp/z3yTh9QTwVVAbFdDy/+4BtIQR2sp6baF2SJU/8CKK/hhnGIDY2L90Az2jIqTwZPnN2p+BweiQ==", + "dev": true, + "dependencies": { + "bs-logger": "0.x", + "fast-json-stable-stringify": "2.x", + "jest-util": "^29.0.0", + "json5": "^2.2.1", + "lodash.memoize": "4.x", + "make-error": "1.x", + "semver": "7.x", + "yargs-parser": "^21.0.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, "node_modules/ts-node": { "version": "10.9.1", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", @@ -10652,9 +11315,9 @@ } }, "node_modules/typescript": { - "version": "4.8.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", - "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", + "version": "4.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.3.tgz", + "integrity": "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10681,6 +11344,11 @@ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", "dev": true }, + "node_modules/universal-user-agent": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", + "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -10741,7 +11409,7 @@ "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "optional": true, + "devOptional": true, "bin": { "uuid": "dist/bin/uuid" } @@ -10856,6 +11524,54 @@ "node": ">= 8" } }, + "node_modules/winston": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.8.2.tgz", + "integrity": "sha512-MsE1gRx1m5jdTTO9Ld/vND4krP2To+lgDoMEHGGa4HIlAUyXJtfc7CxQcGXVyz2IBpw5hbFkj2b/AtUdQwyRew==", + "dependencies": { + "@colors/colors": "1.5.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.4.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.5.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-loki": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/winston-loki/-/winston-loki-6.0.6.tgz", + "integrity": "sha512-cll+nv5T/b9uJXqca0N2WKL1JJNuJND9E6WOOAuSGkZ44L9VQ/QK9F+/5VKbv6LIP9p0nvPSOYxtACCDb/9iWw==", + "dependencies": { + "async-exit-hook": "2.0.1", + "btoa": "^1.2.1", + "protobufjs": "^6.8.8", + "winston-transport": "^4.3.0" + }, + "optionalDependencies": { + "snappy": "7.1.1" + } + }, + "node_modules/winston-transport": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.5.0.tgz", + "integrity": "sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==", + "dependencies": { + "logform": "^2.3.2", + "readable-stream": "^3.6.0", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 6.4.0" + } + }, "node_modules/word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", @@ -10905,6 +11621,12 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==", + "dev": true + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -10917,8 +11639,7 @@ "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yaml": { "version": "2.0.0-1", @@ -12775,6 +13496,11 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==" + }, "@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -12784,6 +13510,16 @@ "@jridgewell/trace-mapping": "0.3.9" } }, + "@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "requires": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, "@eslint/eslintrc": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.3.tgz", @@ -12801,6 +13537,14 @@ "strip-json-comments": "^3.1.1" } }, + "@godaddy/terminus": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/@godaddy/terminus/-/terminus-4.11.2.tgz", + "integrity": "sha512-e/kbOWpGKME42eltM/wXM3RxSUOrfureZxEd6Dt6NXyFoJ7E8lnmm7znXydJsL3B7ky4HRFZI+eHrep54NZbeQ==", + "requires": { + "stoppable": "^1.1.0" + } + }, "@humanwhocodes/config-array": { "version": "0.11.7", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz", @@ -13228,6 +13972,12 @@ "maxmind": "^4.2.0" } }, + "@napi-rs/snappy-darwin-arm64": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-darwin-arm64/-/snappy-darwin-arm64-7.1.1.tgz", + "integrity": "sha512-3LZyoAw3Qa5F7sCCTkSkhmGlydwUKU6L3Jl46eKHO2Ctm8Gcjxww6T7MfwlwGZ6JqAM6d1d++WLzUZPCGXVmag==", + "optional": true + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -13254,6 +14004,118 @@ "fastq": "^1.6.0" } }, + "@octokit/auth-token": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-3.0.2.tgz", + "integrity": "sha512-pq7CwIMV1kmzkFTimdwjAINCXKTajZErLB4wMLYapR2nuB/Jpr66+05wOTZMSCBXP6n4DdDWT2W19Bm17vU69Q==", + "requires": { + "@octokit/types": "^8.0.0" + } + }, + "@octokit/core": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-4.1.0.tgz", + "integrity": "sha512-Czz/59VefU+kKDy+ZfDwtOIYIkFjExOKf+HA92aiTZJ6EfWpFzYQWw0l54ji8bVmyhc+mGaLUbSUmXazG7z5OQ==", + "requires": { + "@octokit/auth-token": "^3.0.0", + "@octokit/graphql": "^5.0.0", + "@octokit/request": "^6.0.0", + "@octokit/request-error": "^3.0.0", + "@octokit/types": "^8.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + } + }, + "@octokit/endpoint": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-7.0.3.tgz", + "integrity": "sha512-57gRlb28bwTsdNXq+O3JTQ7ERmBTuik9+LelgcLIVfYwf235VHbN9QNo4kXExtp/h8T423cR5iJThKtFYxC7Lw==", + "requires": { + "@octokit/types": "^8.0.0", + "is-plain-object": "^5.0.0", + "universal-user-agent": "^6.0.0" + } + }, + "@octokit/graphql": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-5.0.4.tgz", + "integrity": "sha512-amO1M5QUQgYQo09aStR/XO7KAl13xpigcy/kI8/N1PnZYSS69fgte+xA4+c2DISKqUZfsh0wwjc2FaCt99L41A==", + "requires": { + "@octokit/request": "^6.0.0", + "@octokit/types": "^8.0.0", + "universal-user-agent": "^6.0.0" + } + }, + "@octokit/openapi-types": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-14.0.0.tgz", + "integrity": "sha512-HNWisMYlR8VCnNurDU6os2ikx0s0VyEjDYHNS/h4cgb8DeOxQ0n72HyinUtdDVxJhFy3FWLGl0DJhfEWk3P5Iw==" + }, + "@octokit/plugin-paginate-rest": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-5.0.1.tgz", + "integrity": "sha512-7A+rEkS70pH36Z6JivSlR7Zqepz3KVucEFVDnSrgHXzG7WLAzYwcHZbKdfTXHwuTHbkT1vKvz7dHl1+HNf6Qyw==", + "requires": { + "@octokit/types": "^8.0.0" + } + }, + "@octokit/plugin-request-log": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz", + "integrity": "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==", + "requires": {} + }, + "@octokit/plugin-rest-endpoint-methods": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-6.7.0.tgz", + "integrity": "sha512-orxQ0fAHA7IpYhG2flD2AygztPlGYNAdlzYz8yrD8NDgelPfOYoRPROfEyIe035PlxvbYrgkfUZIhSBKju/Cvw==", + "requires": { + "@octokit/types": "^8.0.0", + "deprecation": "^2.3.1" + } + }, + "@octokit/request": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-6.2.2.tgz", + "integrity": "sha512-6VDqgj0HMc2FUX2awIs+sM6OwLgwHvAi4KCK3mT2H2IKRt6oH9d0fej5LluF5mck1lRR/rFWN0YIDSYXYSylbw==", + "requires": { + "@octokit/endpoint": "^7.0.0", + "@octokit/request-error": "^3.0.0", + "@octokit/types": "^8.0.0", + "is-plain-object": "^5.0.0", + "node-fetch": "^2.6.7", + "universal-user-agent": "^6.0.0" + } + }, + "@octokit/request-error": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-3.0.2.tgz", + "integrity": "sha512-WMNOFYrSaX8zXWoJg9u/pKgWPo94JXilMLb2VManNOby9EZxrQaBe/QSC4a1TzpAlpxofg2X/jMnCyZgL6y7eg==", + "requires": { + "@octokit/types": "^8.0.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + } + }, + "@octokit/rest": { + "version": "19.0.5", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-19.0.5.tgz", + "integrity": "sha512-+4qdrUFq2lk7Va+Qff3ofREQWGBeoTKNqlJO+FGjFP35ZahP+nBenhZiGdu8USSgmq4Ky3IJ/i4u0xbLqHaeow==", + "requires": { + "@octokit/core": "^4.1.0", + "@octokit/plugin-paginate-rest": "^5.0.0", + "@octokit/plugin-request-log": "^1.0.4", + "@octokit/plugin-rest-endpoint-methods": "^6.7.0" + } + }, + "@octokit/types": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-8.0.0.tgz", + "integrity": "sha512-65/TPpOJP1i3K4lBJMnWqPUJ6zuOtzhtagDvydAWbEXpbFYA0oMKKyLb95NFZZP0lSh/4b6K+DQlzvYQJQQePg==", + "requires": { + "@octokit/openapi-types": "^14.0.0" + } + }, "@posthog/plugin-scaffold": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/@posthog/plugin-scaffold/-/plugin-scaffold-1.3.4.tgz", @@ -13263,53 +14125,135 @@ "@maxmind/geoip2-node": "^3.4.0" } }, - "@sentry/core": { - "version": "7.17.4", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.17.4.tgz", - "integrity": "sha512-U3ABSJBKGK8dJ01nEG2+qNOb6Wv7U3VqoajiZxfV4lpPWNFGCoEhiTytxBlFTOCmdUH8209zSZiWJZaDLy+TSA==", + "@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", "requires": { - "@sentry/types": "7.17.4", - "@sentry/utils": "7.17.4", - "tslib": "^1.9.3" + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" } }, + "@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, "@sentry/node": { - "version": "7.17.4", - "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.17.4.tgz", - "integrity": "sha512-cR+Gsir9c/tzFWxvk4zXkMQy6tNRHEYixHrb88XIjZVYDqDS9l2/bKs5nJusdmaUeLtmPp5Et2o7RJyS7gvKTQ==", + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.19.0.tgz", + "integrity": "sha512-yG7Tx32WqOkEHVotFLrumCcT9qlaSDTkFNZ+yLSvZXx74ifsE781DzBA9W7K7bBdYO3op+p2YdsOKzf3nPpAyQ==", "requires": { - "@sentry/core": "7.17.4", - "@sentry/types": "7.17.4", - "@sentry/utils": "7.17.4", + "@sentry/core": "7.19.0", + "@sentry/types": "7.19.0", + "@sentry/utils": "7.19.0", "cookie": "^0.4.1", "https-proxy-agent": "^5.0.0", "lru_map": "^0.3.3", "tslib": "^1.9.3" + }, + "dependencies": { + "@sentry/core": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.19.0.tgz", + "integrity": "sha512-YF9cTBcAnO4R44092BJi5Wa2/EO02xn2ziCtmNgAVTN2LD31a/YVGxGBt/FDr4Y6yeuVehaqijVVvtpSmXrGJw==", + "requires": { + "@sentry/types": "7.19.0", + "@sentry/utils": "7.19.0", + "tslib": "^1.9.3" + } + }, + "@sentry/types": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.19.0.tgz", + "integrity": "sha512-oGRAT6lfzoKrxO1mvxiSj0XHxWPd6Gd1wpPGuu6iJo03xgWDS+MIlD1h2unqL4N5fAzLjzmbC2D2lUw50Kn2pA==" + }, + "@sentry/utils": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.19.0.tgz", + "integrity": "sha512-2L6lq+c9Ol2uiRxQDdcgoapmHJp24MhMN0gIkn2alSfMJ+ls6bGXzQHx6JAIdoOiwFQXRZHKL9ecfAc8O+vItA==", + "requires": { + "@sentry/types": "7.19.0", + "tslib": "^1.9.3" + } + } } }, "@sentry/tracing": { - "version": "7.17.4", - "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-7.17.4.tgz", - "integrity": "sha512-9Fz6DI16ddnd970mlB5MiCNRSmSXp4SVZ1Yv3L22oS3kQeNxjBTE+htYNwJzSPrQp9aL/LqTYwlnrCy24u9XQA==", + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-7.19.0.tgz", + "integrity": "sha512-SWY17M3TsgBePaGowUcSqBwaT0TJQzuNexVnLojuU0k6F57L9hubvP9zaoosoCfARXQ/3NypAFWnlJyf570rFQ==", "requires": { - "@sentry/core": "7.17.4", - "@sentry/types": "7.17.4", - "@sentry/utils": "7.17.4", - "tslib": "^1.9.3" - } - }, - "@sentry/types": { - "version": "7.17.4", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.17.4.tgz", - "integrity": "sha512-QJj8vO4AtxuzQfJIzDnECSmoxwnS+WJsm1Ta2Cwdy+TUCBJyWpW7aIJJGta76zb9gNPGb3UcAbeEjhMJBJeRMQ==" - }, - "@sentry/utils": { - "version": "7.17.4", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.17.4.tgz", - "integrity": "sha512-ioG0ANy8uiWzig82/e7cc+6C9UOxkyBzJDi1luoQVDH6P0/PvM8GzVU+1iUVUipf8+OL1Jh09GrWnd5wLm3XNQ==", - "requires": { - "@sentry/types": "7.17.4", + "@sentry/core": "7.19.0", + "@sentry/types": "7.19.0", + "@sentry/utils": "7.19.0", "tslib": "^1.9.3" + }, + "dependencies": { + "@sentry/core": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.19.0.tgz", + "integrity": "sha512-YF9cTBcAnO4R44092BJi5Wa2/EO02xn2ziCtmNgAVTN2LD31a/YVGxGBt/FDr4Y6yeuVehaqijVVvtpSmXrGJw==", + "requires": { + "@sentry/types": "7.19.0", + "@sentry/utils": "7.19.0", + "tslib": "^1.9.3" + } + }, + "@sentry/types": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.19.0.tgz", + "integrity": "sha512-oGRAT6lfzoKrxO1mvxiSj0XHxWPd6Gd1wpPGuu6iJo03xgWDS+MIlD1h2unqL4N5fAzLjzmbC2D2lUw50Kn2pA==" + }, + "@sentry/utils": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.19.0.tgz", + "integrity": "sha512-2L6lq+c9Ol2uiRxQDdcgoapmHJp24MhMN0gIkn2alSfMJ+ls6bGXzQHx6JAIdoOiwFQXRZHKL9ecfAc8O+vItA==", + "requires": { + "@sentry/types": "7.19.0", + "tslib": "^1.9.3" + } + } } }, "@sinclair/typebox": { @@ -13429,6 +14373,12 @@ "@types/express": "*" } }, + "@types/cookiejar": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.2.tgz", + "integrity": "sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==", + "dev": true + }, "@types/cors": { "version": "2.8.12", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", @@ -13496,6 +14446,16 @@ "@types/istanbul-lib-report": "*" } }, + "@types/jest": { + "version": "29.2.4", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.2.4.tgz", + "integrity": "sha512-PipFB04k2qTRPePduVLTRiPzQfvMeLwUN3Z21hsAKaB/W9IIzgB2pizCL466ftJlcyZqnHoC9ZHpxLGl3fS86A==", + "dev": true, + "requires": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, "@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", @@ -13510,6 +14470,16 @@ "@types/node": "*" } }, + "@types/libsodium-wrappers": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/@types/libsodium-wrappers/-/libsodium-wrappers-0.7.10.tgz", + "integrity": "sha512-BqI9B92u+cM3ccp8mpHf+HzJ8fBlRwdmyd6+fz3p99m3V6ifT5O3zmOMi612PGkpeFeG/G6loxUnzlDNhfjPSA==" + }, + "@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" + }, "@types/mime": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", @@ -13531,9 +14501,9 @@ } }, "@types/prettier": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.1.tgz", - "integrity": "sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow==", + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz", + "integrity": "sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg==", "dev": true }, "@types/qs": { @@ -13570,6 +14540,25 @@ "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", "dev": true }, + "@types/superagent": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-4.1.16.tgz", + "integrity": "sha512-tLfnlJf6A5mB6ddqF159GqcDizfzbMUB1/DeT59/wBNqzRTNNKsaw79A/1TZ84X+f/EwWH8FeuSkjlCLyqS/zQ==", + "dev": true, + "requires": { + "@types/cookiejar": "*", + "@types/node": "*" + } + }, + "@types/supertest": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-2.0.12.tgz", + "integrity": "sha512-X3HPWTwXRerBZS7Mo1k6vMVR1Z6zmJcDVn5O/31whe0tnjE4te6ZJSJGq1RiqHPjzPdMTfjCFogDJmwng9xHaQ==", + "dev": true, + "requires": { + "@types/superagent": "*" + } + }, "@types/swagger-jsdoc": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/@types/swagger-jsdoc/-/swagger-jsdoc-6.0.1.tgz", @@ -13831,12 +14820,28 @@ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true + }, "assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", "dev": true }, + "async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" + }, + "async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==" + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -13932,6 +14937,11 @@ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, + "before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==" + }, "bigint-conversion": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/bigint-conversion/-/bigint-conversion-2.2.2.tgz", @@ -14016,6 +15026,15 @@ "update-browserslist-db": "^1.0.9" } }, + "bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "requires": { + "fast-json-stable-stringify": "2.x" + } + }, "bser": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", @@ -14033,6 +15052,11 @@ "buffer": "^5.6.0" } }, + "btoa": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", + "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==" + }, "buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -14189,6 +15213,30 @@ "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", "dev": true }, + "color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "requires": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + }, + "dependencies": { + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + } + } + }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -14201,8 +15249,25 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "requires": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "requires": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } }, "combined-stream": { "version": "1.0.8", @@ -14212,10 +15277,11 @@ "delayed-stream": "~1.0.0" } }, - "commander": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", - "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==" + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true }, "concat-map": { "version": "0.0.1", @@ -14267,6 +15333,12 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "cookiejar": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.3.tgz", + "integrity": "sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ==", + "dev": true + }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -14300,6 +15372,15 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true }, + "cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.1" + } + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -14325,9 +15406,9 @@ } }, "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==" + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==" }, "dedent": { "version": "0.7.0", @@ -14362,6 +15443,11 @@ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" }, + "deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" + }, "destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -14373,6 +15459,16 @@ "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", "dev": true }, + "dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "requires": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, "diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -14438,6 +15534,11 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" + }, "encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -14534,22 +15635,6 @@ } } }, - "eslint-config-prettier": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz", - "integrity": "sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==", - "dev": true, - "requires": {} - }, - "eslint-plugin-prettier": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz", - "integrity": "sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==", - "dev": true, - "requires": { - "prettier-linter-helpers": "^1.0.0" - } - }, "eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -14745,9 +15830,9 @@ } }, "express-rate-limit": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.6.0.tgz", - "integrity": "sha512-HFN2+4ZGdkQOS8Qli4z6knmJFnw6lZed67o6b7RGplWeb1Z0s8VXaj3dUgPIdm9hrhZXTRpCTHXA0/2Eqex0vA==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.7.0.tgz", + "integrity": "sha512-vhwIdRoqcYB/72TK3tRZI+0ttS8Ytrk24GfmsxDXK9o9IhHNO5bXRiXQSExPQ4GbaE5tvIS7j1SGrxsuWs+sGA==", "requires": {} }, "express-validator": { @@ -14771,12 +15856,6 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, - "fast-diff": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", - "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", - "dev": true - }, "fast-glob": { "version": "3.2.12", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", @@ -14813,6 +15892,12 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true + }, "fast-xml-parser": { "version": "4.0.11", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.0.11.tgz", @@ -14840,6 +15925,11 @@ "bser": "2.1.1" } }, + "fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" + }, "file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -14918,6 +16008,11 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, + "fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" + }, "follow-redirects": { "version": "1.15.2", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", @@ -14933,6 +16028,18 @@ "mime-types": "^2.1.12" } }, + "formidable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.1.tgz", + "integrity": "sha512-0EcS9wCFEzLvfiks7omJ+SiYJAiD+TzK4Pcw1UlUoGnhUxDcMKjt0P7x8wEb0u6OHu8Nb98WG3nxtlF5C7bvUQ==", + "dev": true, + "requires": { + "dezalgo": "^1.0.4", + "hexoid": "^1.0.0", + "once": "^1.4.0", + "qs": "^6.11.0" + } + }, "forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -15105,6 +16212,12 @@ "resolved": "https://registry.npmjs.org/helmet/-/helmet-5.1.1.tgz", "integrity": "sha512-/yX0oVZBggA9cLJh8aw3PPCfedBnbd7J2aowjzsaWwZh7/UFY0nccn/aHAggIgWUFfnykX8GKd3a1pSbrmlcVQ==" }, + "hexoid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", + "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==", + "dev": true + }, "html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -15138,12 +16251,6 @@ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true }, - "husky": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.1.tgz", - "integrity": "sha512-xs7/chUH/CKdOCs7Zy0Aev9e/dKOMZf3K1Az1nar3tzlv0jfqnYtu235bstsWTmXOR0EfINrPa97yy4Lz6RiKw==", - "dev": true - }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -15298,11 +16405,15 @@ "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true }, + "is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" + }, "is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" }, "isexe": { "version": "2.0.0", @@ -15542,6 +16653,18 @@ "walker": "^1.0.8" } }, + "jest-junit": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/jest-junit/-/jest-junit-15.0.0.tgz", + "integrity": "sha512-Z5sVX0Ag3HZdMUnD5DFlG+1gciIFSy7yIVPhOdGUi8YJaI9iLvvBb530gtQL2CHmv0JJeiwRZenr0VrSR7frvg==", + "dev": true, + "requires": { + "mkdirp": "^1.0.4", + "strip-ansi": "^6.0.1", + "uuid": "^8.3.2", + "xml": "^1.0.1" + } + }, "jest-leak-detector": { "version": "29.3.1", "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.3.1.tgz", @@ -15852,27 +16975,14 @@ "dev": true }, "jsonwebtoken": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", - "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", + "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", "requires": { "jws": "^3.2.2", - "lodash.includes": "^4.3.0", - "lodash.isboolean": "^3.0.3", - "lodash.isinteger": "^4.0.4", - "lodash.isnumber": "^3.0.3", - "lodash.isplainobject": "^4.0.6", - "lodash.isstring": "^4.0.1", - "lodash.once": "^4.0.0", + "lodash": "^4.17.21", "ms": "^2.1.1", - "semver": "^5.6.0" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" - } + "semver": "^7.3.8" } }, "jsprim": { @@ -15927,6 +17037,11 @@ "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", "dev": true }, + "kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" + }, "leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -15943,6 +17058,19 @@ "type-check": "~0.4.0" } }, + "libsodium": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.10.tgz", + "integrity": "sha512-eY+z7hDrDKxkAK+QKZVNv92A5KYkxfvIshtBJkmg5TSiCnYqZP3i9OO9whE79Pwgm4jGaoHgkM4ao/b9Cyu4zQ==" + }, + "libsodium-wrappers": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.7.10.tgz", + "integrity": "sha512-pO3F1Q9NPLB/MWIhehim42b/Fwb30JNScCNh8TcQ/kIc+qGLQch8ag8wb0keK3EP5kbGakk1H8Wwo7v+36rNQg==", + "requires": { + "libsodium": "^0.7.0" + } + }, "lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -15963,45 +17091,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, - "lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" - }, - "lodash.includes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" - }, - "lodash.isboolean": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" - }, - "lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" - }, - "lodash.isinteger": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", - "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" - }, - "lodash.isnumber": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", - "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" - }, - "lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" - }, - "lodash.isstring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + "lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true }, "lodash.merge": { "version": "4.6.2", @@ -16009,15 +17103,22 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, - "lodash.mergewith": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", - "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==" + "logform": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.4.2.tgz", + "integrity": "sha512-W4c9himeAwXEdZ05dQNerhFz2XG80P9Oj0loPUMV23VC2it0orMHQhJm4hdnnor3rd1HsGf6a2lPwBM1zeXHGw==", + "requires": { + "@colors/colors": "1.5.0", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + } }, - "lodash.once": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + "long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" }, "lru_map": { "version": "0.3.3", @@ -16028,7 +17129,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "requires": { "yallist": "^4.0.0" } @@ -16171,6 +17271,12 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==" }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + }, "mmdb-lib": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/mmdb-lib/-/mmdb-lib-2.0.2.tgz", @@ -16200,9 +17306,9 @@ } }, "mongoose": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.7.1.tgz", - "integrity": "sha512-qbagtqSyvIhUz4EWzXC00EA0DJHFrQwlzTlNGX5DjiESoJiPKqkEga1k9hviFKRFgBna+OlW54mkdi+0+AqxCw==", + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.7.2.tgz", + "integrity": "sha512-lrP2V5U1qhaf+z33fiIn7aYAZZ1fVDly+TkFRjTujNBF/FIHESATj2RbgAOSlWqv32fsZXkXejXzeVfjbv35Ow==", "requires": { "bson": "^4.7.0", "kareem": "2.4.1", @@ -16260,6 +17366,35 @@ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, + "node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "requires": { + "whatwg-url": "^5.0.0" + }, + "dependencies": { + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + } + } + }, "node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -18166,6 +19301,14 @@ "wrappy": "1" } }, + "one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "requires": { + "fn.name": "1.x.x" + } + }, "onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -18346,9 +19489,9 @@ } }, "posthog-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-2.1.0.tgz", - "integrity": "sha512-xr56mZRQo7rnL2YdwbipcxTZeyi5dcI6IM4++wIN7JLYwinrJYcQv01nan4gU4kMy33Qz5qT6boWMQRwpKZJVQ==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-2.2.3.tgz", + "integrity": "sha512-dYlLZhrDus+uRov/Hh+EiRlMoMhRKchNjNa7mNE2iWmKg/ryOTipf0XYKS9UKdki7aU1NzWFhnLe11HF615XuA==", "requires": { "axios": "^0.27.0" }, @@ -18370,21 +19513,6 @@ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true }, - "prettier": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz", - "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==", - "dev": true - }, - "prettier-linter-helpers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", - "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", - "dev": true, - "requires": { - "fast-diff": "^1.1.2" - } - }, "pretty-format": { "version": "29.3.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.3.1.tgz", @@ -18414,6 +19542,26 @@ "sisteransi": "^1.0.5" } }, + "protobufjs": { + "version": "6.11.3", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.3.tgz", + "integrity": "sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg==", + "requires": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.1", + "@types/node": ">=13.7.0", + "long": "^4.0.0" + } + }, "proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -18448,11 +19596,11 @@ } }, "query-string": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.1.tgz", - "integrity": "sha512-MplouLRDHBZSG9z7fpuAAcI7aAYjDLhtsiVZsevsfaHWDS2IDdORKbSd1kWUA+V4zyva/HZoSfpwnYMMQDhb0w==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", + "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", "requires": { - "decode-uri-component": "^0.2.0", + "decode-uri-component": "^0.2.2", "filter-obj": "^1.1.0", "split-on-first": "^1.0.0", "strict-uri-encode": "^2.0.0" @@ -18608,6 +19756,11 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" }, + "safe-stable-stringify": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.1.tgz", + "integrity": "sha512-dVHE6bMtS/bnL2mwualjc6IxEv1F+OCUpA46pKUj6F8uDbUM0jCCulPqRNPSnWwGNKx5etqMjZYdXtrm5KJZGA==" + }, "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -18626,7 +19779,6 @@ "version": "7.3.8", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", - "dev": true, "requires": { "lru-cache": "^6.0.0" } @@ -18734,6 +19886,21 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "requires": { + "is-arrayish": "^0.3.1" + }, + "dependencies": { + "is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + } + } + }, "simple-update-notifier": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.0.7.tgz", @@ -18768,6 +19935,27 @@ "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==" }, + "snappy": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/snappy/-/snappy-7.1.1.tgz", + "integrity": "sha512-mL7GGPJ+WdsaFT5aR/uEqCq8cPg2VbhyifDEP7AeqIVDsAC8LBGYbZP1Qzoa2Ym84OW7JEQXqIpwqFp1EQw5BA==", + "optional": true, + "requires": { + "@napi-rs/snappy-android-arm-eabi": "7.1.1", + "@napi-rs/snappy-android-arm64": "7.1.1", + "@napi-rs/snappy-darwin-arm64": "7.1.1", + "@napi-rs/snappy-darwin-x64": "7.1.1", + "@napi-rs/snappy-freebsd-x64": "7.1.1", + "@napi-rs/snappy-linux-arm-gnueabihf": "7.1.1", + "@napi-rs/snappy-linux-arm64-gnu": "7.1.1", + "@napi-rs/snappy-linux-arm64-musl": "7.1.1", + "@napi-rs/snappy-linux-x64-gnu": "7.1.1", + "@napi-rs/snappy-linux-x64-musl": "7.1.1", + "@napi-rs/snappy-win32-arm64-msvc": "7.1.1", + "@napi-rs/snappy-win32-ia32-msvc": "7.1.1", + "@napi-rs/snappy-win32-x64-msvc": "7.1.1" + } + }, "socks": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", @@ -18812,6 +20000,11 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, + "stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==" + }, "stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -18834,6 +20027,11 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" }, + "stoppable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", + "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==" + }, "strict-uri-encode": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", @@ -18910,6 +20108,42 @@ "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", "optional": true }, + "superagent": { + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.0.6.tgz", + "integrity": "sha512-HqSe6DSIh3hEn6cJvCkaM1BLi466f1LHi4yubR0tpewlMpk4RUFFy35bKz8SsPBwYfIIJy5eclp+3tCYAuX0bw==", + "dev": true, + "requires": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.3", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^2.1.1", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.0", + "semver": "^7.3.8" + }, + "dependencies": { + "mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true + } + } + }, + "supertest": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.3.3.tgz", + "integrity": "sha512-EMCG6G8gDu5qEqRQ3JjjPs6+FYT1a7Hv5ApHvtSghmOFJYtsU5S+pSb6Y2EUeCEY3CmEL3mmQ8YWlPOzQomabA==", + "dev": true, + "requires": { + "methods": "^1.1.2", + "superagent": "^8.0.5" + } + }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -18985,6 +20219,11 @@ "minimatch": "^3.0.4" } }, + "text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" + }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -19040,6 +20279,27 @@ "punycode": "^2.1.1" } }, + "triple-beam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", + "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" + }, + "ts-jest": { + "version": "29.0.3", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.0.3.tgz", + "integrity": "sha512-Ibygvmuyq1qp/z3yTh9QTwVVAbFdDy/+4BtIQR2sp6baF2SJU/8CKK/hhnGIDY2L90Az2jIqTwZPnN2p+BweiQ==", + "dev": true, + "requires": { + "bs-logger": "0.x", + "fast-json-stable-stringify": "2.x", + "jest-util": "^29.0.0", + "json5": "^2.2.1", + "lodash.memoize": "4.x", + "make-error": "1.x", + "semver": "7.x", + "yargs-parser": "^21.0.1" + } + }, "ts-node": { "version": "10.9.1", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", @@ -19116,9 +20376,9 @@ } }, "typescript": { - "version": "4.8.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", - "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==" + "version": "4.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.3.tgz", + "integrity": "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==" }, "uglify-js": { "version": "3.17.4", @@ -19132,6 +20392,11 @@ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", "dev": true }, + "universal-user-agent": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", + "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==" + }, "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -19170,7 +20435,7 @@ "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "optional": true + "devOptional": true }, "v8-compile-cache-lib": { "version": "3.0.1", @@ -19260,6 +20525,46 @@ "isexe": "^2.0.0" } }, + "winston": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.8.2.tgz", + "integrity": "sha512-MsE1gRx1m5jdTTO9Ld/vND4krP2To+lgDoMEHGGa4HIlAUyXJtfc7CxQcGXVyz2IBpw5hbFkj2b/AtUdQwyRew==", + "requires": { + "@colors/colors": "1.5.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.4.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.5.0" + } + }, + "winston-loki": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/winston-loki/-/winston-loki-6.0.6.tgz", + "integrity": "sha512-cll+nv5T/b9uJXqca0N2WKL1JJNuJND9E6WOOAuSGkZ44L9VQ/QK9F+/5VKbv6LIP9p0nvPSOYxtACCDb/9iWw==", + "requires": { + "async-exit-hook": "2.0.1", + "btoa": "^1.2.1", + "protobufjs": "^6.8.8", + "snappy": "7.1.1", + "winston-transport": "^4.3.0" + } + }, + "winston-transport": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.5.0.tgz", + "integrity": "sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==", + "requires": { + "logform": "^2.3.2", + "readable-stream": "^3.6.0", + "triple-beam": "^1.3.0" + } + }, "word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", @@ -19297,6 +20602,12 @@ "signal-exit": "^3.0.7" } }, + "xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==", + "dev": true + }, "y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -19306,8 +20617,7 @@ "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "yaml": { "version": "2.0.0-1", diff --git a/backend/package.json b/backend/package.json index 2bd296d9c..d1a03a74c 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,8 +1,11 @@ { "dependencies": { + "@godaddy/terminus": "^4.11.2", + "@octokit/rest": "^19.0.5", "@sentry/node": "^7.14.0", - "@sentry/tracing": "^7.14.0", + "@sentry/tracing": "^7.19.0", "@types/crypto-js": "^4.1.1", + "@types/libsodium-wrappers": "^0.7.10", "axios": "^1.1.3", "bigint-conversion": "^2.2.2", "cookie-parser": "^1.4.6", @@ -10,32 +13,40 @@ "crypto-js": "^4.1.1", "dotenv": "^16.0.1", "express": "^4.18.1", - "express-rate-limit": "^6.5.1", + "express-rate-limit": "^6.7.0", "express-validator": "^6.14.2", "handlebars": "^4.7.7", "helmet": "^5.1.1", - "jsonwebtoken": "^8.5.1", + "jsonwebtoken": "^9.0.0", "jsrp": "^0.2.4", - "mongoose": "^6.7.1", + "libsodium-wrappers": "^0.7.10", + "mongoose": "^6.7.2", "nodemailer": "^6.8.0", - "posthog-node": "^2.1.0", - "query-string": "^7.1.1", + "posthog-node": "^2.2.2", + "query-string": "^7.1.3", "rimraf": "^3.0.2", "stripe": "^10.7.0", "tweetnacl": "^1.0.3", "tweetnacl-util": "^0.15.1", - "typescript": "^4.8.4" + "typescript": "^4.9.3", + "winston": "^3.8.2", + "winston-loki": "^6.0.6" }, "name": "infisical-api", "version": "1.0.0", "main": "src/index.js", "scripts": { + "prepare": "cd .. && npm install", "start": "npm run build && node build/index.js", "dev": "nodemon", - "build": "rimraf ./build && tsc && cp -R ./src/templates ./src/json ./build", + "build": "rimraf ./build && tsc && cp -R ./src/templates ./build", "lint": "eslint . --ext .ts", "lint-and-fix": "eslint . --ext .ts --fix", - "prettier-format": "prettier --config .prettierrc 'src/**/*.ts' --write" + "lint-staged": "lint-staged", + "pretest": "docker compose -f test-resources/docker-compose.test.yml up -d", + "test": "cross-env NODE_ENV=test jest --testTimeout=10000 --detectOpenHandles", + "test:ci": "npm test -- --watchAll=false --ci --reporters=default --reporters=jest-junit --reporters=github-actions --coverage --testLocationInResults --json --outputFile=coverage/report.json", + "posttest": "docker compose -f test-resources/docker-compose.test.yml down" }, "repository": { "type": "git", @@ -49,26 +60,49 @@ "homepage": "https://github.com/Infisical/infisical-api#readme", "description": "", "devDependencies": { + "@jest/globals": "^29.3.1", "@posthog/plugin-scaffold": "^1.3.4", "@types/cookie-parser": "^1.4.3", "@types/cors": "^2.8.12", "@types/express": "^4.17.14", + "@types/jest": "^29.2.4", "@types/jsonwebtoken": "^8.5.9", "@types/node": "^18.11.3", "@types/nodemailer": "^6.4.6", + "@types/supertest": "^2.0.12", "@types/swagger-jsdoc": "^6.0.1", "@types/swagger-ui-express": "^4.1.3", "@typescript-eslint/eslint-plugin": "^5.40.1", "@typescript-eslint/parser": "^5.40.1", + "cross-env": "^7.0.3", "eslint": "^8.26.0", - "eslint-config-prettier": "^8.5.0", - "eslint-plugin-prettier": "^4.2.1", - "husky": "^8.0.1", "install": "^0.13.0", "jest": "^29.3.1", + "jest-junit": "^15.0.0", "nodemon": "^2.0.19", "npm": "^8.19.3", - "prettier": "^2.7.1", + "supertest": "^6.3.3", + "ts-jest": "^29.0.3", "ts-node": "^10.9.1" + }, + "jest": { + "preset": "ts-jest", + "testEnvironment": "node", + "collectCoverageFrom": [ + "src/*.{js,ts}", + "!**/node_modules/**" + ], + "setupFiles": [ + "/test-resources/env-vars.js" + ] + }, + "jest-junit": { + "outputDirectory": "reports", + "outputName": "jest-junit.xml", + "ancestorSeparator": " โ€บ ", + "uniqueOutputName": "false", + "suiteNameTemplate": "{filepath}", + "classNameTemplate": "{classname}", + "titleTemplate": "{title}" } } diff --git a/backend/src/app.ts b/backend/src/app.ts new file mode 100644 index 000000000..e49551a91 --- /dev/null +++ b/backend/src/app.ts @@ -0,0 +1,91 @@ + +import { patchRouterParam } from './utils/patchAsyncRoutes'; +import express from 'express'; +import helmet from 'helmet'; +import cors from 'cors'; +import cookieParser from 'cookie-parser'; +import dotenv from 'dotenv'; + +dotenv.config(); +import { PORT, NODE_ENV, SITE_URL } from './config'; +import { apiLimiter } from './helpers/rateLimiter'; + +import { + signup as signupRouter, + auth as authRouter, + bot as botRouter, + organization as organizationRouter, + workspace as workspaceRouter, + membershipOrg as membershipOrgRouter, + membership as membershipRouter, + key as keyRouter, + inviteOrg as inviteOrgRouter, + user as userRouter, + userAction as userActionRouter, + secret as secretRouter, + serviceToken as serviceTokenRouter, + password as passwordRouter, + stripe as stripeRouter, + integration as integrationRouter, + integrationAuth as integrationAuthRouter +} from './routes'; +import { getLogger } from './utils/logger'; +import { RouteNotFoundError } from './utils/errors'; +import { requestErrorHandler } from './middleware/requestErrorHandler'; + +//* Patch Async route params to handle Promise Rejections +patchRouterParam() + +export const app = express(); + +app.enable('trust proxy'); +app.use(express.json()); +app.use(cookieParser()); +app.use( + cors({ + credentials: true, + origin: SITE_URL + }) +); + +if (NODE_ENV === 'production') { + // enable app-wide rate-limiting + helmet security + // in production + app.disable('x-powered-by'); + app.use(apiLimiter); + app.use(helmet()); +} + +// routers +app.use('/api/v1/signup', signupRouter); +app.use('/api/v1/auth', authRouter); +app.use('/api/v1/bot', botRouter); +app.use('/api/v1/user', userRouter); +app.use('/api/v1/user-action', userActionRouter); +app.use('/api/v1/organization', organizationRouter); +app.use('/api/v1/workspace', workspaceRouter); +app.use('/api/v1/membership-org', membershipOrgRouter); +app.use('/api/v1/membership', membershipRouter); +app.use('/api/v1/key', keyRouter); +app.use('/api/v1/invite-org', inviteOrgRouter); +app.use('/api/v1/secret', secretRouter); +app.use('/api/v1/service-token', serviceTokenRouter); +app.use('/api/v1/password', passwordRouter); +app.use('/api/v1/stripe', stripeRouter); +app.use('/api/v1/integration', integrationRouter); +app.use('/api/v1/integration-auth', integrationAuthRouter); + + +//* Handle unrouted requests and respond with proper error message as well as status code +app.use((req, res, next)=>{ + if(res.headersSent) return next(); + next(RouteNotFoundError({message: `The requested source '(${req.method})${req.url}' was not found`})) +}) + +//* Error Handling Middleware (must be after all routing logic) +app.use(requestErrorHandler) + + +export const server = app.listen(PORT, () => { + getLogger("backend-main").info(`Server started listening at port ${PORT}`) +}); diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index fd8f9c55b..dfbc2111c 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -10,56 +10,78 @@ const JWT_SIGNUP_LIFETIME = process.env.JWT_SIGNUP_LIFETIME! || '15m'; const JWT_SIGNUP_SECRET = process.env.JWT_SIGNUP_SECRET!; const MONGO_URL = process.env.MONGO_URL!; const NODE_ENV = process.env.NODE_ENV! || 'production'; -const OAUTH_CLIENT_SECRET_HEROKU = process.env.OAUTH_CLIENT_SECRET_HEROKU!; -const OAUTH_TOKEN_URL_HEROKU = process.env.OAUTH_TOKEN_URL_HEROKU!; +const VERBOSE_ERROR_OUTPUT = process.env.VERBOSE_ERROR_OUTPUT! === 'true' && true; +const LOKI_HOST = process.env.LOKI_HOST || undefined; +const CLIENT_SECRET_HEROKU = process.env.CLIENT_SECRET_HEROKU!; +const CLIENT_ID_HEROKU = process.env.CLIENT_ID_HEROKU!; +const CLIENT_ID_VERCEL = process.env.CLIENT_ID_VERCEL!; +const CLIENT_ID_NETLIFY = process.env.CLIENT_ID_NETLIFY!; +const CLIENT_ID_GITHUB = process.env.CLIENT_ID_GITHUB!; +const CLIENT_SECRET_VERCEL = process.env.CLIENT_SECRET_VERCEL!; +const CLIENT_SECRET_NETLIFY = process.env.CLIENT_SECRET_NETLIFY!; +const CLIENT_SECRET_GITHUB = process.env.CLIENT_SECRET_GITHUB!; +const CLIENT_SLUG_VERCEL= process.env.CLIENT_SLUG_VERCEL!; const POSTHOG_HOST = process.env.POSTHOG_HOST! || 'https://app.posthog.com'; -const POSTHOG_PROJECT_API_KEY = process.env.POSTHOG_PROJECT_API_KEY! || 'phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE'; -const PRIVATE_KEY = process.env.PRIVATE_KEY!; -const PUBLIC_KEY = process.env.PUBLIC_KEY!; +const POSTHOG_PROJECT_API_KEY = + process.env.POSTHOG_PROJECT_API_KEY! || + 'phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE'; const SENTRY_DSN = process.env.SENTRY_DSN!; const SITE_URL = process.env.SITE_URL!; -const SMTP_HOST = process.env.SMTP_HOST! || 'smtp.gmail.com'; -const SMTP_NAME = process.env.SMTP_NAME!; +const SMTP_HOST = process.env.SMTP_HOST!; +const SMTP_SECURE = process.env.SMTP_SECURE! === 'true' || false; +const SMTP_PORT = parseInt(process.env.SMTP_PORT!) || 587; const SMTP_USERNAME = process.env.SMTP_USERNAME!; const SMTP_PASSWORD = process.env.SMTP_PASSWORD!; +const SMTP_FROM_ADDRESS = process.env.SMTP_FROM_ADDRESS!; +const SMTP_FROM_NAME = process.env.SMTP_FROM_NAME! || 'Infisical'; const STRIPE_PRODUCT_CARD_AUTH = process.env.STRIPE_PRODUCT_CARD_AUTH!; const STRIPE_PRODUCT_PRO = process.env.STRIPE_PRODUCT_PRO!; const STRIPE_PRODUCT_STARTER = process.env.STRIPE_PRODUCT_STARTER!; const STRIPE_PUBLISHABLE_KEY = process.env.STRIPE_PUBLISHABLE_KEY!; const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY!; const STRIPE_WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET!; -const TELEMETRY_ENABLED = (process.env.TELEMETRY_ENABLED! !== 'false') && true; +const TELEMETRY_ENABLED = process.env.TELEMETRY_ENABLED! !== 'false' && true; export { - PORT, - EMAIL_TOKEN_LIFETIME, - ENCRYPTION_KEY, - JWT_AUTH_LIFETIME, - JWT_AUTH_SECRET, - JWT_REFRESH_LIFETIME, - JWT_REFRESH_SECRET, - JWT_SERVICE_SECRET, - JWT_SIGNUP_LIFETIME, - JWT_SIGNUP_SECRET, - MONGO_URL, - NODE_ENV, - OAUTH_CLIENT_SECRET_HEROKU, - OAUTH_TOKEN_URL_HEROKU, - POSTHOG_HOST, - POSTHOG_PROJECT_API_KEY, - PRIVATE_KEY, - PUBLIC_KEY, - SENTRY_DSN, - SITE_URL, - SMTP_HOST, - SMTP_NAME, - SMTP_USERNAME, - SMTP_PASSWORD, - STRIPE_PRODUCT_CARD_AUTH, - STRIPE_PRODUCT_PRO, - STRIPE_PRODUCT_STARTER, - STRIPE_PUBLISHABLE_KEY, - STRIPE_SECRET_KEY, - STRIPE_WEBHOOK_SECRET, - TELEMETRY_ENABLED + PORT, + EMAIL_TOKEN_LIFETIME, + ENCRYPTION_KEY, + JWT_AUTH_LIFETIME, + JWT_AUTH_SECRET, + JWT_REFRESH_LIFETIME, + JWT_REFRESH_SECRET, + JWT_SERVICE_SECRET, + JWT_SIGNUP_LIFETIME, + JWT_SIGNUP_SECRET, + MONGO_URL, + NODE_ENV, + VERBOSE_ERROR_OUTPUT, + LOKI_HOST, + CLIENT_ID_HEROKU, + CLIENT_ID_VERCEL, + CLIENT_ID_NETLIFY, + CLIENT_ID_GITHUB, + CLIENT_SECRET_HEROKU, + CLIENT_SECRET_VERCEL, + CLIENT_SECRET_NETLIFY, + CLIENT_SECRET_GITHUB, + CLIENT_SLUG_VERCEL, + POSTHOG_HOST, + POSTHOG_PROJECT_API_KEY, + SENTRY_DSN, + SITE_URL, + SMTP_HOST, + SMTP_PORT, + SMTP_SECURE, + SMTP_USERNAME, + SMTP_PASSWORD, + SMTP_FROM_ADDRESS, + SMTP_FROM_NAME, + STRIPE_PRODUCT_CARD_AUTH, + STRIPE_PRODUCT_PRO, + STRIPE_PRODUCT_STARTER, + STRIPE_PUBLISHABLE_KEY, + STRIPE_SECRET_KEY, + STRIPE_WEBHOOK_SECRET, + TELEMETRY_ENABLED }; diff --git a/backend/src/controllers/authController.ts b/backend/src/controllers/authController.ts index 9fbd58e93..20ac813d4 100644 --- a/backend/src/controllers/authController.ts +++ b/backend/src/controllers/authController.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ import { Request, Response } from 'express'; import jwt from 'jsonwebtoken'; import * as Sentry from '@sentry/node'; @@ -5,17 +6,17 @@ import * as bigintConversion from 'bigint-conversion'; const jsrp = require('jsrp'); import { User } from '../models'; import { createToken, issueTokens, clearTokens } from '../helpers/auth'; -import { - NODE_ENV, - JWT_AUTH_LIFETIME, - JWT_AUTH_SECRET, - JWT_REFRESH_SECRET +import { + NODE_ENV, + JWT_AUTH_LIFETIME, + JWT_AUTH_SECRET, + JWT_REFRESH_SECRET } from '../config'; declare module 'jsonwebtoken' { - export interface UserIDJwtPayload extends jwt.JwtPayload { - userId: string; - } + export interface UserIDJwtPayload extends jwt.JwtPayload { + userId: string; + } } const clientPublicKeys: any = {}; @@ -27,47 +28,45 @@ const clientPublicKeys: any = {}; * @returns */ export const login1 = async (req: Request, res: Response) => { - try { - const { - email, - clientPublicKey - }: { email: string; clientPublicKey: string } = req.body; - - const user = await User.findOne({ - email - }).select('+salt +verifier'); - + try { + const { + email, + clientPublicKey + }: { email: string; clientPublicKey: string } = req.body; - if (!user) throw new Error('Failed to find user'); + const user = await User.findOne({ + email + }).select('+salt +verifier'); - const server = new jsrp.server(); - server.init( - { - salt: user.salt, - verifier: user.verifier - }, - () => { - // generate server-side public key - const serverPublicKey = server.getPublicKey(); - clientPublicKeys[email] = { - clientPublicKey, - serverBInt: bigintConversion.bigintToBuf(server.bInt) - }; - + if (!user) throw new Error('Failed to find user'); - return res.status(200).send({ - serverPublicKey, - salt: user.salt - }); - } - ); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to start authentication process' - }); - } + const server = new jsrp.server(); + server.init( + { + salt: user.salt, + verifier: user.verifier + }, + () => { + // generate server-side public key + const serverPublicKey = server.getPublicKey(); + clientPublicKeys[email] = { + clientPublicKey, + serverBInt: bigintConversion.bigintToBuf(server.bInt) + }; + + return res.status(200).send({ + serverPublicKey, + salt: user.salt + }); + } + ); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to start authentication process' + }); + } }; /** @@ -78,59 +77,59 @@ export const login1 = async (req: Request, res: Response) => { * @returns */ export const login2 = async (req: Request, res: Response) => { - try { - const { email, clientProof } = req.body; - const user = await User.findOne({ - email - }).select('+salt +verifier +publicKey +encryptedPrivateKey +iv +tag'); + try { + const { email, clientProof } = req.body; + const user = await User.findOne({ + email + }).select('+salt +verifier +publicKey +encryptedPrivateKey +iv +tag'); - if (!user) throw new Error('Failed to find user'); + if (!user) throw new Error('Failed to find user'); - const server = new jsrp.server(); - server.init( - { - salt: user.salt, - verifier: user.verifier, - b: clientPublicKeys[email].serverBInt - }, - async () => { - server.setClientPublicKey(clientPublicKeys[email].clientPublicKey); + const server = new jsrp.server(); + server.init( + { + salt: user.salt, + verifier: user.verifier, + b: clientPublicKeys[email].serverBInt + }, + async () => { + server.setClientPublicKey(clientPublicKeys[email].clientPublicKey); - // compare server and client shared keys - if (server.checkClientProof(clientProof)) { - // issue tokens - const tokens = await issueTokens({ userId: user._id.toString() }); - - // store (refresh) token in httpOnly cookie - res.cookie('jid', tokens.refreshToken, { - httpOnly: true, - path: '/token', - sameSite: "strict", - secure: NODE_ENV === 'production' ? true : false - }); + // compare server and client shared keys + if (server.checkClientProof(clientProof)) { + // issue tokens + const tokens = await issueTokens({ userId: user._id.toString() }); - // return (access) token in response - return res.status(200).send({ - token: tokens.token, - publicKey: user.publicKey, - encryptedPrivateKey: user.encryptedPrivateKey, - iv: user.iv, - tag: user.tag - }); - } + // store (refresh) token in httpOnly cookie + res.cookie('jid', tokens.refreshToken, { + httpOnly: true, + path: '/', + sameSite: 'strict', + secure: NODE_ENV === 'production' ? true : false + }); - return res.status(400).send({ - message: 'Failed to authenticate. Try again?' - }); - } - ); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to authenticate. Try again?' - }); - } + // return (access) token in response + return res.status(200).send({ + token: tokens.token, + publicKey: user.publicKey, + encryptedPrivateKey: user.encryptedPrivateKey, + iv: user.iv, + tag: user.tag + }); + } + + return res.status(400).send({ + message: 'Failed to authenticate. Try again?' + }); + } + ); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to authenticate. Try again?' + }); + } }; /** @@ -140,29 +139,29 @@ export const login2 = async (req: Request, res: Response) => { * @returns */ export const logout = async (req: Request, res: Response) => { - try { - await clearTokens({ - userId: req.user._id.toString() - }); - - // clear httpOnly cookie - res.cookie('jid', '', { - httpOnly: true, - path: '/token', - sameSite: "strict", - secure: NODE_ENV === 'production' ? true : false - }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to logout' - }); - } + try { + await clearTokens({ + userId: req.user._id.toString() + }); - return res.status(200).send({ - message: 'Successfully logged out.' - }); + // clear httpOnly cookie + res.cookie('jid', '', { + httpOnly: true, + path: '/', + sameSite: 'strict', + secure: NODE_ENV === 'production' ? true : false + }); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to logout' + }); + } + + return res.status(200).send({ + message: 'Successfully logged out.' + }); }; /** @@ -172,9 +171,9 @@ export const logout = async (req: Request, res: Response) => { * @returns */ export const checkAuth = async (req: Request, res: Response) => - res.status(200).send({ - message: 'Authenticated' - }); + res.status(200).send({ + message: 'Authenticated' + }); /** * Return new token by redeeming refresh token @@ -183,42 +182,41 @@ export const checkAuth = async (req: Request, res: Response) => * @returns */ export const getNewToken = async (req: Request, res: Response) => { - try { - const refreshToken = req.cookies.jid; - - if (!refreshToken) { - throw new Error('Failed to find token in request cookies'); - } - - const decodedToken = ( - jwt.verify(refreshToken, JWT_REFRESH_SECRET) - ); - - const user = await User.findOne({ - _id: decodedToken.userId - }).select('+publicKey'); + try { + const refreshToken = req.cookies.jid; - if (!user) throw new Error('Failed to authenticate unfound user'); - if (!user?.publicKey) - throw new Error('Failed to authenticate not fully set up account'); - - const token = createToken({ - payload: { - userId: decodedToken.userId - }, - expiresIn: JWT_AUTH_LIFETIME, - secret: JWT_AUTH_SECRET - }); - - return res.status(200).send({ - token - }); - - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Invalid request' - }); - } + if (!refreshToken) { + throw new Error('Failed to find token in request cookies'); + } + + const decodedToken = ( + jwt.verify(refreshToken, JWT_REFRESH_SECRET) + ); + + const user = await User.findOne({ + _id: decodedToken.userId + }).select('+publicKey'); + + if (!user) throw new Error('Failed to authenticate unfound user'); + if (!user?.publicKey) + throw new Error('Failed to authenticate not fully set up account'); + + const token = createToken({ + payload: { + userId: decodedToken.userId + }, + expiresIn: JWT_AUTH_LIFETIME, + secret: JWT_AUTH_SECRET + }); + + return res.status(200).send({ + token + }); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Invalid request' + }); + } }; diff --git a/backend/src/controllers/botController.ts b/backend/src/controllers/botController.ts new file mode 100644 index 000000000..7819e32df --- /dev/null +++ b/backend/src/controllers/botController.ts @@ -0,0 +1,107 @@ +import { Request, Response } from 'express'; +import * as Sentry from '@sentry/node'; +import { Bot, BotKey } from '../models'; +import { createBot } from '../helpers/bot'; + +interface BotKey { + encryptedKey: string; + nonce: string; +} + +/** + * Return bot for workspace with id [workspaceId]. If a workspace bot doesn't exist, + * then create and return a new bot. + * @param req + * @param res + * @returns + */ +export const getBotByWorkspaceId = async (req: Request, res: Response) => { + let bot; + try { + const { workspaceId } = req.params; + + bot = await Bot.findOne({ + workspace: workspaceId + }); + + if (!bot) { + // case: bot doesn't exist for workspace with id [workspaceId] + // -> create a new bot and return it + bot = await createBot({ + name: 'Infisical Bot', + workspaceId + }); + } + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to get bot for workspace' + }); + } + + return res.status(200).send({ + bot + }); +}; + +/** + * Return bot with id [req.bot._id] with active state set to [isActive]. + * @param req + * @param res + * @returns + */ +export const setBotActiveState = async (req: Request, res: Response) => { + let bot; + try { + const { isActive, botKey }: { isActive: boolean, botKey: BotKey } = req.body; + + if (isActive) { + // bot state set to active -> share workspace key with bot + if (!botKey?.encryptedKey || !botKey?.nonce) { + return res.status(400).send({ + message: 'Failed to set bot state to active - missing bot key' + }); + } + + await BotKey.findOneAndUpdate({ + workspace: req.bot.workspace + }, { + encryptedKey: botKey.encryptedKey, + nonce: botKey.nonce, + sender: req.user._id, + bot: req.bot._id, + workspace: req.bot.workspace + }, { + upsert: true, + new: true + }); + } else { + // case: bot state set to inactive -> delete bot's workspace key + await BotKey.deleteOne({ + bot: req.bot._id + }); + } + + bot = await Bot.findOneAndUpdate({ + _id: req.bot._id + }, { + isActive + }, { + new: true + }); + + if (!bot) throw new Error('Failed to update bot active state'); + + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to update bot active state' + }); + } + + return res.status(200).send({ + bot + }); +}; diff --git a/backend/src/controllers/index.ts b/backend/src/controllers/index.ts index 2d3debfb5..1da61835f 100644 --- a/backend/src/controllers/index.ts +++ b/backend/src/controllers/index.ts @@ -1,4 +1,5 @@ import * as authController from './authController'; +import * as botController from './botController'; import * as integrationAuthController from './integrationAuthController'; import * as integrationController from './integrationController'; import * as keyController from './keyController'; @@ -16,6 +17,7 @@ import * as workspaceController from './workspaceController'; export { authController, + botController, integrationAuthController, integrationController, keyController, diff --git a/backend/src/controllers/integrationAuthController.ts b/backend/src/controllers/integrationAuthController.ts index 009bcd391..c242c239a 100644 --- a/backend/src/controllers/integrationAuthController.ts +++ b/backend/src/controllers/integrationAuthController.ts @@ -3,69 +3,45 @@ import * as Sentry from '@sentry/node'; import axios from 'axios'; import { readFileSync } from 'fs'; import { IntegrationAuth, Integration } from '../models'; -import { processOAuthTokenRes } from '../helpers/integrationAuth'; -import { INTEGRATION_SET, ENV_DEV } from '../variables'; -import { OAUTH_CLIENT_SECRET_HEROKU, OAUTH_TOKEN_URL_HEROKU } from '../config'; +import { INTEGRATION_SET, INTEGRATION_OPTIONS, ENV_DEV } from '../variables'; +import { IntegrationService } from '../services'; +import { getApps, revokeAccess } from '../integrations'; + +export const getIntegrationOptions = async ( + req: Request, + res: Response +) => { + return res.status(200).send({ + integrationOptions: INTEGRATION_OPTIONS + }); +} /** * Perform OAuth2 code-token exchange as part of integration [integration] for workspace with id [workspaceId] - * Note: integration [integration] must be set up compatible/designed for OAuth2 * @param req * @param res * @returns */ -export const integrationAuthOauthExchange = async ( +export const oAuthExchange = async ( req: Request, res: Response ) => { try { - let clientSecret; - const { workspaceId, code, integration } = req.body; if (!INTEGRATION_SET.has(integration)) throw new Error('Failed to validate integration'); - - // use correct client secret - switch (integration) { - case 'heroku': - clientSecret = OAUTH_CLIENT_SECRET_HEROKU; - } - - // TODO: unfinished - make compatible with other integration types - const res = await axios.post( - OAUTH_TOKEN_URL_HEROKU!, - new URLSearchParams({ - grant_type: 'authorization_code', - code: code, - client_secret: clientSecret - } as any) - ); - - const integrationAuth = await processOAuthTokenRes({ + + await IntegrationService.handleOAuthExchange({ workspaceId, integration, - res + code }); - - // create or replace integration - const integrationObj = await Integration.findOneAndUpdate( - { workspace: workspaceId, integration }, - { - workspace: workspaceId, - environment: ENV_DEV, - isActive: false, - app: null, - integration, - integrationAuth: integrationAuth._id - }, - { upsert: true, new: true } - ); } catch (err) { Sentry.setUser(null); Sentry.captureException(err); return res.status(400).send({ - message: 'Failed to get OAuth2 token' + message: 'Failed to get OAuth2 code-token exchange' }); } @@ -75,26 +51,25 @@ export const integrationAuthOauthExchange = async ( }; /** - * Return list of applications allowed for integration with id [integrationAuthId] + * Return list of applications allowed for integration with integration authorization id [integrationAuthId] * @param req * @param res * @returns */ export const getIntegrationAuthApps = async (req: Request, res: Response) => { - // TODO: unfinished - make compatible with other integration types let apps; try { - const res = await axios.get('https://api.heroku.com/apps', { - headers: { - Accept: 'application/vnd.heroku+json; version=3', - Authorization: 'Bearer ' + req.accessToken - } + apps = await getApps({ + integrationAuth: req.integrationAuth, + accessToken: req.accessToken }); - - apps = res.data.map((a: any) => ({ - name: a.name - })); - } catch (err) {} + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to get integration authorization applications' + }); + } return res.status(200).send({ apps @@ -108,46 +83,22 @@ export const getIntegrationAuthApps = async (req: Request, res: Response) => { * @returns */ export const deleteIntegrationAuth = async (req: Request, res: Response) => { - // TODO: unfinished - disable application via Heroku API and make compatible with other integration types try { const { integrationAuthId } = req.params; - // TODO: disable application via Heroku API; figure out what authorization id is - - const integrations = JSON.parse( - readFileSync('./src/json/integrations.json').toString() - ); - - let authorizationId; - switch (req.integrationAuth.integration) { - case 'heroku': - authorizationId = integrations.heroku.clientId; - } - - // not sure what authorizationId is? - // // revoke authorization - // const res2 = await axios.delete( - // `https://api.heroku.com/oauth/authorizations/${authorizationId}`, - // { - // headers: { - // 'Accept': 'application/vnd.heroku+json; version=3', - // 'Authorization': 'Bearer ' + req.accessToken - // } - // } - // ); - - const deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({ - _id: integrationAuthId + await revokeAccess({ + integrationAuth: req.integrationAuth, + accessToken: req.accessToken }); - - if (deletedIntegrationAuth) { - await Integration.deleteMany({ - integrationAuth: deletedIntegrationAuth._id - }); - } } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); return res.status(400).send({ message: 'Failed to delete integration authorization' }); } -}; + + return res.status(200).send({ + message: 'Successfully deleted integration authorization' + }); +} \ No newline at end of file diff --git a/backend/src/controllers/integrationController.ts b/backend/src/controllers/integrationController.ts index b75d9b74a..910c7e825 100644 --- a/backend/src/controllers/integrationController.ts +++ b/backend/src/controllers/integrationController.ts @@ -1,11 +1,9 @@ import { Request, Response } from 'express'; import { readFileSync } from 'fs'; import * as Sentry from '@sentry/node'; -import axios from 'axios'; -import { Integration } from '../models'; -import { decryptAsymmetric } from '../utils/crypto'; -import { decryptSecrets } from '../helpers/secret'; -import { PRIVATE_KEY } from '../config'; +import { Integration, Bot, BotKey } from '../models'; +import { EventService } from '../services'; +import { eventPushSecrets } from '../events'; interface Key { encryptedKey: string; @@ -24,104 +22,58 @@ interface PushSecret { type: 'shared' | 'personal'; } -/** - * Return list of all available integrations on Infisical - * @param req - * @param res - * @returns - */ -export const getIntegrations = async (req: Request, res: Response) => { - let integrations; - try { - integrations = JSON.parse( - readFileSync('./src/json/integrations.json').toString() - ); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to get integrations' - }); - } - - return res.status(200).send({ - integrations - }); -}; - -/** - * Sync secrets [secrets] to integration with id [integrationId] - * @param req - * @param res - * @returns - */ -export const syncIntegration = async (req: Request, res: Response) => { - // TODO: unfinished - make more versatile to accomodate for other integrations - try { - const { key, secrets }: { key: Key; secrets: PushSecret[] } = req.body; - const symmetricKey = decryptAsymmetric({ - ciphertext: key.encryptedKey, - nonce: key.nonce, - publicKey: req.user.publicKey, - privateKey: PRIVATE_KEY - }); - - // decrypt secrets with symmetric key - const content = decryptSecrets({ - secrets, - key: symmetricKey, - format: 'object' - }); - - // TODO: make integration work for other integrations as well - const res = await axios.patch( - `https://api.heroku.com/apps/${req.integration.app}/config-vars`, - content, - { - headers: { - Accept: 'application/vnd.heroku+json; version=3', - Authorization: 'Bearer ' + req.accessToken - } - } - ); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to sync secrets with integration' - }); - } - - return res.status(200).send({ - message: 'Successfully synced secrets with integration' - }); -}; - /** * Change environment or name of integration with id [integrationId] * @param req * @param res * @returns */ -export const modifyIntegration = async (req: Request, res: Response) => { +export const updateIntegration = async (req: Request, res: Response) => { let integration; + + // TODO: add integration-specific validation to ensure that each + // integration has the correct fields populated in [Integration] + try { - const { update } = req.body; - + const { + app, + environment, + isActive, + target, // vercel-specific integration param + context, // netlify-specific integration param + siteId // netlify-specific integration param + } = req.body; + integration = await Integration.findOneAndUpdate( { _id: req.integration._id }, - update, + { + environment, + isActive, + app, + target, + context, + siteId + }, { new: true } ); + + if (integration) { + // trigger event - push secrets + EventService.handleEvent({ + event: eventPushSecrets({ + workspaceId: integration.workspace.toString() + }) + }); + } } catch (err) { Sentry.setUser({ email: req.user.email }); Sentry.captureException(err); return res.status(400).send({ - message: 'Failed to modify integration' + message: 'Failed to update integration' }); } @@ -131,7 +83,8 @@ export const modifyIntegration = async (req: Request, res: Response) => { }; /** - * Delete integration with id [integrationId] + * Delete integration with id [integrationId] and deactivate bot if there are + * no integrations left * @param req * @param res * @returns @@ -144,6 +97,29 @@ export const deleteIntegration = async (req: Request, res: Response) => { deletedIntegration = await Integration.findOneAndDelete({ _id: integrationId }); + + if (!deletedIntegration) throw new Error('Failed to find integration'); + + const integrations = await Integration.find({ + workspace: deletedIntegration.workspace + }); + + if (integrations.length === 0) { + // case: no integrations left, deactivate bot + const bot = await Bot.findOneAndUpdate({ + workspace: deletedIntegration.workspace + }, { + isActive: false + }, { + new: true + }); + + if (bot) { + await BotKey.deleteOne({ + bot: bot._id + }); + } + } } catch (err) { Sentry.setUser({ email: req.user.email }); Sentry.captureException(err); diff --git a/backend/src/controllers/keyController.ts b/backend/src/controllers/keyController.ts index 1c9b5e15c..70446a76c 100644 --- a/backend/src/controllers/keyController.ts +++ b/backend/src/controllers/keyController.ts @@ -2,7 +2,6 @@ import { Request, Response } from 'express'; import * as Sentry from '@sentry/node'; import { Key } from '../models'; import { findMembership } from '../helpers/membership'; -import { PUBLIC_KEY } from '../config'; import { GRANTED } from '../variables'; /** @@ -17,16 +16,6 @@ export const uploadKey = async (req: Request, res: Response) => { const { workspaceId } = req.params; const { key } = req.body; - // validate membership of sender - const senderMembership = await findMembership({ - user: req.user._id, - workspace: workspaceId - }); - - if (!senderMembership) { - throw new Error('Failed sender membership validation for workspace'); - } - // validate membership of receiver const receiverMembership = await findMembership({ user: key.userId, @@ -94,16 +83,4 @@ export const getLatestKey = async (req: Request, res: Response) => { } return res.status(200).send(resObj); -}; - -/** - * Return public key of Infisical - * @param req - * @param res - * @returns - */ -export const getPublicKeyInfisical = async (req: Request, res: Response) => { - return res.status(200).send({ - publicKey: PUBLIC_KEY - }); -}; +}; \ No newline at end of file diff --git a/backend/src/controllers/membershipOrgController.ts b/backend/src/controllers/membershipOrgController.ts index bc2804996..a2159bcd0 100644 --- a/backend/src/controllers/membershipOrgController.ts +++ b/backend/src/controllers/membershipOrgController.ts @@ -217,7 +217,7 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => { try { const { email, code } = req.body; - user = await User.findOne({ email }); + user = await User.findOne({ email }).select('+publicKey'); if (user && user?.publicKey) { // case: user has already completed account return res.status(403).send({ @@ -257,7 +257,7 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => { Sentry.setUser(null); Sentry.captureException(err); return res.status(400).send({ - error: 'Failed email magic link confirmation' + error: 'Failed email magic link verification for organization invitation' }); } diff --git a/backend/src/controllers/passwordController.ts b/backend/src/controllers/passwordController.ts index 86b5355db..b029bc0be 100644 --- a/backend/src/controllers/passwordController.ts +++ b/backend/src/controllers/passwordController.ts @@ -1,11 +1,121 @@ import { Request, Response } from 'express'; import * as Sentry from '@sentry/node'; +import crypto from 'crypto'; const jsrp = require('jsrp'); import * as bigintConversion from 'bigint-conversion'; -import { User, BackupPrivateKey } from '../models'; +import { User, Token, BackupPrivateKey } from '../models'; +import { checkEmailVerification } from '../helpers/signup'; +import { createToken } from '../helpers/auth'; +import { sendMail } from '../helpers/nodemailer'; +import { JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, SITE_URL } from '../config'; const clientPublicKeys: any = {}; +/** + * Password reset step 1: Send email verification link to email [email] + * for account recovery. + * @param req + * @param res + * @returns + */ +export const emailPasswordReset = async (req: Request, res: Response) => { + let email: string; + try { + email = req.body.email; + + const user = await User.findOne({ email }).select('+publicKey'); + if (!user || !user?.publicKey) { + // case: user has already completed account + + return res.status(403).send({ + error: 'Failed to send email verification for password reset' + }); + } + + const token = crypto.randomBytes(16).toString('hex'); + + await Token.findOneAndUpdate( + { email }, + { + email, + token, + createdAt: new Date() + }, + { upsert: true, new: true } + ); + + await sendMail({ + template: 'passwordReset.handlebars', + subjectLine: 'Infisical password reset', + recipients: [email], + substitutions: { + email, + token, + callback_url: SITE_URL + '/password-reset' + } + }); + + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to send email for account recovery' + }); + } + + return res.status(200).send({ + message: `Sent an email for account recovery to ${email}` + }); +} + +/** + * Password reset step 2: Verify email verification link sent to email [email] + * @param req + * @param res + * @returns + */ +export const emailPasswordResetVerify = async (req: Request, res: Response) => { + let user, token; + try { + const { email, code } = req.body; + + user = await User.findOne({ email }).select('+publicKey'); + if (!user || !user?.publicKey) { + // case: user doesn't exist with email [email] or + // hasn't even completed their account + return res.status(403).send({ + error: 'Failed email verification for password reset' + }); + } + + await checkEmailVerification({ + email, + code + }); + + // generate temporary password-reset token + token = createToken({ + payload: { + userId: user._id.toString() + }, + expiresIn: JWT_SIGNUP_LIFETIME, + secret: JWT_SIGNUP_SECRET + }); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed email verification for password reset' + }); + } + + return res.status(200).send({ + message: 'Successfully verified email', + user, + token + }); +} + /** * Return [salt] and [serverPublicKey] as part of step 1 of SRP protocol * @param req @@ -43,7 +153,7 @@ export const srp1 = async (req: Request, res: Response) => { } ); } catch (err) { - Sentry.setUser(null); + Sentry.setUser({ email: req.user.email }); Sentry.captureException(err); return res.status(400).send({ error: 'Failed to start change password process' @@ -110,7 +220,7 @@ export const changePassword = async (req: Request, res: Response) => { } ); } catch (err) { - Sentry.setUser(null); + Sentry.setUser({ email: req.user.email }); Sentry.captureException(err); return res.status(400).send({ error: 'Failed to change password. Try again?' @@ -180,10 +290,73 @@ export const createBackupPrivateKey = async (req: Request, res: Response) => { } ); } catch (err) { - Sentry.setUser(null); + Sentry.setUser({ email: req.user.email }); Sentry.captureException(err); return res.status(400).send({ message: 'Failed to update backup private key' }); } }; + +/** + * Return backup private key for user + * @param req + * @param res + * @returns + */ +export const getBackupPrivateKey = async (req: Request, res: Response) => { + let backupPrivateKey; + try { + backupPrivateKey = await BackupPrivateKey.findOne({ + user: req.user._id + }).select('+encryptedPrivateKey +iv +tag'); + + if (!backupPrivateKey) throw new Error('Failed to find backup private key'); + } catch (err) { + Sentry.setUser({ email: req.user.email}); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to get backup private key' + }); + } + + return res.status(200).send({ + backupPrivateKey + }); +} + +export const resetPassword = async (req: Request, res: Response) => { + try { + const { + encryptedPrivateKey, + iv, + tag, + salt, + verifier, + } = req.body; + + await User.findByIdAndUpdate( + req.user._id.toString(), + { + encryptedPrivateKey, + iv, + tag, + salt, + verifier + }, + { + new: true + } + ); + } catch (err) { + Sentry.setUser({ email: req.user.email}); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to get backup private key' + }); + } + + return res.status(200).send({ + message: 'Successfully reset password' + }); +} \ No newline at end of file diff --git a/backend/src/controllers/secretController.ts b/backend/src/controllers/secretController.ts index d1cf5f65d..bfd9aee1f 100644 --- a/backend/src/controllers/secretController.ts +++ b/backend/src/controllers/secretController.ts @@ -7,8 +7,9 @@ import { reformatPullSecrets } from '../helpers/secret'; import { pushKeys } from '../helpers/key'; +import { eventPushSecrets } from '../events'; +import { EventService } from '../services'; import { ENV_SET } from '../variables'; - import { postHogClient } from '../services'; interface PushSecret { @@ -60,7 +61,8 @@ export const pushSecrets = async (req: Request, res: Response) => { workspaceId, keys }); - + + if (postHogClient) { postHogClient.capture({ event: 'secrets pushed', @@ -74,6 +76,13 @@ export const pushSecrets = async (req: Request, res: Response) => { }); } + // trigger event - push secrets + EventService.handleEvent({ + event: eventPushSecrets({ + workspaceId + }) + }); + } catch (err) { Sentry.setUser({ email: req.user.email }); Sentry.captureException(err); @@ -192,7 +201,7 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => { }; if (postHogClient) { - // capture secrets pushed event in production + // capture secrets pulled event in production postHogClient.capture({ distinctId: req.serviceToken.user.email, event: 'secrets pulled', diff --git a/backend/src/controllers/serviceTokenController.ts b/backend/src/controllers/serviceTokenController.ts index ecc3ca0ca..4cc53c4f9 100644 --- a/backend/src/controllers/serviceTokenController.ts +++ b/backend/src/controllers/serviceTokenController.ts @@ -58,7 +58,8 @@ export const createServiceToken = async (req: Request, res: Response) => { token = createToken({ payload: { - serviceTokenId: serviceToken._id.toString() + serviceTokenId: serviceToken._id.toString(), + workspaceId }, expiresIn: expiresIn, secret: JWT_SERVICE_SECRET diff --git a/backend/src/events/index.ts b/backend/src/events/index.ts new file mode 100644 index 000000000..461a3ece6 --- /dev/null +++ b/backend/src/events/index.ts @@ -0,0 +1,5 @@ +import { eventPushSecrets } from "./secret" + +export { + eventPushSecrets +} \ No newline at end of file diff --git a/backend/src/events/secret.ts b/backend/src/events/secret.ts new file mode 100644 index 000000000..8bb3a86c3 --- /dev/null +++ b/backend/src/events/secret.ts @@ -0,0 +1,37 @@ +import { EVENT_PUSH_SECRETS } from '../variables'; + +interface PushSecret { + ciphertextKey: string; + ivKey: string; + tagKey: string; + hashKey: string; + ciphertextValue: string; + ivValue: string; + tagValue: string; + hashValue: string; + type: 'shared' | 'personal'; +} + +/** + * Return event for pushing secrets + * @param {Object} obj + * @param {String} obj.workspaceId - id of workspace to push secrets to + * @returns + */ +const eventPushSecrets = ({ + workspaceId, +}: { + workspaceId: string; +}) => { + return ({ + name: EVENT_PUSH_SECRETS, + workspaceId, + payload: { + + } + }); +} + +export { + eventPushSecrets +} diff --git a/backend/src/helpers/bot.ts b/backend/src/helpers/bot.ts new file mode 100644 index 000000000..abaf73af4 --- /dev/null +++ b/backend/src/helpers/bot.ts @@ -0,0 +1,230 @@ +import * as Sentry from '@sentry/node'; +import { + Bot, + BotKey, + Secret, + ISecret, + IUser +} from '../models'; +import { + generateKeyPair, + encryptSymmetric, + decryptSymmetric, + decryptAsymmetric +} from '../utils/crypto'; +import { decryptSecrets } from '../helpers/secret'; +import { ENCRYPTION_KEY } from '../config'; +import { SECRET_SHARED } from '../variables'; + +/** + * Create an inactive bot with name [name] for workspace with id [workspaceId] + * @param {Object} obj + * @param {String} obj.name - name of bot + * @param {String} obj.workspaceId - id of workspace that bot belongs to + */ +const createBot = async ({ + name, + workspaceId, +}: { + name: string; + workspaceId: string; +}) => { + let bot; + try { + const { publicKey, privateKey } = generateKeyPair(); + const { ciphertext, iv, tag } = encryptSymmetric({ + plaintext: privateKey, + key: ENCRYPTION_KEY + }); + + bot = await new Bot({ + name, + workspace: workspaceId, + isActive: false, + publicKey, + encryptedPrivateKey: ciphertext, + iv, + tag + }).save(); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to create bot'); + } + + return bot; +} + +/** + * Return decrypted secrets for workspace with id [workspaceId] + * and [environment] using bot + * @param {Object} obj + * @param {String} obj.workspaceId - id of workspace + * @param {String} obj.environment - environment + */ +const getSecretsHelper = async ({ + workspaceId, + environment +}: { + workspaceId: string; + environment: string; +}) => { + const content = {} as any; + try { + const key = await getKey({ workspaceId }); + const secrets = await Secret.find({ + workspaceId, + environment, + type: SECRET_SHARED + }); + + secrets.forEach((secret: ISecret) => { + const secretKey = decryptSymmetric({ + ciphertext: secret.secretKeyCiphertext, + iv: secret.secretKeyIV, + tag: secret.secretKeyTag, + key + }); + + const secretValue = decryptSymmetric({ + ciphertext: secret.secretValueCiphertext, + iv: secret.secretValueIV, + tag: secret.secretValueTag, + key + }); + + content[secretKey] = secretValue; + }); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to get secrets'); + } + + return content; +} + +/** + * Return bot's copy of the workspace key for workspace + * with id [workspaceId] + * @param {Object} obj + * @param {String} obj.workspaceId - id of workspace + * @returns {String} key - decrypted workspace key + */ +const getKey = async ({ workspaceId }: { workspaceId: string }) => { + let key; + try { + const botKey = await BotKey.findOne({ + workspace: workspaceId + }).populate<{ sender: IUser }>('sender', 'publicKey'); + + if (!botKey) throw new Error('Failed to find bot key'); + + const bot = await Bot.findOne({ + workspace: workspaceId + }).select('+encryptedPrivateKey +iv +tag'); + + if (!bot) throw new Error('Failed to find bot'); + if (!bot.isActive) throw new Error('Bot is not active'); + + const privateKeyBot = decryptSymmetric({ + ciphertext: bot.encryptedPrivateKey, + iv: bot.iv, + tag: bot.tag, + key: ENCRYPTION_KEY + }); + + key = decryptAsymmetric({ + ciphertext: botKey.encryptedKey, + nonce: botKey.nonce, + publicKey: botKey.sender.publicKey as string, + privateKey: privateKeyBot + }); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to get workspace key'); + } + + return key; +} + +/** + * Return symmetrically encrypted [plaintext] using the + * key for workspace with id [workspaceId] + * @param {Object} obj1 + * @param {String} obj1.workspaceId - id of workspace + * @param {String} obj1.plaintext - plaintext to encrypt + */ +const encryptSymmetricHelper = async ({ + workspaceId, + plaintext +}: { + workspaceId: string; + plaintext: string; +}) => { + + try { + const key = await getKey({ workspaceId }); + const { ciphertext, iv, tag } = encryptSymmetric({ + plaintext, + key + }); + + return ({ + ciphertext, + iv, + tag + }); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to perform symmetric encryption with bot'); + } +} +/** + * Return symmetrically decrypted [ciphertext] using the + * key for workspace with id [workspaceId] + * @param {Object} obj + * @param {String} obj.workspaceId - id of workspace + * @param {String} obj.ciphertext - ciphertext to decrypt + * @param {String} obj.iv - iv + * @param {String} obj.tag - tag + */ +const decryptSymmetricHelper = async ({ + workspaceId, + ciphertext, + iv, + tag +}: { + workspaceId: string; + ciphertext: string; + iv: string; + tag: string; +}) => { + let plaintext; + try { + const key = await getKey({ workspaceId }); + const plaintext = decryptSymmetric({ + ciphertext, + iv, + tag, + key + }); + + return plaintext; + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to perform symmetric decryption with bot'); + } + + return plaintext; +} + +export { + createBot, + getSecretsHelper, + encryptSymmetricHelper, + decryptSymmetricHelper +} \ No newline at end of file diff --git a/backend/src/helpers/event.ts b/backend/src/helpers/event.ts new file mode 100644 index 000000000..4128752e5 --- /dev/null +++ b/backend/src/helpers/event.ts @@ -0,0 +1,51 @@ +import { Bot, IBot } from '../models'; +import * as Sentry from '@sentry/node'; +import { EVENT_PUSH_SECRETS } from '../variables'; +import { IntegrationService } from '../services'; + +interface Event { + name: string; + workspaceId: string; + payload: any; +} + +/** + * Handle event [event] + * @param {Object} obj + * @param {Event} obj.event - an event + * @param {String} obj.event.name - name of event + * @param {String} obj.event.workspaceId - id of workspace that event is part of + * @param {Object} obj.event.payload - payload of event (depends on event) + */ +const handleEventHelper = async ({ + event +}: { + event: Event; +}) => { + const { workspaceId } = event; + + // TODO: moduralize bot check into separate function + const bot = await Bot.findOne({ + workspace: workspaceId, + isActive: true + }); + + if (!bot) return; + + try { + switch (event.name) { + case EVENT_PUSH_SECRETS: + IntegrationService.syncIntegrations({ + workspaceId + }); + break; + } + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + } +} + +export { + handleEventHelper +} \ No newline at end of file diff --git a/backend/src/helpers/integration.ts b/backend/src/helpers/integration.ts index e69de29bb..d92156ece 100644 --- a/backend/src/helpers/integration.ts +++ b/backend/src/helpers/integration.ts @@ -0,0 +1,358 @@ +import * as Sentry from '@sentry/node'; +import { + Bot, + Integration, + IntegrationAuth, +} from '../models'; +import { exchangeCode, exchangeRefresh, syncSecrets } from '../integrations'; +import { BotService } from '../services'; +import { + ENV_DEV, + EVENT_PUSH_SECRETS, + INTEGRATION_VERCEL, + INTEGRATION_NETLIFY +} from '../variables'; +import { UnauthorizedRequestError } from '../utils/errors'; +import RequestError from '../utils/requestError'; + +interface Update { + workspace: string; + integration: string; + teamId?: string; + accountId?: string; +} + +/** + * Perform OAuth2 code-token exchange for workspace with id [workspaceId] and integration + * named [integration] + * - Store integration access and refresh tokens returned from the OAuth2 code-token exchange + * - Add placeholder inactive integration + * - Create bot sequence for integration + * @param {Object} obj + * @param {String} obj.workspaceId - id of workspace + * @param {String} obj.integration - name of integration + * @param {String} obj.code - code +*/ +const handleOAuthExchangeHelper = async ({ + workspaceId, + integration, + code +}: { + workspaceId: string; + integration: string; + code: string; +}) => { + let action; + let integrationAuth; + try { + const bot = await Bot.findOne({ + workspace: workspaceId, + isActive: true + }); + + if (!bot) throw new Error('Bot must be enabled for OAuth2 code-token exchange'); + + // exchange code for access and refresh tokens + const res = await exchangeCode({ + integration, + code + }); + + const update: Update = { + workspace: workspaceId, + integration + } + + switch (integration) { + case INTEGRATION_VERCEL: + update.teamId = res.teamId; + break; + case INTEGRATION_NETLIFY: + update.accountId = res.accountId; + break; + } + + integrationAuth = await IntegrationAuth.findOneAndUpdate({ + workspace: workspaceId, + integration + }, update, { + new: true, + upsert: true + }); + + if (res.refreshToken) { + // case: refresh token returned from exchange + // set integration auth refresh token + await setIntegrationAuthRefreshHelper({ + integrationAuthId: integrationAuth._id.toString(), + refreshToken: res.refreshToken + }); + } + + if (res.accessToken) { + // case: access token returned from exchange + // set integration auth access token + await setIntegrationAuthAccessHelper({ + integrationAuthId: integrationAuth._id.toString(), + accessToken: res.accessToken, + accessExpiresAt: res.accessExpiresAt + }); + } + + // initialize new integration after exchange + await new Integration({ + workspace: workspaceId, + environment: ENV_DEV, + isActive: false, + app: null, + integration, + integrationAuth: integrationAuth._id + }).save(); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to handle OAuth2 code-token exchange') + } +} +/** + * Sync/push environment variables in workspace with id [workspaceId] to + * all active integrations for that workspace + * @param {Object} obj + * @param {Object} obj.workspaceId - id of workspace + */ +const syncIntegrationsHelper = async ({ + workspaceId +}: { + workspaceId: string; +}) => { + let integrations; + try { + + integrations = await Integration.find({ + workspace: workspaceId, + isActive: true, + app: { $ne: null } + }); + + // for each workspace integration, sync/push secrets + // to that integration + for await (const integration of integrations) { + // get workspace, environment (shared) secrets + const secrets = await BotService.getSecrets({ // issue here? + workspaceId: integration.workspace.toString(), + environment: integration.environment + }); + + const integrationAuth = await IntegrationAuth.findById(integration.integrationAuth); + if (!integrationAuth) throw new Error('Failed to find integration auth'); + + // get integration auth access token + const accessToken = await getIntegrationAuthAccessHelper({ + integrationAuthId: integration.integrationAuth.toString() + }); + + // sync secrets to integration + await syncSecrets({ + integration, + integrationAuth, + secrets, + accessToken + }); + } + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to sync secrets to integrations'); + } +} + +/** + * Return decrypted refresh token using the bot's copy + * of the workspace key for workspace belonging to integration auth + * with id [integrationAuthId] + * @param {Object} obj + * @param {String} obj.integrationAuthId - id of integration auth + * @param {String} refreshToken - decrypted refresh token + */ + const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: string }) => { + let refreshToken; + + try { + const integrationAuth = await IntegrationAuth + .findById(integrationAuthId) + .select('+refreshCiphertext +refreshIV +refreshTag'); + + if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'}); + + refreshToken = await BotService.decryptSymmetric({ + workspaceId: integrationAuth.workspace.toString(), + ciphertext: integrationAuth.refreshCiphertext as string, + iv: integrationAuth.refreshIV as string, + tag: integrationAuth.refreshTag as string + }); + + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + if(err instanceof RequestError) + throw err + else + throw new Error('Failed to get integration refresh token'); + } + + return refreshToken; +} + +/** + * Return decrypted access token using the bot's copy + * of the workspace key for workspace belonging to integration auth + * with id [integrationAuthId] + * @param {Object} obj + * @param {String} obj.integrationAuthId - id of integration auth + * @returns {String} accessToken - decrypted access token + */ +const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: string }) => { + let accessToken; + + try { + const integrationAuth = await IntegrationAuth + .findById(integrationAuthId) + .select('workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext'); + + if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'}); + + accessToken = await BotService.decryptSymmetric({ + workspaceId: integrationAuth.workspace.toString(), + ciphertext: integrationAuth.accessCiphertext as string, + iv: integrationAuth.accessIV as string, + tag: integrationAuth.accessTag as string + }); + + if (integrationAuth?.accessExpiresAt && integrationAuth?.refreshCiphertext) { + // there is a access token expiration date + // and refresh token to exchange with the OAuth2 server + + if (integrationAuth.accessExpiresAt < new Date()) { + // access token is expired + const refreshToken = await getIntegrationAuthRefreshHelper({ integrationAuthId }); + accessToken = await exchangeRefresh({ + integration: integrationAuth.integration, + refreshToken + }); + } + } + + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + if(err instanceof RequestError) + throw err + else + throw new Error('Failed to get integration access token'); + } + + return accessToken; +} + +/** + * Encrypt refresh token [refreshToken] using the bot's copy + * of the workspace key for workspace belonging to integration auth + * with id [integrationAuthId] and store it + * @param {Object} obj + * @param {String} obj.integrationAuthId - id of integration auth + * @param {String} obj.refreshToken - refresh token + */ +const setIntegrationAuthRefreshHelper = async ({ + integrationAuthId, + refreshToken +}: { + integrationAuthId: string; + refreshToken: string; +}) => { + + let integrationAuth; + try { + integrationAuth = await IntegrationAuth + .findById(integrationAuthId); + + if (!integrationAuth) throw new Error('Failed to find integration auth'); + + const obj = await BotService.encryptSymmetric({ + workspaceId: integrationAuth.workspace.toString(), + plaintext: refreshToken + }); + + integrationAuth = await IntegrationAuth.findOneAndUpdate({ + _id: integrationAuthId + }, { + refreshCiphertext: obj.ciphertext, + refreshIV: obj.iv, + refreshTag: obj.tag + }, { + new: true + }); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to set integration auth refresh token'); + } + + return integrationAuth; +} + +/** + * Encrypt access token [accessToken] using the bot's copy + * of the workspace key for workspace belonging to integration auth + * with id [integrationAuthId] and store it along with [accessExpiresAt] + * @param {Object} obj + * @param {String} obj.integrationAuthId - id of integration auth + * @param {String} obj.accessToken - access token + * @param {Date} obj.accessExpiresAt - expiration date of access token + */ +const setIntegrationAuthAccessHelper = async ({ + integrationAuthId, + accessToken, + accessExpiresAt +}: { + integrationAuthId: string; + accessToken: string; + accessExpiresAt: Date; +}) => { + let integrationAuth; + try { + integrationAuth = await IntegrationAuth.findById(integrationAuthId); + + if (!integrationAuth) throw new Error('Failed to find integration auth'); + + const obj = await BotService.encryptSymmetric({ + workspaceId: integrationAuth.workspace.toString(), + plaintext: accessToken + }); + + integrationAuth = await IntegrationAuth.findOneAndUpdate({ + _id: integrationAuthId + }, { + accessCiphertext: obj.ciphertext, + accessIV: obj.iv, + accessTag: obj.tag, + accessExpiresAt + }, { + new: true + }); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to save integration auth access token'); + } + + return integrationAuth; +} + +export { + handleOAuthExchangeHelper, + syncIntegrationsHelper, + getIntegrationAuthRefreshHelper, + getIntegrationAuthAccessHelper, + setIntegrationAuthRefreshHelper, + setIntegrationAuthAccessHelper +} \ No newline at end of file diff --git a/backend/src/helpers/integrationAuth.ts b/backend/src/helpers/integrationAuth.ts index 17f101676..e69de29bb 100644 --- a/backend/src/helpers/integrationAuth.ts +++ b/backend/src/helpers/integrationAuth.ts @@ -1,174 +0,0 @@ -import * as Sentry from '@sentry/node'; -import axios from 'axios'; -import { IntegrationAuth } from '../models'; -import { encryptSymmetric, decryptSymmetric } from '../utils/crypto'; -import { IIntegrationAuth } from '../models'; -import { - ENCRYPTION_KEY, - OAUTH_CLIENT_SECRET_HEROKU, - OAUTH_TOKEN_URL_HEROKU -} from '../config'; - -/** - * Process token exchange and refresh responses from respective OAuth2 authorization servers by - * encrypting access and refresh tokens, computing new access token expiration times [accessExpiresAt], - * and upserting them into the DB for workspace with id [workspaceId] and integration [integration]. - * @param {Object} obj - * @param {String} obj.workspaceId - id of workspace - * @param {String} obj.integration - name of integration (e.g. heroku) - * @param {Object} obj.res - response from OAuth2 authorization server - */ -const processOAuthTokenRes = async ({ - workspaceId, - integration, - res -}: { - workspaceId: string; - integration: string; - res: any; -}): Promise => { - let integrationAuth; - try { - // encrypt refresh + access tokens - const { - ciphertext: refreshCiphertext, - iv: refreshIV, - tag: refreshTag - } = encryptSymmetric({ - plaintext: res.data.refresh_token, - key: ENCRYPTION_KEY - }); - - const { - ciphertext: accessCiphertext, - iv: accessIV, - tag: accessTag - } = encryptSymmetric({ - plaintext: res.data.access_token, - key: ENCRYPTION_KEY - }); - - // compute access token expiration date - const accessExpiresAt = new Date(); - accessExpiresAt.setSeconds( - accessExpiresAt.getSeconds() + res.data.expires_in - ); - - // create or replace integration authorization with encrypted tokens - // and access token expiration date - integrationAuth = await IntegrationAuth.findOneAndUpdate( - { workspace: workspaceId, integration }, - { - workspace: workspaceId, - integration, - refreshCiphertext, - refreshIV, - refreshTag, - accessCiphertext, - accessIV, - accessTag, - accessExpiresAt - }, - { upsert: true, new: true } - ); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error( - 'Failed to process OAuth2 authorization server token response' - ); - } - - return integrationAuth; -}; - -/** - * Return access token for integration either by decrypting a non-expired access token [accessCiphertext] on - * the integration authorization document or by requesting a new one by decrypting and exchanging the - * refresh token [refreshCiphertext] with the respective OAuth2 authorization server. - * @param {Object} obj - * @param {IIntegrationAuth} obj.integrationAuth - an integration authorization document - * @returns {String} access token - new access token - */ -const getOAuthAccessToken = async ({ - integrationAuth -}: { - integrationAuth: IIntegrationAuth; -}) => { - let accessToken; - try { - const { - refreshCiphertext, - refreshIV, - refreshTag, - accessCiphertext, - accessIV, - accessTag, - accessExpiresAt - } = integrationAuth; - - if ( - refreshCiphertext && - refreshIV && - refreshTag && - accessCiphertext && - accessIV && - accessTag && - accessExpiresAt - ) { - if (accessExpiresAt < new Date()) { - // case: access token expired - // TODO: fetch another access token - - let clientSecret; - switch (integrationAuth.integration) { - case 'heroku': - clientSecret = OAUTH_CLIENT_SECRET_HEROKU; - } - - // record new access token and refresh token - // encrypt refresh + access tokens - const refreshToken = decryptSymmetric({ - ciphertext: refreshCiphertext, - iv: refreshIV, - tag: refreshTag, - key: ENCRYPTION_KEY - }); - - // TODO: make route compatible with other integration types - const res = await axios.post( - OAUTH_TOKEN_URL_HEROKU, // maybe shouldn't be a config variable? - new URLSearchParams({ - grant_type: 'refresh_token', - refresh_token: refreshToken, - client_secret: clientSecret - } as any) - ); - - accessToken = res.data.access_token; - - await processOAuthTokenRes({ - workspaceId: integrationAuth.workspace.toString(), - integration: integrationAuth.integration, - res - }); - } else { - // case: access token still works - accessToken = decryptSymmetric({ - ciphertext: accessCiphertext, - iv: accessIV, - tag: accessTag, - key: ENCRYPTION_KEY - }); - } - } - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to get OAuth2 access token'); - } - - return accessToken; -}; - -export { processOAuthTokenRes, getOAuthAccessToken }; diff --git a/backend/src/helpers/membership.ts b/backend/src/helpers/membership.ts index 14cd567bb..b06460cde 100644 --- a/backend/src/helpers/membership.ts +++ b/backend/src/helpers/membership.ts @@ -1,6 +1,52 @@ import * as Sentry from '@sentry/node'; import { Membership, Key } from '../models'; +/** + * Validate that user with id [userId] is a member of workspace with id [workspaceId] + * and has at least one of the roles in [acceptedRoles] and statuses in [acceptedStatuses] + * @param {Object} obj + * @param {String} obj.userId - id of user to validate + * @param {String} obj.workspaceId - id of workspace + */ +const validateMembership = async ({ + userId, + workspaceId, + acceptedRoles, + acceptedStatuses +}: { + userId: string; + workspaceId: string; + acceptedRoles: string[]; + acceptedStatuses: string[]; +}) => { + + let membership; + //TODO: Refactor code to take advantage of using RequestError. It's possible to create new types of errors for more detailed errors + try { + membership = await Membership.findOne({ + user: userId, + workspace: workspaceId + }); + + if (!membership) throw new Error('Failed to find membership'); + + if (!acceptedRoles.includes(membership.role)) { + throw new Error('Failed to validate membership role'); + } + + if (!acceptedStatuses.includes(membership.status)) { + throw new Error('Failed to validate membership status'); + } + + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to validate membership'); + } + + return membership; +} + /** * Return membership matching criteria specified in query [queryObj] * @param {Object} queryObj - query object @@ -97,4 +143,9 @@ const deleteMembership = async ({ membershipId }: { membershipId: string }) => { return deletedMembership; }; -export { addMemberships, findMembership, deleteMembership }; +export { + validateMembership, + addMemberships, + findMembership, + deleteMembership +}; diff --git a/backend/src/helpers/nodemailer.ts b/backend/src/helpers/nodemailer.ts index 7e70f6ac1..958342aae 100644 --- a/backend/src/helpers/nodemailer.ts +++ b/backend/src/helpers/nodemailer.ts @@ -2,21 +2,10 @@ import fs from 'fs'; import path from 'path'; import handlebars from 'handlebars'; import nodemailer from 'nodemailer'; -import { SMTP_HOST, SMTP_NAME, SMTP_USERNAME, SMTP_PASSWORD } from '../config'; +import { SMTP_FROM_NAME, SMTP_FROM_ADDRESS } from '../config'; +import * as Sentry from '@sentry/node'; -// create nodemailer transporter -const transporter = nodemailer.createTransport({ - host: SMTP_HOST, - port: 587, - auth: { - user: SMTP_USERNAME, - pass: SMTP_PASSWORD - } -}); -transporter - .verify() - .then(() => console.log('SMTP - Successfully connected')) - .catch((err) => console.log('SMTP - Failed to connect')); +let smtpTransporter: nodemailer.Transporter; /** * @param {Object} obj @@ -26,33 +15,38 @@ transporter * @param {Object} obj.substitutions - object containing template substitutions */ const sendMail = async ({ - template, - subjectLine, - recipients, - substitutions + template, + subjectLine, + recipients, + substitutions }: { - template: string; - subjectLine: string; - recipients: string[]; - substitutions: any; + template: string; + subjectLine: string; + recipients: string[]; + substitutions: any; }) => { - try { - const html = fs.readFileSync( - path.resolve(__dirname, '../templates/' + template), - 'utf8' - ); - const temp = handlebars.compile(html); - const htmlToSend = temp(substitutions); + try { + const html = fs.readFileSync( + path.resolve(__dirname, '../templates/' + template), + 'utf8' + ); + const temp = handlebars.compile(html); + const htmlToSend = temp(substitutions); - await transporter.sendMail({ - from: `"${SMTP_NAME}" <${SMTP_USERNAME}>`, - to: recipients.join(', '), - subject: subjectLine, - html: htmlToSend - }); - } catch (err) { - console.error(err); - } + await smtpTransporter.sendMail({ + from: `"${SMTP_FROM_NAME}" <${SMTP_FROM_ADDRESS}>`, + to: recipients.join(', '), + subject: subjectLine, + html: htmlToSend + }); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + } }; -export { sendMail }; +const setTransporter = (transporter: nodemailer.Transporter) => { + smtpTransporter = transporter; +}; + +export { sendMail, setTransporter }; diff --git a/backend/src/helpers/rateLimiter.ts b/backend/src/helpers/rateLimiter.ts index a0788246a..6153369e3 100644 --- a/backend/src/helpers/rateLimiter.ts +++ b/backend/src/helpers/rateLimiter.ts @@ -2,34 +2,35 @@ import rateLimit from 'express-rate-limit'; // 300 requests per 15 minutes const apiLimiter = rateLimit({ - windowMs: 15 * 60 * 1000, - max: 400, - standardHeaders: true, - legacyHeaders: false + windowMs: 15 * 60 * 1000, + max: 400, + standardHeaders: true, + legacyHeaders: false, + skip: (request) => request.path === '/healthcheck' }); // 5 requests per hour const signupLimiter = rateLimit({ - windowMs: 60 * 60 * 1000, - max: 10, - standardHeaders: true, - legacyHeaders: false + windowMs: 60 * 60 * 1000, + max: 10, + standardHeaders: true, + legacyHeaders: false }); // 10 requests per hour const loginLimiter = rateLimit({ - windowMs: 60 * 60 * 1000, - max: 20, - standardHeaders: true, - legacyHeaders: false + windowMs: 60 * 60 * 1000, + max: 20, + standardHeaders: true, + legacyHeaders: false }); // 5 requests per hour const passwordLimiter = rateLimit({ - windowMs: 60 * 60 * 1000, - max: 10, - standardHeaders: true, - legacyHeaders: false + windowMs: 60 * 60 * 1000, + max: 10, + standardHeaders: true, + legacyHeaders: false }); export { apiLimiter, signupLimiter, loginLimiter, passwordLimiter }; diff --git a/backend/src/helpers/signup.ts b/backend/src/helpers/signup.ts index e26b9b360..8a201cb11 100644 --- a/backend/src/helpers/signup.ts +++ b/backend/src/helpers/signup.ts @@ -33,7 +33,7 @@ const sendEmailVerification = async ({ email }: { email: string }) => { // send mail await sendMail({ template: 'emailVerification.handlebars', - subjectLine: 'Infisical workspace invitation', + subjectLine: 'Infisical confirmation code', recipients: [email], substitutions: { code: token @@ -66,7 +66,7 @@ const checkEmailVerification = async ({ email, token: code }); - + if (!token) throw new Error('Failed to find email verification token'); } catch (err) { Sentry.setUser(null); diff --git a/backend/src/helpers/workspace.ts b/backend/src/helpers/workspace.ts index 52d7d227b..b43252bf3 100644 --- a/backend/src/helpers/workspace.ts +++ b/backend/src/helpers/workspace.ts @@ -1,13 +1,16 @@ import * as Sentry from '@sentry/node'; import { Workspace, + Bot, Membership, Key, Secret } from '../models'; +import { createBot } from '../helpers/bot'; /** * Create a workspace with name [name] in organization with id [organizationId] + * and a bot for it. * @param {String} name - name of workspace to create. * @param {String} organizationId - id of organization to create workspace in * @param {Object} workspace - new workspace @@ -21,10 +24,16 @@ const createWorkspace = async ({ }) => { let workspace; try { + // create workspace workspace = await new Workspace({ name, organization: organizationId }).save(); + + const bot = await createBot({ + name: 'Infisical Bot', + workspaceId: workspace._id.toString() + }); } catch (err) { Sentry.setUser(null); Sentry.captureException(err); @@ -43,6 +52,9 @@ const createWorkspace = async ({ const deleteWorkspace = async ({ id }: { id: string }) => { try { await Workspace.deleteOne({ _id: id }); + await Bot.deleteOne({ + workspace: id + }); await Membership.deleteMany({ workspace: id }); diff --git a/backend/src/index.ts b/backend/src/index.ts index a5ae44969..d182c2655 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -1,91 +1,25 @@ -import express from 'express'; -import helmet from 'helmet'; -import cors from 'cors'; -import cookieParser from 'cookie-parser'; -import mongoose from 'mongoose'; import dotenv from 'dotenv'; - dotenv.config(); + import * as Sentry from '@sentry/node'; -import { PORT, SENTRY_DSN, NODE_ENV, MONGO_URL, SITE_URL, POSTHOG_PROJECT_API_KEY, POSTHOG_HOST, TELEMETRY_ENABLED } from './config'; -import { apiLimiter } from './helpers/rateLimiter'; +import { SENTRY_DSN, NODE_ENV, MONGO_URL } from './config'; +import { server } from './app'; +import { initDatabase } from './services/database'; +import { setUpHealthEndpoint } from './services/health'; +import { initSmtp } from './services/smtp'; +import { setTransporter } from './helpers/nodemailer'; -const app = express(); +initDatabase(MONGO_URL); -Sentry.init({ - dsn: SENTRY_DSN, - tracesSampleRate: 1.0, - debug: NODE_ENV === 'production' ? false : true, - environment: NODE_ENV -}); +setUpHealthEndpoint(server); -import { - signup as signupRouter, - auth as authRouter, - organization as organizationRouter, - workspace as workspaceRouter, - membershipOrg as membershipOrgRouter, - membership as membershipRouter, - key as keyRouter, - inviteOrg as inviteOrgRouter, - user as userRouter, - userAction as userActionRouter, - secret as secretRouter, - serviceToken as serviceTokenRouter, - password as passwordRouter, - stripe as stripeRouter, - integration as integrationRouter, - integrationAuth as integrationAuthRouter -} from './routes'; +setTransporter(initSmtp()); -const connectWithRetry = () => { - mongoose.connect(MONGO_URL) - .then(() => console.log('Successfully connected to DB')) - .catch((e) => { - console.log('Failed to connect to DB ', e); - setTimeout(() => { - console.log(e); - }, 5000); - }); +if (NODE_ENV !== 'test') { + Sentry.init({ + dsn: SENTRY_DSN, + tracesSampleRate: 1.0, + debug: NODE_ENV === 'production' ? false : true, + environment: NODE_ENV + }); } - -connectWithRetry(); - -app.enable('trust proxy'); -app.use(cookieParser()); -app.use(cors({ - credentials: true, - origin: SITE_URL -})); - -if (NODE_ENV === 'production') { - // enable app-wide rate-limiting + helmet security - // in production - app.disable('x-powered-by'); - app.use(apiLimiter); - app.use(helmet()); -} - -app.use(express.json()); - -// routers -app.use('/api/v1/signup', signupRouter); -app.use('/api/v1/auth', authRouter); -app.use('/api/v1/user', userRouter); -app.use('/api/v1/user-action', userActionRouter); -app.use('/api/v1/organization', organizationRouter); -app.use('/api/v1/workspace', workspaceRouter); -app.use('/api/v1/membership-org', membershipOrgRouter); -app.use('/api/v1/membership', membershipRouter); -app.use('/api/v1/key', keyRouter); -app.use('/api/v1/invite-org', inviteOrgRouter); -app.use('/api/v1/secret', secretRouter); -app.use('/api/v1/service-token', serviceTokenRouter); -app.use('/api/v1/password', passwordRouter); -app.use('/api/v1/stripe', stripeRouter); -app.use('/api/v1/integration', integrationRouter); -app.use('/api/v1/integration-auth', integrationAuthRouter); - -app.listen(PORT, () => { - console.log('Listening on PORT ' + PORT); -}); diff --git a/backend/src/integrations/apps.ts b/backend/src/integrations/apps.ts new file mode 100644 index 000000000..e3b78c481 --- /dev/null +++ b/backend/src/integrations/apps.ts @@ -0,0 +1,213 @@ +import axios from 'axios'; +import * as Sentry from '@sentry/node'; +import { Octokit } from '@octokit/rest'; +import { IIntegrationAuth } from '../models'; +import { + INTEGRATION_HEROKU, + INTEGRATION_VERCEL, + INTEGRATION_NETLIFY, + INTEGRATION_GITHUB, + INTEGRATION_HEROKU_API_URL, + INTEGRATION_VERCEL_API_URL, + INTEGRATION_NETLIFY_API_URL, + INTEGRATION_GITHUB_API_URL +} from '../variables'; + +interface GitHubApp { + name: string; +} + +/** + * Return list of names of apps for integration named [integration] + * @param {Object} obj + * @param {String} obj.integration - name of integration + * @param {String} obj.accessToken - access token for integration + * @returns {Object[]} apps - names of integration apps + * @returns {String} apps.name - name of integration app + */ +const getApps = async ({ + integrationAuth, + accessToken +}: { + integrationAuth: IIntegrationAuth; + accessToken: string; +}) => { + interface App { + name: string; + siteId?: string; + } + + let apps: App[]; // TODO: add type and define payloads for apps + try { + switch (integrationAuth.integration) { + case INTEGRATION_HEROKU: + apps = await getAppsHeroku({ + accessToken + }); + break; + case INTEGRATION_VERCEL: + apps = await getAppsVercel({ + accessToken + }); + break; + case INTEGRATION_NETLIFY: + apps = await getAppsNetlify({ + integrationAuth, + accessToken + }); + break; + case INTEGRATION_GITHUB: + apps = await getAppsGithub({ + integrationAuth, + accessToken + }); + break; + } + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to get integration apps'); + } + + return apps; +}; + +/** + * Return list of names of apps for Heroku integration + * @param {Object} obj + * @param {String} obj.accessToken - access token for Heroku API + * @returns {Object[]} apps - names of Heroku apps + * @returns {String} apps.name - name of Heroku app + */ +const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => { + let apps; + try { + const res = ( + await axios.get(`${INTEGRATION_HEROKU_API_URL}/apps`, { + headers: { + Accept: 'application/vnd.heroku+json; version=3', + Authorization: `Bearer ${accessToken}` + } + }) + ).data; + + apps = res.map((a: any) => ({ + name: a.name + })); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to get Heroku integration apps'); + } + + return apps; +}; + +/** + * Return list of names of apps for Vercel integration + * @param {Object} obj + * @param {String} obj.accessToken - access token for Vercel API + * @returns {Object[]} apps - names of Vercel apps + * @returns {String} apps.name - name of Vercel app + */ +const getAppsVercel = async ({ accessToken }: { accessToken: string }) => { + let apps; + try { + const res = ( + await axios.get(`${INTEGRATION_VERCEL_API_URL}/v9/projects`, { + headers: { + Authorization: `Bearer ${accessToken}` + } + }) + ).data; + + apps = res.projects.map((a: any) => ({ + name: a.name + })); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to get Vercel integration apps'); + } + + return apps; +}; + +/** + * Return list of names of sites for Netlify integration + * @param {Object} obj + * @param {String} obj.accessToken - access token for Netlify API + * @returns {Object[]} apps - names of Netlify sites + * @returns {String} apps.name - name of Netlify site + */ +const getAppsNetlify = async ({ + integrationAuth, + accessToken +}: { + integrationAuth: IIntegrationAuth; + accessToken: string; +}) => { + let apps; + try { + const res = ( + await axios.get(`${INTEGRATION_NETLIFY_API_URL}/api/v1/sites`, { + headers: { + Authorization: `Bearer ${accessToken}` + } + }) + ).data; + + apps = res.map((a: any) => ({ + name: a.name, + siteId: a.site_id + })); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to get Netlify integration apps'); + } + + return apps; +}; + +/** + * Return list of names of repositories for Github integration + * @param {Object} obj + * @param {String} obj.accessToken - access token for Netlify API + * @returns {Object[]} apps - names of Netlify sites + * @returns {String} apps.name - name of Netlify site + */ +const getAppsGithub = async ({ + integrationAuth, + accessToken +}: { + integrationAuth: IIntegrationAuth; + accessToken: string; +}) => { + let apps; + try { + const octokit = new Octokit({ + auth: accessToken + }); + + const repos = (await octokit.request( + 'GET /user/repos{?visibility,affiliation,type,sort,direction,per_page,page,since,before}', + {} + )).data; + + apps = repos + .filter((a:any) => a.permissions.admin === true) + .map((a: any) => ({ + name: a.name + }) + ); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to get Github repos'); + } + + return apps; +}; + +export { getApps }; diff --git a/backend/src/integrations/exchange.ts b/backend/src/integrations/exchange.ts new file mode 100644 index 000000000..cb0ff84e0 --- /dev/null +++ b/backend/src/integrations/exchange.ts @@ -0,0 +1,286 @@ +import axios from 'axios'; +import * as Sentry from '@sentry/node'; +import { + INTEGRATION_HEROKU, + INTEGRATION_VERCEL, + INTEGRATION_NETLIFY, + INTEGRATION_GITHUB, + INTEGRATION_HEROKU_TOKEN_URL, + INTEGRATION_VERCEL_TOKEN_URL, + INTEGRATION_NETLIFY_TOKEN_URL, + INTEGRATION_GITHUB_TOKEN_URL, + INTEGRATION_GITHUB_API_URL +} from '../variables'; +import { + SITE_URL, + CLIENT_ID_VERCEL, + CLIENT_ID_NETLIFY, + CLIENT_ID_GITHUB, + CLIENT_SECRET_HEROKU, + CLIENT_SECRET_VERCEL, + CLIENT_SECRET_NETLIFY, + CLIENT_SECRET_GITHUB +} from '../config'; +import { user } from '../routes'; + +interface ExchangeCodeHerokuResponse { + token_type: string; + access_token: string; + expires_in: number; + refresh_token: string; + user_id: string; + session_nonce?: string; +} + +interface ExchangeCodeVercelResponse { + token_type: string; + access_token: string; + installation_id: string; + user_id: string; + team_id?: string; +} + +interface ExchangeCodeNetlifyResponse { + access_token: string; + token_type: string; + refresh_token: string; + scope: string; + created_at: number; +} + +interface ExchangeCodeGithubResponse { + access_token: string; + scope: string; + token_type: string; +} + +/** + * Return [accessToken], [accessExpiresAt], and [refreshToken] for OAuth2 + * code-token exchange for integration named [integration] + * @param {Object} obj1 + * @param {String} obj1.integration - name of integration + * @param {String} obj1.code - code for code-token exchange + * @returns {Object} obj + * @returns {String} obj.accessToken - access token for integration + * @returns {String} obj.refreshToken - refresh token for integration + * @returns {Date} obj.accessExpiresAt - date of expiration for access token + * @returns {String} obj.action - integration action for bot sequence + */ +const exchangeCode = async ({ + integration, + code +}: { + integration: string; + code: string; +}) => { + let obj = {} as any; + + try { + switch (integration) { + case INTEGRATION_HEROKU: + obj = await exchangeCodeHeroku({ + code + }); + break; + case INTEGRATION_VERCEL: + obj = await exchangeCodeVercel({ + code + }); + break; + case INTEGRATION_NETLIFY: + obj = await exchangeCodeNetlify({ + code + }); + break; + case INTEGRATION_GITHUB: + obj = await exchangeCodeGithub({ + code + }); + break; + } + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed OAuth2 code-token exchange'); + } + + return obj; +}; + +/** + * Return [accessToken], [accessExpiresAt], and [refreshToken] for Heroku + * OAuth2 code-token exchange + * @param {Object} obj1 + * @param {Object} obj1.code - code for code-token exchange + * @returns {Object} obj2 + * @returns {String} obj2.accessToken - access token for Heroku API + * @returns {String} obj2.refreshToken - refresh token for Heroku API + * @returns {Date} obj2.accessExpiresAt - date of expiration for access token + */ +const exchangeCodeHeroku = async ({ + code +}: { + code: string; +}) => { + let res: ExchangeCodeHerokuResponse; + const accessExpiresAt = new Date(); + try { + res = (await axios.post( + INTEGRATION_HEROKU_TOKEN_URL, + new URLSearchParams({ + grant_type: 'authorization_code', + code: code, + client_secret: CLIENT_SECRET_HEROKU + } as any) + )).data; + + accessExpiresAt.setSeconds( + accessExpiresAt.getSeconds() + res.expires_in + ); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed OAuth2 code-token exchange with Heroku'); + } + + return ({ + accessToken: res.access_token, + refreshToken: res.refresh_token, + accessExpiresAt + }); +} + +/** + * Return [accessToken], [accessExpiresAt], and [refreshToken] for Vercel + * code-token exchange + * @param {Object} obj1 + * @param {Object} obj1.code - code for code-token exchange + * @returns {Object} obj2 + * @returns {String} obj2.accessToken - access token for Heroku API + * @returns {String} obj2.refreshToken - refresh token for Heroku API + * @returns {Date} obj2.accessExpiresAt - date of expiration for access token + */ +const exchangeCodeVercel = async ({ code }: { code: string }) => { + let res: ExchangeCodeVercelResponse; + try { + res = ( + await axios.post( + INTEGRATION_VERCEL_TOKEN_URL, + new URLSearchParams({ + code: code, + client_id: CLIENT_ID_VERCEL, + client_secret: CLIENT_SECRET_VERCEL, + redirect_uri: `${SITE_URL}/vercel` + } as any) + ) + ).data; + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed OAuth2 code-token exchange with Vercel'); + } + + return { + accessToken: res.access_token, + refreshToken: null, + accessExpiresAt: null, + teamId: res.team_id + }; +}; + +/** + * Return [accessToken], [accessExpiresAt], and [refreshToken] for Vercel + * code-token exchange + * @param {Object} obj1 + * @param {Object} obj1.code - code for code-token exchange + * @returns {Object} obj2 + * @returns {String} obj2.accessToken - access token for Heroku API + * @returns {String} obj2.refreshToken - refresh token for Heroku API + * @returns {Date} obj2.accessExpiresAt - date of expiration for access token + */ +const exchangeCodeNetlify = async ({ code }: { code: string }) => { + let res: ExchangeCodeNetlifyResponse; + let accountId; + try { + res = ( + await axios.post( + INTEGRATION_NETLIFY_TOKEN_URL, + new URLSearchParams({ + grant_type: 'authorization_code', + code: code, + client_id: CLIENT_ID_NETLIFY, + client_secret: CLIENT_SECRET_NETLIFY, + redirect_uri: `${SITE_URL}/netlify` + } as any) + ) + ).data; + + const res2 = await axios.get('https://api.netlify.com/api/v1/sites', { + headers: { + Authorization: `Bearer ${res.access_token}` + } + }); + + const res3 = ( + await axios.get('https://api.netlify.com/api/v1/accounts', { + headers: { + Authorization: `Bearer ${res.access_token}` + } + }) + ).data; + + accountId = res3[0].id; + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed OAuth2 code-token exchange with Netlify'); + } + + return { + accessToken: res.access_token, + refreshToken: res.refresh_token, + accountId + }; +}; + +/** + * Return [accessToken], [accessExpiresAt], and [refreshToken] for Github + * code-token exchange + * @param {Object} obj1 + * @param {Object} obj1.code - code for code-token exchange + * @returns {Object} obj2 + * @returns {String} obj2.accessToken - access token for Github API + * @returns {String} obj2.refreshToken - refresh token for Github API + * @returns {Date} obj2.accessExpiresAt - date of expiration for access token + */ +const exchangeCodeGithub = async ({ code }: { code: string }) => { + let res: ExchangeCodeGithubResponse; + try { + res = ( + await axios.get(INTEGRATION_GITHUB_TOKEN_URL, { + params: { + client_id: CLIENT_ID_GITHUB, + client_secret: CLIENT_SECRET_GITHUB, + code: code, + redirect_uri: `${SITE_URL}/github` + }, + headers: { + Accept: 'application/json' + } + }) + ).data; + + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed OAuth2 code-token exchange with Github'); + } + + return { + accessToken: res.access_token, + refreshToken: null, + accessExpiresAt: null + }; +}; + +export { exchangeCode }; diff --git a/backend/src/integrations/index.ts b/backend/src/integrations/index.ts new file mode 100644 index 000000000..86c22de0c --- /dev/null +++ b/backend/src/integrations/index.ts @@ -0,0 +1,13 @@ +import { exchangeCode } from './exchange'; +import { exchangeRefresh } from './refresh'; +import { getApps } from './apps'; +import { syncSecrets } from './sync'; +import { revokeAccess } from './revoke'; + +export { + exchangeCode, + exchangeRefresh, + getApps, + syncSecrets, + revokeAccess +} \ No newline at end of file diff --git a/backend/src/integrations/refresh.ts b/backend/src/integrations/refresh.ts new file mode 100644 index 000000000..ea232f1e5 --- /dev/null +++ b/backend/src/integrations/refresh.ts @@ -0,0 +1,77 @@ +import axios from 'axios'; +import * as Sentry from '@sentry/node'; +import { INTEGRATION_HEROKU } from '../variables'; +import { + CLIENT_SECRET_HEROKU +} from '../config'; +import { + INTEGRATION_HEROKU_TOKEN_URL +} from '../variables'; + +/** + * Return new access token by exchanging refresh token [refreshToken] for integration + * named [integration] + * @param {Object} obj + * @param {String} obj.integration - name of integration + * @param {String} obj.refreshToken - refresh token to use to get new access token for Heroku + */ +const exchangeRefresh = async ({ + integration, + refreshToken +}: { + integration: string; + refreshToken: string; +}) => { + let accessToken; + try { + switch (integration) { + case INTEGRATION_HEROKU: + accessToken = await exchangeRefreshHeroku({ + refreshToken + }); + break; + } + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to get new OAuth2 access token'); + } + + return accessToken; +}; + +/** + * Return new access token by exchanging refresh token [refreshToken] for the + * Heroku integration + * @param {Object} obj + * @param {String} obj.refreshToken - refresh token to use to get new access token for Heroku + * @returns + */ +const exchangeRefreshHeroku = async ({ + refreshToken +}: { + refreshToken: string; +}) => { + let accessToken; + //TODO: Refactor code to take advantage of using RequestError. It's possible to create new types of errors for more detailed errors + try { + const res = await axios.post( + INTEGRATION_HEROKU_TOKEN_URL, + new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: refreshToken, + client_secret: CLIENT_SECRET_HEROKU + } as any) + ); + + accessToken = res.data.access_token; + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to get new OAuth2 access token for Heroku'); + } + + return accessToken; +}; + +export { exchangeRefresh }; diff --git a/backend/src/integrations/revoke.ts b/backend/src/integrations/revoke.ts new file mode 100644 index 000000000..483486343 --- /dev/null +++ b/backend/src/integrations/revoke.ts @@ -0,0 +1,47 @@ +import axios from 'axios'; +import * as Sentry from '@sentry/node'; +import { IIntegrationAuth, IntegrationAuth, Integration } from '../models'; +import { + INTEGRATION_HEROKU, + INTEGRATION_VERCEL, + INTEGRATION_NETLIFY, + INTEGRATION_GITHUB +} from '../variables'; + +const revokeAccess = async ({ + integrationAuth, + accessToken +}: { + integrationAuth: IIntegrationAuth; + accessToken: string; +}) => { + try { + // add any integration-specific revocation logic + switch (integrationAuth.integration) { + case INTEGRATION_HEROKU: + break; + case INTEGRATION_VERCEL: + break; + case INTEGRATION_NETLIFY: + break; + case INTEGRATION_GITHUB: + break; + } + + const deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({ + _id: integrationAuth._id + }); + + if (deletedIntegrationAuth) { + await Integration.deleteMany({ + integrationAuth: deletedIntegrationAuth._id + }); + } + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to delete integration authorization'); + } +}; + +export { revokeAccess }; diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts new file mode 100644 index 000000000..30628fb9a --- /dev/null +++ b/backend/src/integrations/sync.ts @@ -0,0 +1,605 @@ +import axios from 'axios'; +import * as Sentry from '@sentry/node'; +import { Octokit } from '@octokit/rest'; +// import * as sodium from 'libsodium-wrappers'; +import sodium from 'libsodium-wrappers'; +// const sodium = require('libsodium-wrappers'); +import { IIntegration, IIntegrationAuth } from '../models'; +import { + INTEGRATION_HEROKU, + INTEGRATION_VERCEL, + INTEGRATION_NETLIFY, + INTEGRATION_GITHUB, + INTEGRATION_HEROKU_API_URL, + INTEGRATION_VERCEL_API_URL, + INTEGRATION_NETLIFY_API_URL, + INTEGRATION_GITHUB_API_URL +} from '../variables'; +import { access, appendFile } from 'fs'; + +// TODO: need a helper function in the future to handle integration +// envar priorities (i.e. prioritize secrets within integration or those on Infisical) + +/** + * Sync/push [secrets] to [app] in integration named [integration] + * @param {Object} obj + * @param {IIntegration} obj.integration - integration details + * @param {IIntegrationAuth} obj.integrationAuth - integration auth details + * @param {Object} obj.app - app in integration + * @param {Object} obj.target - (optional) target (environment) in integration + * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) + * @param {String} obj.accessToken - access token for integration + */ +const syncSecrets = async ({ + integration, + integrationAuth, + secrets, + accessToken +}: { + integration: IIntegration; + integrationAuth: IIntegrationAuth; + secrets: any; + accessToken: string; +}) => { + try { + switch (integration.integration) { + case INTEGRATION_HEROKU: + await syncSecretsHeroku({ + integration, + secrets, + accessToken + }); + break; + case INTEGRATION_VERCEL: + await syncSecretsVercel({ + integration, + secrets, + accessToken + }); + break; + case INTEGRATION_NETLIFY: + await syncSecretsNetlify({ + integration, + integrationAuth, + secrets, + accessToken + }); + break; + case INTEGRATION_GITHUB: + await syncSecretsGitHub({ + integration, + secrets, + accessToken + }); + break; + } + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to sync secrets to integration'); + } +}; + +/** + * Sync/push [secrets] to Heroku [app] + * @param {Object} obj + * @param {IIntegration} obj.integration - integration details + * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) + */ +const syncSecretsHeroku = async ({ + integration, + secrets, + accessToken +}: { + integration: IIntegration; + secrets: any; + accessToken: string; +}) => { + try { + const herokuSecrets = ( + await axios.get( + `${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`, + { + headers: { + Accept: 'application/vnd.heroku+json; version=3', + Authorization: `Bearer ${accessToken}` + } + } + ) + ).data; + + Object.keys(herokuSecrets).forEach((key) => { + if (!(key in secrets)) { + secrets[key] = null; + } + }); + + await axios.patch( + `${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`, + secrets, + { + headers: { + Accept: 'application/vnd.heroku+json; version=3', + Authorization: `Bearer ${accessToken}` + } + } + ); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to sync secrets to Heroku'); + } +}; + +/** + * Sync/push [secrets] to Heroku [app] + * @param {Object} obj + * @param {IIntegration} obj.integration - integration details + * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) + */ +const syncSecretsVercel = async ({ + integration, + secrets, + accessToken +}: { + integration: IIntegration, + secrets: any; + accessToken: string; +}) => { + + interface VercelSecret { + id?: string; + type: string; + key: string; + value: string; + target: string[]; + } + + try { + // Get all (decrypted) secrets back from Vercel in + // decrypted format + const params = new URLSearchParams({ + decrypt: "true" + }); + + const res = (await Promise.all((await axios.get( + `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`, + { + params, + headers: { + Authorization: `Bearer ${accessToken}` + } + } + )) + .data + .envs + .filter((secret: VercelSecret) => secret.target.includes(integration.target)) + .map(async (secret: VercelSecret) => (await axios.get( + `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, + { + headers: { + Authorization: `Bearer ${accessToken}` + } + + } + )).data) + )).reduce((obj: any, secret: any) => ({ + ...obj, + [secret.key]: secret + }), {}); + + const updateSecrets: VercelSecret[] = []; + const deleteSecrets: VercelSecret[] = []; + const newSecrets: VercelSecret[] = []; + + // Identify secrets to create + Object.keys(secrets).map((key) => { + if (!(key in res)) { + // case: secret has been created + newSecrets.push({ + key: key, + value: secrets[key], + type: 'encrypted', + target: [integration.target] + }); + } + }); + + // Identify secrets to update and delete + Object.keys(res).map((key) => { + if (key in secrets) { + if (res[key].value !== secrets[key]) { + // case: secret value has changed + updateSecrets.push({ + id: res[key].id, + key: key, + value: secrets[key], + type: 'encrypted', + target: [integration.target] + }); + } + } else { + // case: secret has been deleted + deleteSecrets.push({ + id: res[key].id, + key: key, + value: res[key].value, + type: 'encrypted', + target: [integration.target], + }); + } + }); + + // Sync/push new secrets + if (newSecrets.length > 0) { + await axios.post( + `${INTEGRATION_VERCEL_API_URL}/v10/projects/${integration.app}/env`, + newSecrets, + { + headers: { + Authorization: `Bearer ${accessToken}` + } + } + ); + } + + // Sync/push updated secrets + if (updateSecrets.length > 0) { + updateSecrets.forEach(async (secret: VercelSecret) => { + const { + id, + ...updatedSecret + } = secret; + await axios.patch( + `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, + updatedSecret, + { + headers: { + Authorization: `Bearer ${accessToken}` + } + } + ); + }); + } + + // Delete secrets + if (deleteSecrets.length > 0) { + deleteSecrets.forEach(async (secret: VercelSecret) => { + await axios.delete( + `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, + { + headers: { + Authorization: `Bearer ${accessToken}` + } + } + ); + }); + } + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to sync secrets to Vercel'); + } +} + +/** + * Sync/push [secrets] to Netlify site [app] + * @param {Object} obj + * @param {IIntegration} obj.integration - integration details + * @param {IIntegrationAuth} obj.integrationAuth - integration auth details + * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) + */ +const syncSecretsNetlify = async ({ + integration, + integrationAuth, + secrets, + accessToken +}: { + integration: IIntegration; + integrationAuth: IIntegrationAuth; + secrets: any; + accessToken: string; +}) => { + try { + + interface NetlifyValue { + id?: string; + context: string; // 'dev' | 'branch-deploy' | 'deploy-preview' | 'production', + value: string; + } + + interface NetlifySecret { + key: string; + values: NetlifyValue[]; + } + + interface NetlifySecretsRes { + [index: string]: NetlifySecret; + } + + const getParams = new URLSearchParams({ + context_name: 'all', // integration.context or all + site_id: integration.siteId + }); + + const res = (await axios.get( + `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`, + { + params: getParams, + headers: { + Authorization: `Bearer ${accessToken}` + } + } + )) + .data + .reduce((obj: any, secret: any) => ({ + ...obj, + [secret.key]: secret + }), {}); + + const newSecrets: NetlifySecret[] = []; // createEnvVars + const deleteSecrets: string[] = []; // deleteEnvVar + const deleteSecretValues: NetlifySecret[] = []; // deleteEnvVarValue + const updateSecrets: NetlifySecret[] = []; // setEnvVarValue + + // identify secrets to create and update + Object.keys(secrets).map((key) => { + if (!(key in res)) { + // case: Infisical secret does not exist in Netlify -> create secret + newSecrets.push({ + key, + values: [{ + value: secrets[key], + context: integration.context + }] + }); + } else { + // case: Infisical secret exists in Netlify + const contexts = res[key].values + .reduce((obj: any, value: NetlifyValue) => ({ + ...obj, + [value.context]: value + }), {}); + + if (integration.context in contexts) { + // case: Netlify secret value exists in integration context + if (secrets[key] !== contexts[integration.context].value) { + // case: Infisical and Netlify secret values are different + // -> update Netlify secret context and value + updateSecrets.push({ + key, + values: [{ + context: integration.context, + value: secrets[key] + }] + }); + } + } else { + // case: Netlify secret value does not exist in integration context + // -> add the new Netlify secret context and value + updateSecrets.push({ + key, + values: [{ + context: integration.context, + value: secrets[key] + }] + }); + } + } + }) + + // identify secrets to delete + // TODO: revise (patch case where 1 context was deleted but others still there + Object.keys(res).map((key) => { + // loop through each key's context + if (!(key in secrets)) { + // case: Netlify secret does not exist in Infisical + + const numberOfValues = res[key].values.length; + + res[key].values.forEach((value: NetlifyValue) => { + if (value.context === integration.context) { + if (numberOfValues <= 1) { + // case: Netlify secret value has less than 1 context -> delete secret + deleteSecrets.push(key); + } else { + // case: Netlify secret value has more than 1 context -> delete secret value context + deleteSecretValues.push({ + key, + values: [{ + id: value.id, + context: integration.context, + value: value.value + }] + }); + } + } + }); + } + }); + + const syncParams = new URLSearchParams({ + site_id: integration.siteId + }); + + if (newSecrets.length > 0) { + await axios.post( + `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`, + newSecrets, + { + params: syncParams, + headers: { + Authorization: `Bearer ${accessToken}` + } + } + ); + } + + if (updateSecrets.length > 0) { + updateSecrets.forEach(async (secret: NetlifySecret) => { + await axios.patch( + `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`, + { + context: secret.values[0].context, + value: secret.values[0].value + }, + { + params: syncParams, + headers: { + Authorization: `Bearer ${accessToken}` + } + } + ); + }); + } + + if (deleteSecrets.length > 0) { + deleteSecrets.forEach(async (key: string) => { + await axios.delete( + `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${key}`, + { + params: syncParams, + headers: { + Authorization: `Bearer ${accessToken}` + } + } + ); + }); + } + + if (deleteSecretValues.length > 0) { + deleteSecretValues.forEach(async (secret: NetlifySecret) => { + await axios.delete( + `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}/value/${secret.values[0].id}`, + { + params: syncParams, + headers: { + Authorization: `Bearer ${accessToken}` + } + } + ); + }); + } + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to sync secrets to Heroku'); + } +} + +/** + * Sync/push [secrets] to GitHub [repo] + * @param {Object} obj + * @param {IIntegration} obj.integration - integration details + * @param {IIntegrationAuth} obj.integrationAuth - integration auth details + * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) + */ +const syncSecretsGitHub = async ({ + integration, + secrets, + accessToken +}: { + integration: IIntegration; + secrets: any; + accessToken: string; +}) => { + try { + + interface GitHubRepoKey { + key_id: string; + key: string; + } + + interface GitHubSecret { + name: string; + created_at: string; + updated_at: string; + } + + interface GitHubSecretRes { + [index: string]: GitHubSecret; + } + + const deleteSecrets: GitHubSecret[] = []; + + const octokit = new Octokit({ + auth: accessToken + }); + + const user = (await octokit.request('GET /user', {})).data; + + const repoPublicKey: GitHubRepoKey = (await octokit.request( + 'GET /repos/{owner}/{repo}/actions/secrets/public-key', + { + owner: user.login, + repo: integration.app + } + )).data; + + // // Get local copy of decrypted secrets. We cannot decrypt them as we dont have access to GH private key + const encryptedSecrets: GitHubSecretRes = (await octokit.request( + 'GET /repos/{owner}/{repo}/actions/secrets', + { + owner: user.login, + repo: integration.app + } + )) + .data + .secrets + .reduce((obj: any, secret: any) => ({ + ...obj, + [secret.name]: secret + }), {}); + + Object.keys(encryptedSecrets).map(async (key) => { + if (!(key in secrets)) { + await octokit.request( + 'DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}', + { + owner: user.login, + repo: integration.app, + secret_name: key + } + ); + } + }); + + Object.keys(secrets).map((key) => { + // let encryptedSecret; + sodium.ready.then(async () => { + // convert secret & base64 key to Uint8Array. + const binkey = sodium.from_base64( + repoPublicKey.key, + sodium.base64_variants.ORIGINAL + ); + const binsec = sodium.from_string(secrets[key]); + + // encrypt secret using libsodium + const encBytes = sodium.crypto_box_seal(binsec, binkey); + + // convert encrypted Uint8Array to base64 + const encryptedSecret = sodium.to_base64( + encBytes, + sodium.base64_variants.ORIGINAL + ); + + await octokit.request( + 'PUT /repos/{owner}/{repo}/actions/secrets/{secret_name}', + { + owner: user.login, + repo: integration.app, + secret_name: key, + encrypted_value: encryptedSecret, + key_id: repoPublicKey.key_id + } + ); + }); + }); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to sync secrets to GitHub'); + } +}; + +export { syncSecrets }; \ No newline at end of file diff --git a/backend/src/json/integrations.json b/backend/src/json/integrations.json deleted file mode 100644 index 16b09ebf4..000000000 --- a/backend/src/json/integrations.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "heroku": { - "name": "Heroku", - "type": "oauth2", - "clientId": "bc132901-935a-4590-b010-f1857efc380d", - "docsLink": "" - }, - "netlify": { - "name": "Netlify", - "type": "oauth2", - "clientId": "", - "docsLink": "" - }, - "digitalocean": { - "name": "Digital Ocean", - "type": "oauth2", - "clientId": "", - "docsLink": "" - }, - "gcp": { - "name": "Google Cloud Platform", - "type": "oauth2", - "clientId": "", - "docsLink": "" - }, - "aws": { - "name": "Amazon Web Services", - "type": "oauth2", - "clientId": "", - "docsLink": "" - }, - "azure": { - "name": "Microsoft Azure", - "type": "oauth2", - "clientId": "", - "docsLink": "" - }, - "travisci": { - "name": "Travis CI", - "type": "oauth2", - "clientId": "", - "docsLink": "" - }, - "circleci": { - "name": "Circle CI", - "type": "oauth2", - "clientId": "", - "docsLink": "" - } -} diff --git a/backend/src/middleware/index.ts b/backend/src/middleware/index.ts index e445b64cf..7fcba66e1 100644 --- a/backend/src/middleware/index.ts +++ b/backend/src/middleware/index.ts @@ -1,4 +1,5 @@ import requireAuth from './requireAuth'; +import requireBotAuth from './requireBotAuth'; import requireSignupAuth from './requireSignupAuth'; import requireWorkspaceAuth from './requireWorkspaceAuth'; import requireOrganizationAuth from './requireOrganizationAuth'; @@ -9,6 +10,7 @@ import validateRequest from './validateRequest'; export { requireAuth, + requireBotAuth, requireSignupAuth, requireWorkspaceAuth, requireOrganizationAuth, diff --git a/backend/src/middleware/requestErrorHandler.ts b/backend/src/middleware/requestErrorHandler.ts new file mode 100644 index 000000000..36f1dce49 --- /dev/null +++ b/backend/src/middleware/requestErrorHandler.ts @@ -0,0 +1,29 @@ +import { ErrorRequestHandler } from "express"; + +import * as Sentry from '@sentry/node'; +import { InternalServerError } from "../utils/errors"; +import { getLogger } from "../utils/logger"; +import RequestError, { LogLevel } from "../utils/requestError"; + + +export const requestErrorHandler: ErrorRequestHandler = (error: RequestError|Error, req, res, next) => { + if(res.headersSent) return next(); + //TODO: Find better way to type check for error. In current setting you need to cast type to get the functions and variables from RequestError + if(!(error instanceof RequestError)){ + error = InternalServerError({context: {exception: error.message}, stack: error.stack}) + getLogger('backend-main').log((error).levelName.toLowerCase(), (error).message) + } + + //* Set Sentry user identification if req.user is populated + if(req.user !== undefined && req.user !== null){ + Sentry.setUser({ email: req.user.email }) + } + //* Only sent error to Sentry if LogLevel is one of the following level 'ERROR', 'EMERGENCY' or 'CRITICAL' + //* with this we will eliminate false-positive errors like 'BadRequestError', 'UnauthorizedRequestError' and so on + if([LogLevel.ERROR, LogLevel.EMERGENCY, LogLevel.CRITICAL].includes((error).level)){ + Sentry.captureException(error) + } + + res.status((error).statusCode).json((error).format(req)) + next() +} \ No newline at end of file diff --git a/backend/src/middleware/requireAuth.ts b/backend/src/middleware/requireAuth.ts index a6bd79073..d917d362a 100644 --- a/backend/src/middleware/requireAuth.ts +++ b/backend/src/middleware/requireAuth.ts @@ -1,8 +1,8 @@ import jwt from 'jsonwebtoken'; import { Request, Response, NextFunction } from 'express'; -import * as Sentry from '@sentry/node'; import { User } from '../models'; import { JWT_AUTH_SECRET } from '../config'; +import { AccountNotFoundError, BadRequestError, UnauthorizedRequestError } from '../utils/errors'; declare module 'jsonwebtoken' { export interface UserIDJwtPayload extends jwt.JwtPayload { @@ -20,32 +20,25 @@ declare module 'jsonwebtoken' { */ const requireAuth = async (req: Request, res: Response, next: NextFunction) => { // JWT authentication middleware - try { - if (!req.headers?.authorization) - throw new Error('Failed to locate authorization header'); + const [ AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE ] = <[string, string]>req.headers['authorization']?.split(' ', 2) ?? [null, null] + if(AUTH_TOKEN_TYPE === null) return next(BadRequestError({message: `Missing Authorization Header in the request header.`})) + if(AUTH_TOKEN_TYPE.toLowerCase() !== 'bearer') return next(BadRequestError({message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.`})) + if(AUTH_TOKEN_VALUE === null) return next(BadRequestError({message: 'Missing Authorization Body in the request header'})) - const token = req.headers.authorization.split(' ')[1]; - const decodedToken = ( - jwt.verify(token, JWT_AUTH_SECRET) - ); + const decodedToken = ( + jwt.verify(AUTH_TOKEN_VALUE, JWT_AUTH_SECRET) + ); - const user = await User.findOne({ - _id: decodedToken.userId - }).select('+publicKey'); + const user = await User.findOne({ + _id: decodedToken.userId + }).select('+publicKey'); - if (!user) throw new Error('Failed to authenticate unfound user'); - if (!user?.publicKey) - throw new Error('Failed to authenticate not fully set up account'); + if (!user) return next(AccountNotFoundError({message: 'Failed to locate User account'})) + if (!user?.publicKey) + return next(UnauthorizedRequestError({message: 'Unable to authenticate due to partially set up account'})) - req.user = user; - return next(); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - return res.status(401).send({ - error: 'Failed to authenticate user. Try logging in' - }); - } + req.user = user; + return next(); }; export default requireAuth; diff --git a/backend/src/middleware/requireBotAuth.ts b/backend/src/middleware/requireBotAuth.ts new file mode 100644 index 000000000..e39f0d1b5 --- /dev/null +++ b/backend/src/middleware/requireBotAuth.ts @@ -0,0 +1,37 @@ +import { Request, Response, NextFunction } from 'express'; +import { Bot } from '../models'; +import { validateMembership } from '../helpers/membership'; +import { AccountNotFoundError } from '../utils/errors'; + +type req = 'params' | 'body' | 'query'; + +const requireBotAuth = ({ + acceptedRoles, + acceptedStatuses, + location = 'params' +}: { + acceptedRoles: string[]; + acceptedStatuses: string[]; + location?: req; +}) => { + return async (req: Request, res: Response, next: NextFunction) => { + const bot = await Bot.findOne({ _id: req[location].botId }); + + if (!bot) { + return next(AccountNotFoundError({message: 'Failed to locate Bot account'})) + } + + await validateMembership({ + userId: req.user._id.toString(), + workspaceId: bot.workspace.toString(), + acceptedRoles, + acceptedStatuses + }); + + req.bot = bot; + + next(); + } +} + +export default requireBotAuth; \ No newline at end of file diff --git a/backend/src/middleware/requireIntegrationAuth.ts b/backend/src/middleware/requireIntegrationAuth.ts index 70ca320c3..4389028ab 100644 --- a/backend/src/middleware/requireIntegrationAuth.ts +++ b/backend/src/middleware/requireIntegrationAuth.ts @@ -1,7 +1,8 @@ -import * as Sentry from '@sentry/node'; import { Request, Response, NextFunction } from 'express'; -import { Integration, IntegrationAuth, Membership } from '../models'; -import { getOAuthAccessToken } from '../helpers/integrationAuth'; +import { Integration, IntegrationAuth } from '../models'; +import { IntegrationService } from '../services'; +import { validateMembership } from '../helpers/membership'; +import { IntegrationNotFoundError, UnauthorizedRequestError } from '../utils/errors'; /** * Validate if user on request is a member of workspace with proper roles associated @@ -20,56 +21,40 @@ const requireIntegrationAuth = ({ return async (req: Request, res: Response, next: NextFunction) => { // integration authorization middleware - try { - const { integrationId } = req.params; + const { integrationId } = req.params; - // validate integration accessibility - const integration = await Integration.findOne({ - _id: integrationId - }); + // validate integration accessibility + const integration = await Integration.findOne({ + _id: integrationId + }); - if (!integration) { - throw new Error('Failed to find integration'); - } - - const membership = await Membership.findOne({ - user: req.user._id, - workspace: integration.workspace - }); - - if (!membership) { - throw new Error('Failed to find integration workspace membership'); - } - - if (!acceptedRoles.includes(membership.role)) { - throw new Error('Failed to validate workspace membership role'); - } - - if (!acceptedStatuses.includes(membership.status)) { - throw new Error('Failed to validate workspace membership status'); - } - - const integrationAuth = await IntegrationAuth.findOne({ - _id: integration.integrationAuth - }).select( - '+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt' - ); - - if (!integrationAuth) { - throw new Error('Failed to find integration authorization'); - } - - req.integration = integration; - req.accessToken = await getOAuthAccessToken({ integrationAuth }); - - return next(); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - return res.status(401).send({ - error: 'Failed integration authorization' - }); + if (!integration) { + return next(IntegrationNotFoundError({message: 'Failed to locate Integration'})) } + + await validateMembership({ + userId: req.user._id.toString(), + workspaceId: integration.workspace.toString(), + acceptedRoles, + acceptedStatuses + }); + + const integrationAuth = await IntegrationAuth.findOne({ + _id: integration.integrationAuth + }).select( + '+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt' + ); + + if (!integrationAuth) { + return next(UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'})) + } + + req.integration = integration; + req.accessToken = await IntegrationService.getIntegrationAuthAccess({ + integrationAuthId: integrationAuth._id.toString() + }); + + return next(); }; }; diff --git a/backend/src/middleware/requireIntegrationAuthorizationAuth.ts b/backend/src/middleware/requireIntegrationAuthorizationAuth.ts index 1f5c6dfc8..278716e60 100644 --- a/backend/src/middleware/requireIntegrationAuthorizationAuth.ts +++ b/backend/src/middleware/requireIntegrationAuthorizationAuth.ts @@ -1,8 +1,9 @@ import * as Sentry from '@sentry/node'; import { Request, Response, NextFunction } from 'express'; -import { IntegrationAuth, Membership } from '../models'; -import { decryptSymmetric } from '../utils/crypto'; -import { getOAuthAccessToken } from '../helpers/integrationAuth'; +import { IntegrationAuth } from '../models'; +import { IntegrationService } from '../services'; +import { validateMembership } from '../helpers/membership'; +import { UnauthorizedRequestError } from '../utils/errors'; /** * Validate if user on request is a member of workspace with proper roles associated @@ -10,62 +11,45 @@ import { getOAuthAccessToken } from '../helpers/integrationAuth'; * @param {Object} obj * @param {String[]} obj.acceptedRoles - accepted workspace roles * @param {String[]} obj.acceptedStatuses - accepted workspace statuses - * @param {Boolean} obj.attachRefresh - whether or not to decrypt and attach integration authorization refresh token onto request + * @param {Boolean} obj.attachAccessToken - whether or not to decrypt and attach integration authorization access token onto request */ const requireIntegrationAuthorizationAuth = ({ acceptedRoles, - acceptedStatuses + acceptedStatuses, + attachAccessToken = true }: { acceptedRoles: string[]; acceptedStatuses: string[]; + attachAccessToken?: boolean; }) => { return async (req: Request, res: Response, next: NextFunction) => { - // (authorization) integration authorization middleware + const { integrationAuthId } = req.params; - try { - const { integrationAuthId } = req.params; + const integrationAuth = await IntegrationAuth.findOne({ + _id: integrationAuthId + }).select( + '+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt' + ); - const integrationAuth = await IntegrationAuth.findOne({ - _id: integrationAuthId - }).select( - '+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt' - ); + if (!integrationAuth) { + return next(UnauthorizedRequestError({message: 'Failed to locate Integration Authorization credentials'})) + } + + await validateMembership({ + userId: req.user._id.toString(), + workspaceId: integrationAuth.workspace.toString(), + acceptedRoles, + acceptedStatuses + }); - if (!integrationAuth) { - throw new Error('Failed to find integration authorization'); - } - - const membership = await Membership.findOne({ - user: req.user._id, - workspace: integrationAuth.workspace - }); - - if (!membership) { - throw new Error( - 'Failed to find integration authorization workspace membership' - ); - } - - if (!acceptedRoles.includes(membership.role)) { - throw new Error('Failed to validate workspace membership role'); - } - - if (!acceptedStatuses.includes(membership.status)) { - throw new Error('Failed to validate workspace membership status'); - } - - req.integrationAuth = integrationAuth; - - // TODO: make compatible with other integration types since they won't necessarily have access tokens - req.accessToken = await getOAuthAccessToken({ integrationAuth }); - return next(); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - return res.status(401).send({ - error: 'Failed (authorization) integration authorizationt' + req.integrationAuth = integrationAuth; + if (attachAccessToken) { + req.accessToken = await IntegrationService.getIntegrationAuthAccess({ + integrationAuthId: integrationAuth._id.toString() }); } + + return next(); }; }; diff --git a/backend/src/middleware/requireOrganizationAuth.ts b/backend/src/middleware/requireOrganizationAuth.ts index c77bd312e..04542b429 100644 --- a/backend/src/middleware/requireOrganizationAuth.ts +++ b/backend/src/middleware/requireOrganizationAuth.ts @@ -1,6 +1,6 @@ -import * as Sentry from '@sentry/node'; import { Request, Response, NextFunction } from 'express'; import { IOrganization, MembershipOrg } from '../models'; +import { UnauthorizedRequestError, ValidationError } from '../utils/errors'; /** * Validate if user on request is a member with proper roles for organization @@ -19,35 +19,28 @@ const requireOrganizationAuth = ({ return async (req: Request, res: Response, next: NextFunction) => { // organization authorization middleware - try { - // validate organization membership - const membershipOrg = await MembershipOrg.findOne({ - user: req.user._id, - organization: req.params.organizationId - }).populate<{ organization: IOrganization }>('organization'); + // validate organization membership + const membershipOrg = await MembershipOrg.findOne({ + user: req.user._id, + organization: req.params.organizationId + }).populate<{ organization: IOrganization }>('organization'); - if (!membershipOrg) { - throw new Error('Failed to find organization membership'); - } - if (!acceptedRoles.includes(membershipOrg.role)) { - throw new Error('Failed to validate organization membership role'); - } - - if (!acceptedStatuses.includes(membershipOrg.status)) { - throw new Error('Failed to validate organization membership status'); - } - - req.membershipOrg = membershipOrg; - - return next(); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - return res.status(401).send({ - error: 'Failed organization authorization' - }); + if (!membershipOrg) { + return next(UnauthorizedRequestError({message: "You're not a member of this Organization."})) } + //TODO is this important to validate? I mean is it possible to save wrong role to database or get wrong role from databse? - Zamion101 + if (!acceptedRoles.includes(membershipOrg.role)) { + return next(ValidationError({message: 'Failed to validate Organization Membership Role'})) + } + + if (!acceptedStatuses.includes(membershipOrg.status)) { + return next(ValidationError({message: 'Failed to validate Organization Membership Status'})) + } + + req.membershipOrg = membershipOrg; + + return next(); }; }; diff --git a/backend/src/middleware/requireServiceTokenAuth.ts b/backend/src/middleware/requireServiceTokenAuth.ts index d403e8fe5..94e8363ff 100644 --- a/backend/src/middleware/requireServiceTokenAuth.ts +++ b/backend/src/middleware/requireServiceTokenAuth.ts @@ -1,8 +1,8 @@ import jwt from 'jsonwebtoken'; import { Request, Response, NextFunction } from 'express'; -import * as Sentry from '@sentry/node'; import { ServiceToken } from '../models'; import { JWT_SERVICE_SECRET } from '../config'; +import { BadRequestError, UnauthorizedRequestError } from '../utils/errors'; declare module 'jsonwebtoken' { export interface UserIDJwtPayload extends jwt.JwtPayload { @@ -24,33 +24,27 @@ const requireServiceTokenAuth = async ( next: NextFunction ) => { // JWT service token middleware - try { - if (!req.headers?.authorization) - throw new Error('Failed to locate authorization header'); + + const [ AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE ] = <[string, string]>req.headers['authorization']?.split(' ', 2) ?? [null, null] + if(AUTH_TOKEN_TYPE === null) return next(BadRequestError({message: `Missing Authorization Header in the request header.`})) + //TODO: Determine what is the actual Token Type for Service Token Authentication (ex. Bearer) + //if(AUTH_TOKEN_TYPE.toLowerCase() !== 'bearer') return next(UnauthorizedRequestError({message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.`})) + if(AUTH_TOKEN_VALUE === null) return next(BadRequestError({message: 'Missing Authorization Body in the request header'})) - const token = req.headers.authorization.split(' ')[1]; + const decodedToken = ( + jwt.verify(AUTH_TOKEN_VALUE, JWT_SERVICE_SECRET) + ); - const decodedToken = ( - jwt.verify(token, JWT_SERVICE_SECRET) - ); + const serviceToken = await ServiceToken.findOne({ + _id: decodedToken.serviceTokenId + }) + .populate('user', '+publicKey') + .select('+encryptedKey +publicKey +nonce'); - const serviceToken = await ServiceToken.findOne({ - _id: decodedToken.serviceTokenId - }) - .populate('user', '+publicKey') - .select('+encryptedKey +publicKey +nonce'); + if (!serviceToken) return next(UnauthorizedRequestError({message: 'The service token does not match the record in the database'})) - if (!serviceToken) throw new Error('Failed to find service token'); - - req.serviceToken = serviceToken; - return next(); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - return res.status(401).send({ - error: 'Failed to authenticate service token' - }); - } + req.serviceToken = serviceToken; + return next(); }; export default requireServiceTokenAuth; diff --git a/backend/src/middleware/requireSignupAuth.ts b/backend/src/middleware/requireSignupAuth.ts index 9384387ee..3318bd8d3 100644 --- a/backend/src/middleware/requireSignupAuth.ts +++ b/backend/src/middleware/requireSignupAuth.ts @@ -1,8 +1,8 @@ import jwt from 'jsonwebtoken'; import { Request, Response, NextFunction } from 'express'; -import * as Sentry from '@sentry/node'; import { User } from '../models'; import { JWT_SIGNUP_SECRET } from '../config'; +import { BadRequestError, UnauthorizedRequestError } from '../utils/errors'; declare module 'jsonwebtoken' { export interface UserIDJwtPayload extends jwt.JwtPayload { @@ -21,32 +21,24 @@ const requireSignupAuth = async ( ) => { // JWT (temporary) authentication middleware for complete signup - try { - if (!req.headers?.authorization) - throw new Error('Failed to locate authorization header'); + const [ AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE ] = <[string, string]>req.headers['authorization']?.split(' ', 2) ?? [null, null] + if(AUTH_TOKEN_TYPE === null) return next(BadRequestError({message: `Missing Authorization Header in the request header.`})) + if(AUTH_TOKEN_TYPE.toLowerCase() !== 'bearer') return next(BadRequestError({message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.`})) + if(AUTH_TOKEN_VALUE === null) return next(BadRequestError({message: 'Missing Authorization Body in the request header'})) + + const decodedToken = ( + jwt.verify(AUTH_TOKEN_VALUE, JWT_SIGNUP_SECRET) + ); - const token = req.headers.authorization.split(' ')[1]; - const decodedToken = ( - jwt.verify(token, JWT_SIGNUP_SECRET) - ); + const user = await User.findOne({ + _id: decodedToken.userId + }).select('+publicKey'); - const user = await User.findOne({ - _id: decodedToken.userId - }).select('+publicKey'); + if (!user) + return next(UnauthorizedRequestError({message: 'Unable to authenticate for User account completion. Try logging in again'})) - if (!user) - throw new Error('Failed to temporarily authenticate unfound user'); - - req.user = user; - return next(); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - return res.status(401).send({ - error: - 'Failed to temporarily authenticate user for complete account. Try logging in' - }); - } + req.user = user; + return next(); }; export default requireSignupAuth; diff --git a/backend/src/middleware/requireWorkspaceAuth.ts b/backend/src/middleware/requireWorkspaceAuth.ts index 03fb7357f..e5b8898f3 100644 --- a/backend/src/middleware/requireWorkspaceAuth.ts +++ b/backend/src/middleware/requireWorkspaceAuth.ts @@ -1,6 +1,6 @@ -import * as Sentry from '@sentry/node'; import { Request, Response, NextFunction } from 'express'; -import { Membership, IWorkspace } from '../models'; +import { validateMembership } from '../helpers/membership'; +import { UnauthorizedRequestError } from '../utils/errors'; type req = 'params' | 'body' | 'query'; @@ -25,34 +25,18 @@ const requireWorkspaceAuth = ({ // workspace authorization middleware try { - // validate workspace membership - - const membership = await Membership.findOne({ - user: req.user._id, - workspace: req[location].workspaceId - }).populate<{ workspace: IWorkspace }>('workspace'); - - if (!membership) { - throw new Error('Failed to find workspace membership'); - } - - if (!acceptedRoles.includes(membership.role)) { - throw new Error('Failed to validate workspace membership role'); - } - - if (!acceptedStatuses.includes(membership.status)) { - throw new Error('Failed to validate workspace membership status'); - } + const membership = await validateMembership({ + userId: req.user._id.toString(), + workspaceId: req[location].workspaceId, + acceptedRoles, + acceptedStatuses + }); req.membership = membership; return next(); } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - return res.status(401).send({ - error: 'Failed workspace authorization' - }); + return next(UnauthorizedRequestError({message: 'Unable to authenticate workspace'})) } }; }; diff --git a/backend/src/middleware/validateRequest.ts b/backend/src/middleware/validateRequest.ts index 4dcafc13b..484b02cab 100644 --- a/backend/src/middleware/validateRequest.ts +++ b/backend/src/middleware/validateRequest.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from 'express'; -import * as Sentry from '@sentry/node'; import { validationResult } from 'express-validator'; +import { BadRequestError, UnauthorizedRequestError } from '../utils/errors'; /** * Validate intended inputs on [req] via express-validator @@ -15,16 +15,12 @@ const validate = (req: Request, res: Response, next: NextFunction) => { try { const errors = validationResult(req); if (!errors.isEmpty()) { - return res.status(400).json({ errors: errors.array() }); + return next(BadRequestError({context: {errors: errors.array}})) } return next(); } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - return res.status(401).send({ - error: "Looks like you're unauthenticated . Try logging in" - }); + return next(UnauthorizedRequestError({message: 'Unauthenticated requests are not allowed. Try logging in'})) } }; diff --git a/backend/src/models/bot.ts b/backend/src/models/bot.ts new file mode 100644 index 000000000..c7e5a9abe --- /dev/null +++ b/backend/src/models/bot.ts @@ -0,0 +1,57 @@ +import { Schema, model, Types } from 'mongoose'; + +export interface IBot { + _id: Types.ObjectId; + name: string; + workspace: Types.ObjectId; + isActive: boolean; + publicKey: string; + encryptedPrivateKey: string; + iv: string; + tag: string; +} + +const botSchema = new Schema( + { + name: { + type: String, + required: true + }, + workspace: { + type: Schema.Types.ObjectId, + ref: 'Workspace', + required: true + }, + isActive: { + type: Boolean, + required: true, + default: false + }, + publicKey: { + type: String, + required: true + }, + encryptedPrivateKey: { + type: String, + required: true, + select: false + }, + iv: { + type: String, + required: true, + select: false + }, + tag: { + type: String, + required: true, + select: false + } + }, + { + timestamps: true + } +); + +const Bot = model('Bot', botSchema); + +export default Bot; diff --git a/backend/src/models/botKey.ts b/backend/src/models/botKey.ts new file mode 100644 index 000000000..79555cd53 --- /dev/null +++ b/backend/src/models/botKey.ts @@ -0,0 +1,45 @@ +import { Schema, model, Types } from 'mongoose'; + +export interface IBotKey { + _id: Types.ObjectId; + encryptedKey: string; + nonce: string; + sender: Types.ObjectId; + bot: Types.ObjectId; + workspace: Types.ObjectId; +} + +const botKeySchema = new Schema( + { + encryptedKey: { + type: String, + required: true + }, + nonce: { + type: String, + required: true + }, + sender: { + type: Schema.Types.ObjectId, + ref: 'User', + required: true + }, + bot: { + type: Schema.Types.ObjectId, + ref: 'Bot', + required: true + }, + workspace: { + type: Schema.Types.ObjectId, + ref: 'Workspace', + required: true + } + }, + { + timestamps: true + } +); + +const BotKey = model('BotKey', botKeySchema); + +export default BotKey; diff --git a/backend/src/models/index.ts b/backend/src/models/index.ts index 9b07f6766..78c38060b 100644 --- a/backend/src/models/index.ts +++ b/backend/src/models/index.ts @@ -1,4 +1,6 @@ import BackupPrivateKey, { IBackupPrivateKey } from './backupPrivateKey'; +import Bot, { IBot } from './bot'; +import BotKey, { IBotKey } from './botKey'; import IncidentContactOrg, { IIncidentContactOrg } from './incidentContactOrg'; import Integration, { IIntegration } from './integration'; import IntegrationAuth, { IIntegrationAuth } from './integrationAuth'; @@ -16,6 +18,10 @@ import Workspace, { IWorkspace } from './workspace'; export { BackupPrivateKey, IBackupPrivateKey, + Bot, + IBot, + BotKey, + IBotKey, IncidentContactOrg, IIncidentContactOrg, Integration, diff --git a/backend/src/models/integration.ts b/backend/src/models/integration.ts index 5e72e8b54..6da699216 100644 --- a/backend/src/models/integration.ts +++ b/backend/src/models/integration.ts @@ -1,59 +1,83 @@ import { Schema, model, Types } from 'mongoose'; import { - ENV_DEV, - ENV_TESTING, - ENV_STAGING, - ENV_PROD, - INTEGRATION_HEROKU, - INTEGRATION_NETLIFY + ENV_DEV, + ENV_TESTING, + ENV_STAGING, + ENV_PROD, + INTEGRATION_HEROKU, + INTEGRATION_VERCEL, + INTEGRATION_NETLIFY, + INTEGRATION_GITHUB } from '../variables'; export interface IIntegration { - _id: Types.ObjectId; - workspace: Types.ObjectId; - environment: 'dev' | 'test' | 'staging' | 'prod'; - isActive: boolean; - app: string; - integration: 'heroku' | 'netlify'; - integrationAuth: Types.ObjectId; + _id: Types.ObjectId; + workspace: Types.ObjectId; + environment: 'dev' | 'test' | 'staging' | 'prod'; + isActive: boolean; + app: string; + target: string; + context: string; + siteId: string; + integration: 'heroku' | 'vercel' | 'netlify' | 'github'; + integrationAuth: Types.ObjectId; } const integrationSchema = new Schema( - { - workspace: { - type: Schema.Types.ObjectId, - ref: 'Workspace', - required: true - }, - environment: { - type: String, - enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD], - required: true - }, - isActive: { - type: Boolean, - required: true - }, - app: { - // name of app in provider - type: String, - default: null, - required: true - }, - integration: { - type: String, - enum: [INTEGRATION_HEROKU, INTEGRATION_NETLIFY], - required: true - }, - integrationAuth: { - type: Schema.Types.ObjectId, - ref: 'IntegrationAuth', - required: true - } - }, - { - timestamps: true - } + { + workspace: { + type: Schema.Types.ObjectId, + ref: 'Workspace', + required: true + }, + environment: { + type: String, + enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD], + required: true + }, + isActive: { + type: Boolean, + required: true + }, + app: { + // name of app in provider + type: String, + default: null + }, + target: { + // vercel-specific target (environment) + type: String, + default: null + }, + context: { + // netlify-specific context (deploy) + type: String, + default: null + }, + siteId: { + // netlify-specific site (app) id + type: String, + default: null + }, + integration: { + type: String, + enum: [ + INTEGRATION_HEROKU, + INTEGRATION_VERCEL, + INTEGRATION_NETLIFY, + INTEGRATION_GITHUB + ], + required: true + }, + integrationAuth: { + type: Schema.Types.ObjectId, + ref: 'IntegrationAuth', + required: true + } + }, + { + timestamps: true + } ); const Integration = model('Integration', integrationSchema); diff --git a/backend/src/models/integrationAuth.ts b/backend/src/models/integrationAuth.ts index 0e9542a20..231416588 100644 --- a/backend/src/models/integrationAuth.ts +++ b/backend/src/models/integrationAuth.ts @@ -1,67 +1,87 @@ import { Schema, model, Types } from 'mongoose'; -import { INTEGRATION_HEROKU, INTEGRATION_NETLIFY } from '../variables'; +import { + INTEGRATION_HEROKU, + INTEGRATION_VERCEL, + INTEGRATION_NETLIFY, + INTEGRATION_GITHUB +} from '../variables'; export interface IIntegrationAuth { - _id: Types.ObjectId; - workspace: Types.ObjectId; - integration: 'heroku' | 'netlify'; - refreshCiphertext?: string; - refreshIV?: string; - refreshTag?: string; - accessCiphertext?: string; - accessIV?: string; - accessTag?: string; - accessExpiresAt?: Date; + _id: Types.ObjectId; + workspace: Types.ObjectId; + integration: 'heroku' | 'vercel' | 'netlify' | 'github'; + teamId: string; + accountId: string; + refreshCiphertext?: string; + refreshIV?: string; + refreshTag?: string; + accessCiphertext?: string; + accessIV?: string; + accessTag?: string; + accessExpiresAt?: Date; } const integrationAuthSchema = new Schema( - { - workspace: { - type: Schema.Types.ObjectId, - required: true - }, - integration: { - type: String, - enum: [INTEGRATION_HEROKU, INTEGRATION_NETLIFY], - required: true - }, - refreshCiphertext: { - type: String, - select: false - }, - refreshIV: { - type: String, - select: false - }, - refreshTag: { - type: String, - select: false - }, - accessCiphertext: { - type: String, - select: false - }, - accessIV: { - type: String, - select: false - }, - accessTag: { - type: String, - select: false - }, - accessExpiresAt: { - type: Date, - select: false - } - }, - { - timestamps: true - } + { + workspace: { + type: Schema.Types.ObjectId, + required: true + }, + integration: { + type: String, + enum: [ + INTEGRATION_HEROKU, + INTEGRATION_VERCEL, + INTEGRATION_NETLIFY, + INTEGRATION_GITHUB + ], + required: true + }, + teamId: { + // vercel-specific integration param + type: String + }, + accountId: { + // netlify-specific integration param + type: String + }, + refreshCiphertext: { + type: String, + select: false + }, + refreshIV: { + type: String, + select: false + }, + refreshTag: { + type: String, + select: false + }, + accessCiphertext: { + type: String, + select: false + }, + accessIV: { + type: String, + select: false + }, + accessTag: { + type: String, + select: false + }, + accessExpiresAt: { + type: Date, + select: false + } + }, + { + timestamps: true + } ); const IntegrationAuth = model( - 'IntegrationAuth', - integrationAuthSchema + 'IntegrationAuth', + integrationAuthSchema ); export default IntegrationAuth; diff --git a/backend/src/models/token.ts b/backend/src/models/token.ts index 7ac996cb6..9569aee0b 100644 --- a/backend/src/models/token.ts +++ b/backend/src/models/token.ts @@ -2,25 +2,30 @@ import { Schema, model } from 'mongoose'; import { EMAIL_TOKEN_LIFETIME } from '../config'; export interface IToken { - email: String; - token: String; - createdAt: Date; + email: string; + token: string; + createdAt: Date; } const tokenSchema = new Schema({ - email: { - type: String, - required: true - }, - token: { - type: String, - required: true - }, - createdAt: { - type: Date, - expires: EMAIL_TOKEN_LIFETIME, - default: Date.now - } + email: { + type: String, + required: true + }, + token: { + type: String, + required: true + }, + createdAt: { + type: Date, + default: Date.now + } +}); + +tokenSchema.index({ + createdAt: 1 +}, { + expireAfterSeconds: parseInt(EMAIL_TOKEN_LIFETIME) }); const Token = model('Token', tokenSchema); diff --git a/backend/src/routes/auth.ts b/backend/src/routes/auth.ts index 43a57fe7e..6be9c09f7 100644 --- a/backend/src/routes/auth.ts +++ b/backend/src/routes/auth.ts @@ -5,28 +5,24 @@ import { requireAuth, validateRequest } from '../middleware'; import { authController } from '../controllers'; import { loginLimiter } from '../helpers/rateLimiter'; +router.post('/token', validateRequest, authController.getNewToken); + router.post( - '/token', - validateRequest, - authController.getNewToken + '/login1', + loginLimiter, + body('email').exists().trim().notEmpty(), + body('clientPublicKey').exists().trim().notEmpty(), + validateRequest, + authController.login1 ); router.post( - '/login1', - loginLimiter, - body('email').exists().trim().notEmpty(), - body('clientPublicKey').exists().trim().notEmpty(), - validateRequest, - authController.login1 -); - -router.post( - '/login2', - loginLimiter, - body('email').exists().trim().notEmpty(), - body('clientProof').exists().trim().notEmpty(), - validateRequest, - authController.login2 + '/login2', + loginLimiter, + body('email').exists().trim().notEmpty(), + body('clientProof').exists().trim().notEmpty(), + validateRequest, + authController.login2 ); router.post('/logout', requireAuth, authController.logout); diff --git a/backend/src/routes/bot.ts b/backend/src/routes/bot.ts new file mode 100644 index 000000000..3189bec44 --- /dev/null +++ b/backend/src/routes/bot.ts @@ -0,0 +1,38 @@ +import express from 'express'; +const router = express.Router(); +import { body, param } from 'express-validator'; +import { + requireAuth, + requireBotAuth, + requireWorkspaceAuth, + validateRequest +} from '../middleware'; +import { botController } from '../controllers'; +import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../variables'; + +router.get( + '/:workspaceId', + requireAuth, + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [COMPLETED, GRANTED] + }), + param('workspaceId').exists().trim().notEmpty(), + validateRequest, + botController.getBotByWorkspaceId +); + +router.patch( + '/:botId/active', + requireAuth, + requireBotAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [COMPLETED, GRANTED] + }), + body('isActive').isBoolean(), + body('botKey'), + validateRequest, + botController.setBotActiveState +); + +export default router; \ No newline at end of file diff --git a/backend/src/routes/index.ts b/backend/src/routes/index.ts index cf015abfb..2dfe58baa 100644 --- a/backend/src/routes/index.ts +++ b/backend/src/routes/index.ts @@ -1,4 +1,5 @@ import signup from './signup'; +import bot from './bot'; import auth from './auth'; import user from './user'; import userAction from './userAction'; @@ -18,6 +19,7 @@ import integrationAuth from './integrationAuth'; export { signup, auth, + bot, user, userAction, organization, diff --git a/backend/src/routes/integration.ts b/backend/src/routes/integration.ts index d16154172..e6738a803 100644 --- a/backend/src/routes/integration.ts +++ b/backend/src/routes/integration.ts @@ -9,22 +9,6 @@ import { ADMIN, MEMBER, GRANTED } from '../variables'; import { body, param } from 'express-validator'; import { integrationController } from '../controllers'; -router.get('/integrations', requireAuth, integrationController.getIntegrations); - -router.post( - '/:integrationId/sync', - requireAuth, - requireIntegrationAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [GRANTED] - }), - param('integrationId').exists().trim(), - body('key').exists(), - body('secrets').exists(), - validateRequest, - integrationController.syncIntegration -); - router.patch( '/:integrationId', requireAuth, @@ -32,10 +16,15 @@ router.patch( acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [GRANTED] }), - param('integrationId'), - body('update'), + param('integrationId').exists().trim(), + body('app').exists().trim(), + body('environment').exists().trim(), + body('isActive').exists().isBoolean(), + body('target').exists(), + body('context').exists(), + body('siteId').exists(), validateRequest, - integrationController.modifyIntegration + integrationController.updateIntegration ); router.delete( @@ -45,7 +34,7 @@ router.delete( acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [GRANTED] }), - param('integrationId'), + param('integrationId').exists().trim(), validateRequest, integrationController.deleteIntegration ); diff --git a/backend/src/routes/integrationAuth.ts b/backend/src/routes/integrationAuth.ts index 61e5f56bf..ef80a2dcc 100644 --- a/backend/src/routes/integrationAuth.ts +++ b/backend/src/routes/integrationAuth.ts @@ -10,6 +10,12 @@ import { import { ADMIN, MEMBER, GRANTED } from '../variables'; import { integrationAuthController } from '../controllers'; +router.get( + '/integration-options', + requireAuth, + integrationAuthController.getIntegrationOptions +); + router.post( '/oauth-token', requireAuth, @@ -22,7 +28,7 @@ router.post( body('code').exists().trim().notEmpty(), body('integration').exists().trim().notEmpty(), validateRequest, - integrationAuthController.integrationAuthOauthExchange + integrationAuthController.oAuthExchange ); router.get( @@ -42,7 +48,8 @@ router.delete( requireAuth, requireIntegrationAuthorizationAuth({ acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [GRANTED] + acceptedStatuses: [GRANTED], + attachAccessToken: false }), param('integrationAuthId'), validateRequest, diff --git a/backend/src/routes/key.ts b/backend/src/routes/key.ts index 9541ca123..a67a729b1 100644 --- a/backend/src/routes/key.ts +++ b/backend/src/routes/key.ts @@ -34,6 +34,4 @@ router.get( keyController.getLatestKey ); -router.get('/publicKey/infisical', keyController.getPublicKeyInfisical); - export default router; diff --git a/backend/src/routes/password.ts b/backend/src/routes/password.ts index 5d39eac28..8032cba83 100644 --- a/backend/src/routes/password.ts +++ b/backend/src/routes/password.ts @@ -1,7 +1,7 @@ import express from 'express'; const router = express.Router(); import { body } from 'express-validator'; -import { requireAuth, validateRequest } from '../middleware'; +import { requireAuth, requireSignupAuth, validateRequest } from '../middleware'; import { passwordController } from '../controllers'; import { passwordLimiter } from '../helpers/rateLimiter'; @@ -27,6 +27,30 @@ router.post( passwordController.changePassword ); +router.post( + '/email/password-reset', + passwordLimiter, + body('email').exists().trim().notEmpty(), + validateRequest, + passwordController.emailPasswordReset +); + +router.post( + '/email/password-reset-verify', + passwordLimiter, + body('email').exists().trim().notEmpty().isEmail(), + body('code').exists().trim().notEmpty(), + validateRequest, + passwordController.emailPasswordResetVerify +); + +router.get( + '/backup-private-key', + passwordLimiter, + requireSignupAuth, + passwordController.getBackupPrivateKey +); + router.post( '/backup-private-key', passwordLimiter, @@ -41,4 +65,16 @@ router.post( passwordController.createBackupPrivateKey ); -export default router; +router.post( + '/password-reset', + requireSignupAuth, + body('encryptedPrivateKey').exists().trim().notEmpty(), // private key encrypted under new pwd + body('iv').exists().trim().notEmpty(), // new iv for private key + body('tag').exists().trim().notEmpty(), // new tag for private key + body('salt').exists().trim().notEmpty(), // part of new pwd + body('verifier').exists().trim().notEmpty(), // part of new pwd + validateRequest, + passwordController.resetPassword +); + +export default router; \ No newline at end of file diff --git a/backend/src/services/BotService.ts b/backend/src/services/BotService.ts new file mode 100644 index 000000000..792bd8e35 --- /dev/null +++ b/backend/src/services/BotService.ts @@ -0,0 +1,82 @@ +import { + getSecretsHelper, + encryptSymmetricHelper, + decryptSymmetricHelper +} from '../helpers/bot'; + +/** + * Class to handle bot actions + */ +class BotService { + + /** + * Return decrypted secrets for workspace with id [workspaceId] and + * environment [environmen] shared to bot. + * @param {Object} obj + * @param {String} obj.workspaceId - id of workspace of secrets + * @param {String} obj.environment - environment for secrets + * @returns {Object} secretObj - object where keys are secret keys and values are secret values + */ + static async getSecrets({ + workspaceId, + environment + }: { + workspaceId: string; + environment: string; + }) { + return await getSecretsHelper({ + workspaceId, + environment + }); + } + + /** + * Return symmetrically encrypted [plaintext] using the + * bot's copy of the workspace key for workspace with id [workspaceId] + * @param {Object} obj + * @param {String} obj.workspaceId - id of workspace + * @param {String} obj.plaintext - plaintext to encrypt + */ + static async encryptSymmetric({ + workspaceId, + plaintext + }: { + workspaceId: string; + plaintext: string; + }) { + return await encryptSymmetricHelper({ + workspaceId, + plaintext + }); + } + + /** + * Return symmetrically decrypted [ciphertext] using the + * bot's copy of the workspace key for workspace with id [workspaceId] + * @param {Object} obj + * @param {String} obj.workspaceId - id of workspace + * @param {String} obj.ciphertext - ciphertext to decrypt + * @param {String} obj.iv - iv + * @param {String} obj.tag - tag + */ + static async decryptSymmetric({ + workspaceId, + ciphertext, + iv, + tag + }: { + workspaceId: string; + ciphertext: string; + iv: string; + tag: string; + }) { + return await decryptSymmetricHelper({ + workspaceId, + ciphertext, + iv, + tag + }); + } +} + +export default BotService; \ No newline at end of file diff --git a/backend/src/services/EventService.ts b/backend/src/services/EventService.ts new file mode 100644 index 000000000..fcbac9ad0 --- /dev/null +++ b/backend/src/services/EventService.ts @@ -0,0 +1,30 @@ +import { Bot, IBot } from '../models'; +import * as Sentry from '@sentry/node'; +import { handleEventHelper } from '../helpers/event'; + +interface Event { + name: string; + workspaceId: string; + payload: any; +} + +/** + * Class to handle events. + */ +class EventService { + /** + * Handle event [event] + * @param {Object} obj + * @param {Event} obj.event - an event + * @param {String} obj.event.name - name of event + * @param {String} obj.event.workspaceId - id of workspace that event is part of + * @param {Object} obj.event.payload - payload of event (depends on event) + */ + static async handleEvent({ event }: { event: Event }): Promise { + await handleEventHelper({ + event + }); + } +} + +export default EventService; \ No newline at end of file diff --git a/backend/src/services/IntegrationService.ts b/backend/src/services/IntegrationService.ts new file mode 100644 index 000000000..32f5f5a88 --- /dev/null +++ b/backend/src/services/IntegrationService.ts @@ -0,0 +1,145 @@ +import * as Sentry from '@sentry/node'; +import { + Integration +} from '../models'; +import { + handleOAuthExchangeHelper, + syncIntegrationsHelper, + getIntegrationAuthRefreshHelper, + getIntegrationAuthAccessHelper, + setIntegrationAuthRefreshHelper, + setIntegrationAuthAccessHelper, +} from '../helpers/integration'; +import { exchangeCode } from '../integrations'; +import { + ENV_DEV, + EVENT_PUSH_SECRETS +} from '../variables'; + +// should sync stuff be here too? Probably. +// TODO: move bot functions to IntegrationService. + +/** + * Class to handle integrations + */ +class IntegrationService { + + /** + * Perform OAuth2 code-token exchange for workspace with id [workspaceId] and integration + * named [integration] + * - Store integration access and refresh tokens returned from the OAuth2 code-token exchange + * - Add placeholder inactive integration + * - Create bot sequence for integration + * @param {Object} obj + * @param {String} obj.workspaceId - id of workspace + * @param {String} obj.integration - name of integration + * @param {String} obj.code - code + */ + static async handleOAuthExchange({ + workspaceId, + integration, + code + }: { + workspaceId: string; + integration: string; + code: string; + }) { + await handleOAuthExchangeHelper({ + workspaceId, + integration, + code + }); + } + + /** + * Sync/push environment variables in workspace with id [workspaceId] to + * all associated integrations + * @param {Object} obj + * @param {Object} obj.workspaceId - id of workspace + */ + static async syncIntegrations({ + workspaceId + }: { + workspaceId: string; + }) { + return await syncIntegrationsHelper({ + workspaceId + }); + } + + /** + * Return decrypted refresh token for integration auth + * with id [integrationAuthId] + * @param {Object} obj + * @param {String} obj.integrationAuthId - id of integration auth + * @param {String} refreshToken - decrypted refresh token + */ + static async getIntegrationAuthRefresh({ integrationAuthId }: { integrationAuthId: string}) { + return await getIntegrationAuthRefreshHelper({ + integrationAuthId + }); + } + + /** + * Return decrypted access token for integration auth + * with id [integrationAuthId] + * @param {Object} obj + * @param {String} obj.integrationAuthId - id of integration auth + * @param {String} accessToken - decrypted access token + */ + static async getIntegrationAuthAccess({ integrationAuthId }: { integrationAuthId: string}) { + return await getIntegrationAuthAccessHelper({ + integrationAuthId + }); + } + + /** + * Encrypt refresh token [refreshToken] using the bot's copy + * of the workspace key for workspace belonging to integration auth + * with id [integrationAuthId] + * @param {Object} obj + * @param {String} obj.integrationAuthId - id of integration auth + * @param {String} obj.refreshToken - refresh token + * @returns {IntegrationAuth} integrationAuth - updated integration auth + */ + static async setIntegrationAuthRefresh({ + integrationAuthId, + refreshToken + }: { + integrationAuthId: string; + refreshToken: string; + }) { + return await setIntegrationAuthRefreshHelper({ + integrationAuthId, + refreshToken + }); + } + + /** + * Encrypt access token [accessToken] using the bot's copy + * of the workspace key for workspace belonging to integration auth + * with id [integrationAuthId] + * @param {Object} obj + * @param {String} obj.integrationAuthId - id of integration auth + * @param {String} obj.accessToken - access token + * @param {String} obj.accessExpiresAt - expiration date of access token + * @returns {IntegrationAuth} - updated integration auth + */ + static async setIntegrationAuthAccess({ + integrationAuthId, + accessToken, + accessExpiresAt + }: { + integrationAuthId: string; + accessToken: string; + accessExpiresAt: Date; + }) { + return await setIntegrationAuthAccessHelper({ + integrationAuthId, + accessToken, + accessExpiresAt + }); + } +} + +export default IntegrationService; \ No newline at end of file diff --git a/backend/src/services/PostHogClient.ts b/backend/src/services/PostHogClient.ts index eaf56aa6a..4ce0117f0 100644 --- a/backend/src/services/PostHogClient.ts +++ b/backend/src/services/PostHogClient.ts @@ -1,15 +1,27 @@ import { PostHog } from 'posthog-node'; -import { NODE_ENV, POSTHOG_HOST, POSTHOG_PROJECT_API_KEY, TELEMETRY_ENABLED } from '../config'; +import { + NODE_ENV, + POSTHOG_HOST, + POSTHOG_PROJECT_API_KEY, + TELEMETRY_ENABLED +} from '../config'; +import { getLogger } from '../utils/logger'; -let postHogClient: any; -if ( - NODE_ENV === 'production' - && TELEMETRY_ENABLED -) { - // case: enable opt-out telemetry in production - postHogClient = new PostHog(POSTHOG_PROJECT_API_KEY, { - host: POSTHOG_HOST - }); +if(TELEMETRY_ENABLED){ + getLogger("backend-main").info([ + "", + "Infisical collects telemetry data about general usage.", + "The data helps us understand how the product is doing and guide our product development to create the best possible platform; it also helps us demonstrate growth for investors as we support Infisical as open-source software.", + "To opt out of telemetry, you can set `TELEMETRY_ENABLED=false` within the environment variables", + ].join('\n')) } -export default postHogClient; \ No newline at end of file +let postHogClient: any; +if (NODE_ENV === 'production' && TELEMETRY_ENABLED) { + // case: enable opt-out telemetry in production + postHogClient = new PostHog(POSTHOG_PROJECT_API_KEY, { + host: POSTHOG_HOST + }); +} + +export default postHogClient; diff --git a/backend/src/services/database.ts b/backend/src/services/database.ts new file mode 100644 index 000000000..85f39c1b2 --- /dev/null +++ b/backend/src/services/database.ts @@ -0,0 +1,10 @@ +import mongoose from 'mongoose'; +import { getLogger } from '../utils/logger'; + +export const initDatabase = (MONGO_URL: string) => { + mongoose + .connect(MONGO_URL) + .then(() => getLogger("database").info("Database connection established")) + .catch((e) => getLogger("database").error(`Unable to establish Database connection due to the error.\n${e}`)); + return mongoose.connection; +}; diff --git a/backend/src/services/health.ts b/backend/src/services/health.ts new file mode 100644 index 000000000..9c441ba9d --- /dev/null +++ b/backend/src/services/health.ts @@ -0,0 +1,32 @@ +import mongoose from 'mongoose'; +import { createTerminus } from '@godaddy/terminus'; +import { getLogger } from '../utils/logger'; + +export const setUpHealthEndpoint = (server: T) => { + const onSignal = () => { + getLogger('backend-main').info('Server is starting clean-up'); + return Promise.all([ + new Promise((resolve) => { + if (mongoose.connection && mongoose.connection.readyState == 1) { + mongoose.connection.close() + .then(() => resolve('Database connection closed')); + } else { + resolve('Database connection already closed'); + } + }) + ]); + }; + + const healthCheck = () => { + // `state.isShuttingDown` (boolean) shows whether the server is shutting down or not + // optionally include a resolve value to be included as info in the health check response + return Promise.resolve(); + }; + + createTerminus(server, { + healthChecks: { + '/healthcheck': healthCheck, + onSignal + } + }); +}; diff --git a/backend/src/services/index.ts b/backend/src/services/index.ts index 54cdf94f4..531033f30 100644 --- a/backend/src/services/index.ts +++ b/backend/src/services/index.ts @@ -1,5 +1,11 @@ import postHogClient from './PostHogClient'; +import BotService from './BotService'; +import EventService from './EventService'; +import IntegrationService from './IntegrationService'; export { - postHogClient + postHogClient, + BotService, + EventService, + IntegrationService } \ No newline at end of file diff --git a/backend/src/services/smtp.ts b/backend/src/services/smtp.ts new file mode 100644 index 000000000..12841eee7 --- /dev/null +++ b/backend/src/services/smtp.ts @@ -0,0 +1,52 @@ +import nodemailer from 'nodemailer'; +import { SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD, SMTP_SECURE } from '../config'; +import { SMTP_HOST_SENDGRID, SMTP_HOST_MAILGUN } from '../variables'; +import SMTPConnection from 'nodemailer/lib/smtp-connection'; +import * as Sentry from '@sentry/node'; + +const mailOpts: SMTPConnection.Options = { + host: SMTP_HOST, + port: SMTP_PORT as number +}; + +if (SMTP_USERNAME && SMTP_PASSWORD) { + mailOpts.auth = { + user: SMTP_USERNAME, + pass: SMTP_PASSWORD + }; +} + +if (SMTP_SECURE) { + switch (SMTP_HOST) { + case SMTP_HOST_SENDGRID: + mailOpts.requireTLS = true; + break; + case SMTP_HOST_MAILGUN: + mailOpts.requireTLS = true; + mailOpts.tls = { + ciphers: 'TLSv1.2' + } + break; + default: + mailOpts.secure = true; + break; + } +} + +export const initSmtp = () => { + const transporter = nodemailer.createTransport(mailOpts); + transporter + .verify() + .then(() => { + Sentry.setUser(null); + Sentry.captureMessage('SMTP - Successfully connected'); + }) + .catch((err) => { + Sentry.setUser(null); + Sentry.captureException( + `SMTP - Failed to connect to ${SMTP_HOST}:${SMTP_PORT} \n\t${err}` + ); + }); + + return transporter; +}; diff --git a/backend/src/templates/organizationInvitation.handlebars b/backend/src/templates/organizationInvitation.handlebars index 663409188..49cc96f38 100644 --- a/backend/src/templates/organizationInvitation.handlebars +++ b/backend/src/templates/organizationInvitation.handlebars @@ -4,7 +4,7 @@ - Email Verification + Organization Invitation

Infisical

diff --git a/backend/src/templates/passwordReset.handlebars b/backend/src/templates/passwordReset.handlebars new file mode 100644 index 000000000..1e629f664 --- /dev/null +++ b/backend/src/templates/passwordReset.handlebars @@ -0,0 +1,15 @@ + + + + + + Account Recovery + + +

Infisical

+

Reset your password

+

Someone requested a password reset.

+ Reset password +

If you didn't initiate this request, please contact us immediately at team@infisical.com

+ + \ No newline at end of file diff --git a/backend/src/templates/workspaceInvitation.handlebars b/backend/src/templates/workspaceInvitation.handlebars index 22f63efc9..252452ce5 100644 --- a/backend/src/templates/workspaceInvitation.handlebars +++ b/backend/src/templates/workspaceInvitation.handlebars @@ -3,7 +3,7 @@ - Email Verification + Project Invitation

Infisical

diff --git a/backend/src/types/express/index.d.ts b/backend/src/types/express/index.d.ts index 8fdb2b3fe..319562fd0 100644 --- a/backend/src/types/express/index.d.ts +++ b/backend/src/types/express/index.d.ts @@ -1,5 +1,6 @@ import * as express from 'express'; + // TODO: fix (any) types declare global { namespace Express { @@ -11,6 +12,7 @@ declare global { membershipOrg: any; integration: any; integrationAuth: any; + bot: any; serviceToken: any; accessToken: any; query?: any; diff --git a/backend/src/utils/crypto.ts b/backend/src/utils/crypto.ts index 742e65e80..28f96b0cf 100644 --- a/backend/src/utils/crypto.ts +++ b/backend/src/utils/crypto.ts @@ -1,6 +1,22 @@ import nacl from 'tweetnacl'; import util from 'tweetnacl-util'; import AesGCM from './aes-gcm'; +import * as Sentry from '@sentry/node'; + +/** + * Return new base64, NaCl, public-private key pair. + * @returns {Object} obj + * @returns {String} obj.publicKey - base64, NaCl, public key + * @returns {String} obj.privateKey - base64, NaCl, private key + */ +const generateKeyPair = () => { + const pair = nacl.box.keyPair(); + + return ({ + publicKey: util.encodeBase64(pair.publicKey), + privateKey: util.encodeBase64(pair.secretKey) + }); +} /** * Return assymmetrically encrypted [plaintext] using [publicKey] where @@ -32,6 +48,8 @@ const encryptAsymmetric = ({ util.decodeBase64(privateKey) ); } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); throw new Error('Failed to perform asymmetric encryption'); } @@ -71,6 +89,8 @@ const decryptAsymmetric = ({ util.decodeBase64(privateKey) ); } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); throw new Error('Failed to perform asymmetric decryption'); } @@ -81,7 +101,7 @@ const decryptAsymmetric = ({ * Return symmetrically encrypted [plaintext] using [key]. * @param {Object} obj * @param {String} obj.plaintext - plaintext to encrypt - * @param {String} obj.key - 16-byte hex key + * @param {String} obj.key - hex key */ const encryptSymmetric = ({ plaintext, @@ -97,6 +117,8 @@ const encryptSymmetric = ({ iv = obj.iv; tag = obj.tag; } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); throw new Error('Failed to perform symmetric encryption'); } @@ -114,7 +136,7 @@ const encryptSymmetric = ({ * @param {String} obj.ciphertext - ciphertext to decrypt * @param {String} obj.iv - iv * @param {String} obj.tag - tag - * @param {String} obj.key - 32-byte hex key + * @param {String} obj.key - hex key * */ const decryptSymmetric = ({ @@ -132,6 +154,8 @@ const decryptSymmetric = ({ try { plaintext = AesGCM.decrypt(ciphertext, iv, tag, key); } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); throw new Error('Failed to perform symmetric decryption'); } @@ -139,6 +163,7 @@ const decryptSymmetric = ({ }; export { + generateKeyPair, encryptAsymmetric, decryptAsymmetric, encryptSymmetric, diff --git a/backend/src/utils/errors.ts b/backend/src/utils/errors.ts new file mode 100644 index 000000000..40c467131 --- /dev/null +++ b/backend/src/utils/errors.ts @@ -0,0 +1,116 @@ +import RequestError, { LogLevel, RequestErrorContext } from "./requestError" + +//* ----->[GENERAL HTTP ERRORS]<----- +export const RouteNotFoundError = (error?: Partial) => new RequestError({ + logLevel: error?.logLevel ?? LogLevel.INFO, + statusCode: error?.statusCode ?? 404, + type: error?.type ?? 'route_not_found', + message: error?.message ?? 'The requested source was not found', + context: error?.context, + stack: error?.stack +}) + +export const MethodNotAllowedError = (error?: Partial) => new RequestError({ + logLevel: error?.logLevel ?? LogLevel.INFO, + statusCode: error?.statusCode ?? 405, + type: error?.type ?? 'method_not_allowed', + message: error?.message ?? 'The requested method is not allowed for the resource', + context: error?.context, + stack: error?.stack +}) + +export const UnauthorizedRequestError = (error?: Partial) => new RequestError({ + logLevel: error?.logLevel ?? LogLevel.INFO, + statusCode: error?.statusCode ?? 401, + type: error?.type ?? 'unauthorized', + message: error?.message ?? 'You are not authorized to access this resource', + context: error?.context, + stack: error?.stack +}) + +export const ForbiddenRequestError = (error?: Partial) => new RequestError({ + logLevel: error?.logLevel ?? LogLevel.INFO, + statusCode: error?.statusCode ?? 403, + type: error?.type ?? 'forbidden', + message: error?.message ?? 'You are not allowed to access this resource', + context: error?.context, + stack: error?.stack +}) + +export const BadRequestError = (error?: Partial) => new RequestError({ + logLevel: error?.logLevel ?? LogLevel.INFO, + statusCode: error?.statusCode ?? 400, + type: error?.type ?? 'bad_request', + message: error?.message ?? 'The request is invalid or cannot be served', + context: error?.context, + stack: error?.stack +}) + +export const InternalServerError = (error?: Partial) => new RequestError({ + logLevel: error?.logLevel ?? LogLevel.ERROR, + statusCode: error?.statusCode ?? 500, + type: error?.type ?? 'internal_server_error', + message: error?.message ?? 'The server encountered an error while processing the request', + context: error?.context, + stack: error?.stack +}) + +export const ServiceUnavailableError = (error?: Partial) => new RequestError({ + logLevel: error?.logLevel ?? LogLevel.ERROR, + statusCode: error?.statusCode ?? 503, + type: error?.type ?? 'service_unavailable', + message: error?.message ?? 'The service is currently unavailable. Please try again later.', + context: error?.context, + stack: error?.stack +}) + +export const ValidationError = (error?: Partial) => new RequestError({ + logLevel: error?.logLevel ?? LogLevel.ERROR, + statusCode: error?.statusCode ?? 400, + type: error?.type ?? 'validation_error', + message: error?.message ?? 'The request failed validation', + context: error?.context, + stack: error?.stack +}) + +//* ----->[INTEGRATION ERRORS]<----- +export const IntegrationNotFoundError = (error?: Partial) => new RequestError({ + logLevel: error?.logLevel ?? LogLevel.ERROR, + statusCode: error?.statusCode ?? 404, + type: error?.type ?? 'integration_not_found_error', + message: error?.message ?? 'The requested integration was not found', + context: error?.context, + stack: error?.stack +}) + +//* ----->[WORKSPACE ERRORS]<----- +export const WorkspaceNotFoundError = (error?: Partial) => new RequestError({ + logLevel: error?.logLevel ?? LogLevel.ERROR, + statusCode: error?.statusCode ?? 404, + type: error?.type ?? 'workspace_not_found_error', + message: error?.message ?? 'The requested workspace was not found', + context: error?.context, + stack: error?.stack +}) + +//* ----->[ORGANIZATION ERRORS]<----- +export const OrganizationNotFoundError = (error?: Partial) => new RequestError({ + logLevel: error?.logLevel ?? LogLevel.ERROR, + statusCode: error?.statusCode ?? 404, + type: error?.type ?? 'organization_not_found_error', + message: error?.message ?? 'The requested organization was not found', + context: error?.context, + stack: error?.stack +}) + +//* ----->[ACCOUNT ERRORS]<----- +export const AccountNotFoundError = (error?: Partial) => new RequestError({ + logLevel: error?.logLevel ?? LogLevel.ERROR, + statusCode: error?.statusCode ?? 404, + type: error?.type ?? 'account_not_found_error', + message: error?.message ?? 'The requested account was not found', + context: error?.context, + stack: error?.stack +}) + +//* ----->[MISC ERRORS]<----- diff --git a/backend/src/utils/logger.ts b/backend/src/utils/logger.ts new file mode 100644 index 000000000..64c65ea49 --- /dev/null +++ b/backend/src/utils/logger.ts @@ -0,0 +1,65 @@ +/* eslint-disable no-console */ +import { createLogger, format, transports } from 'winston'; +import LokiTransport from 'winston-loki'; +import { LOKI_HOST, NODE_ENV } from '../config'; + +const { combine, colorize, label, printf, splat, timestamp } = format; + +const logFormat = (prefix: string) => combine( + timestamp(), + splat(), + label({ label: prefix }), + printf((info) => `${info.timestamp} ${info.label} ${info.level}: ${info.message}`) +); + +const createLoggerWithLabel = (level: string, label: string) => { + const _level = level.toLowerCase() || 'info' + //* Always add Console output to transports + const _transports: any[] = [ + new transports.Console({ + format: combine( + colorize(), + logFormat(label), + // format.json() + ) + }) + ] + //* Add LokiTransport if it's enabled + if(LOKI_HOST !== undefined){ + _transports.push( + new LokiTransport({ + host: LOKI_HOST, + handleExceptions: true, + handleRejections: true, + batching: true, + level: _level, + timeout: 30000, + format: format.combine( + format.json() + ), + labels: {app: process.env.npm_package_name, version: process.env.npm_package_version, environment: NODE_ENV}, + onConnectionError: (err: Error)=> console.error('Connection error while connecting to Loki Server.\n', err) + }) + ) + } + + + return createLogger({ + level: _level, + transports: _transports, + format: format.combine( + logFormat(label), + format.metadata({ fillExcept: ['message', 'level', 'timestamp', 'label'] }) + ) + }); +} + +const DEFAULT_LOGGERS = { + "backend-main": createLoggerWithLabel('info', '[IFSC:backend-main]'), + "database": createLoggerWithLabel('info', '[IFSC:database]'), +} +type LoggerNames = keyof typeof DEFAULT_LOGGERS + +export const getLogger = (loggerName: LoggerNames) => { + return DEFAULT_LOGGERS[loggerName] +} diff --git a/backend/src/utils/patchAsyncRoutes.js b/backend/src/utils/patchAsyncRoutes.js new file mode 100644 index 000000000..6f6d2367f --- /dev/null +++ b/backend/src/utils/patchAsyncRoutes.js @@ -0,0 +1,65 @@ +/* +Original work Copyright (c) 2016, Nikolay Nemshilov +Modified work Copyright (c) 2016, David Banham + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. + +*/ + +/* eslint-disable @typescript-eslint/no-var-requires */ +/* eslint-env node */ +const Layer = require('express/lib/router/layer'); +const Router = require('express/lib/router'); + +const last = (arr = []) => arr[arr.length - 1]; +const noop = Function.prototype; + +function copyFnProps(oldFn, newFn) { + Object.keys(oldFn).forEach((key) => { + newFn[key] = oldFn[key]; + }); + return newFn; +} + +function wrap(fn) { + const newFn = function newFn(...args) { + const ret = fn.apply(this, args); + const next = (args.length === 5 ? args[2] : last(args)) || noop; + if (ret && ret.catch) ret.catch(err => next(err)); + return ret; + }; + Object.defineProperty(newFn, 'length', { + value: fn.length, + writable: false, + }); + return copyFnProps(fn, newFn); +} + +export function patchRouterParam() { + const originalParam = Router.prototype.constructor.param; + Router.prototype.constructor.param = function param(name, fn) { + fn = wrap(fn); + return originalParam.call(this, name, fn); + }; +} + +Object.defineProperty(Layer.prototype, 'handle', { + enumerable: true, + get() { + return this.__handle; + }, + set(fn) { + fn = wrap(fn); + this.__handle = fn; + }, +}); \ No newline at end of file diff --git a/backend/src/utils/requestError.ts b/backend/src/utils/requestError.ts new file mode 100644 index 000000000..da2803da7 --- /dev/null +++ b/backend/src/utils/requestError.ts @@ -0,0 +1,113 @@ +import { Request } from 'express' +import { VERBOSE_ERROR_OUTPUT } from '../config' + +export enum LogLevel { + DEBUG = 100, + INFO = 200, + NOTICE = 250, + WARNING = 300, + ERROR = 400, + CRITICAL = 500, + ALERT = 550, + EMERGENCY = 600, +} + +export type RequestErrorContext = { + logLevel?: LogLevel, + statusCode: number, + type: string, + message: string, + context?: Record, + stack?: string|undefined +} + +export default class RequestError extends Error{ + + private _logLevel: LogLevel + private _logName: string + statusCode: number + type: string + context: Record + extra: Record[] + private stacktrace: string|undefined|string[] + + constructor( + {logLevel, statusCode, type, message, context, stack} : RequestErrorContext + ){ + super(message) + this._logLevel = logLevel || LogLevel.INFO + this._logName = LogLevel[this._logLevel] + this.statusCode = statusCode + this.type = type + this.context = context || {} + this.extra = [] + + if(stack) this.stack = stack + else Error.captureStackTrace(this, this.constructor) + this.stacktrace = this.stack?.split('\n') + } + + static convertFrom(error: Error) { + //This error was not handled by error handler. Please report this incident to the staff. + return new RequestError({ + logLevel: LogLevel.ERROR, + statusCode: 500, + type: 'internal_server_error', + message: 'This error was not handled by error handler. Please report this incident to the staff', + context: { + message: error.message, + name: error.name + }, + stack: error.stack + }) + } + + get level(){ return this._logLevel } + get levelName(){ return this._logName } + + withTags(...tags: string[]|number[]){ + this.context['tags'] = Object.assign(tags, this.context['tags']) + return this + } + + withExtras(...extras: Record[]){ + this.extra = Object.assign(extras, this.extra) + return this + } + + private _omit(obj: any, keys: string[]): typeof obj{ + const exclude = new Set(keys) + obj = Object.fromEntries(Object.entries(obj).filter(e => !exclude.has(e[0]))) + return obj + } + + public format(req: Request){ + let _context = Object.assign({ + stacktrace: this.stacktrace + }, this.context) + + //* Omit sensitive information from context that can leak internal workings of this program if user is not developer + if(!VERBOSE_ERROR_OUTPUT){ + _context = this._omit(_context, [ + 'stacktrace', + 'exception', + ]) + } + + const formatObject = { + type: this.type, + message: this.message, + context: _context, + level: this.level, + level_name: this.levelName, + status_code: this.statusCode, + datetime_iso: new Date().toISOString(), + application: process.env.npm_package_name || 'unknown', + request_id: req.headers["Request-Id"], + extra: this.extra + } + + return formatObject + + } +} \ No newline at end of file diff --git a/backend/src/variables.ts b/backend/src/variables.ts deleted file mode 100644 index cdd771b71..000000000 --- a/backend/src/variables.ts +++ /dev/null @@ -1,60 +0,0 @@ -// membership roles -const OWNER = 'owner'; -const ADMIN = 'admin'; -const MEMBER = 'member'; - -// membership statuses -const INVITED = 'invited'; - -// -- organization -const ACCEPTED = 'accepted'; - -// -- workspace -const COMPLETED = 'completed'; -const GRANTED = 'granted'; - -// subscriptions -const PLAN_STARTER = 'starter'; -const PLAN_PRO = 'pro'; - -// secrets -const SECRET_SHARED = 'shared'; -const SECRET_PERSONAL = 'personal'; - -// environments -const ENV_DEV = 'dev'; -const ENV_TESTING = 'test'; -const ENV_STAGING = 'staging'; -const ENV_PROD = 'prod'; -const ENV_SET = new Set([ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD]); - -// integrations -const INTEGRATION_HEROKU = 'heroku'; -const INTEGRATION_NETLIFY = 'netlify'; -const INTEGRATION_SET = new Set([INTEGRATION_HEROKU, INTEGRATION_NETLIFY]); - -// integration types -const INTEGRATION_OAUTH2 = 'oauth2'; - -export { - OWNER, - ADMIN, - MEMBER, - INVITED, - ACCEPTED, - COMPLETED, - GRANTED, - PLAN_STARTER, - PLAN_PRO, - SECRET_SHARED, - SECRET_PERSONAL, - ENV_DEV, - ENV_TESTING, - ENV_STAGING, - ENV_PROD, - ENV_SET, - INTEGRATION_HEROKU, - INTEGRATION_NETLIFY, - INTEGRATION_SET, - INTEGRATION_OAUTH2 -}; diff --git a/backend/src/variables/environment.ts b/backend/src/variables/environment.ts new file mode 100644 index 000000000..44d7cdbb2 --- /dev/null +++ b/backend/src/variables/environment.ts @@ -0,0 +1,14 @@ +// environments +const ENV_DEV = 'dev'; +const ENV_TESTING = 'test'; +const ENV_STAGING = 'staging'; +const ENV_PROD = 'prod'; +const ENV_SET = new Set([ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD]); + +export { + ENV_DEV, + ENV_TESTING, + ENV_STAGING, + ENV_PROD, + ENV_SET +} \ No newline at end of file diff --git a/backend/src/variables/event.ts b/backend/src/variables/event.ts new file mode 100644 index 000000000..4477e8e02 --- /dev/null +++ b/backend/src/variables/event.ts @@ -0,0 +1,7 @@ +const EVENT_PUSH_SECRETS = 'pushSecrets'; +const EVENT_PULL_SECRETS = 'pullSecrets'; + +export { + EVENT_PUSH_SECRETS, + EVENT_PULL_SECRETS +} \ No newline at end of file diff --git a/backend/src/variables/index.ts b/backend/src/variables/index.ts new file mode 100644 index 000000000..e284d6d5c --- /dev/null +++ b/backend/src/variables/index.ts @@ -0,0 +1,75 @@ +import { + ENV_DEV, + ENV_TESTING, + ENV_STAGING, + ENV_PROD, + ENV_SET +} from './environment'; +import { + INTEGRATION_HEROKU, + INTEGRATION_VERCEL, + INTEGRATION_NETLIFY, + INTEGRATION_GITHUB, + INTEGRATION_SET, + INTEGRATION_OAUTH2, + INTEGRATION_HEROKU_TOKEN_URL, + INTEGRATION_VERCEL_TOKEN_URL, + INTEGRATION_NETLIFY_TOKEN_URL, + INTEGRATION_GITHUB_TOKEN_URL, + INTEGRATION_HEROKU_API_URL, + INTEGRATION_VERCEL_API_URL, + INTEGRATION_NETLIFY_API_URL, + INTEGRATION_GITHUB_API_URL, + INTEGRATION_OPTIONS +} from './integration'; +import { + OWNER, + ADMIN, + MEMBER, + INVITED, + ACCEPTED, + COMPLETED, + GRANTED +} from './organization'; +import { SECRET_SHARED, SECRET_PERSONAL } from './secret'; +import { EVENT_PUSH_SECRETS, EVENT_PULL_SECRETS } from './event'; +import { SMTP_HOST_SENDGRID, SMTP_HOST_MAILGUN } from './smtp'; +import { PLAN_STARTER, PLAN_PRO } from './stripe'; + +export { + OWNER, + ADMIN, + MEMBER, + INVITED, + ACCEPTED, + COMPLETED, + GRANTED, + SECRET_SHARED, + SECRET_PERSONAL, + ENV_DEV, + ENV_TESTING, + ENV_STAGING, + ENV_PROD, + ENV_SET, + INTEGRATION_HEROKU, + INTEGRATION_VERCEL, + INTEGRATION_NETLIFY, + INTEGRATION_GITHUB, + INTEGRATION_SET, + INTEGRATION_OAUTH2, + INTEGRATION_HEROKU_TOKEN_URL, + INTEGRATION_VERCEL_TOKEN_URL, + INTEGRATION_NETLIFY_TOKEN_URL, + INTEGRATION_GITHUB_TOKEN_URL, + INTEGRATION_HEROKU_API_URL, + INTEGRATION_VERCEL_API_URL, + INTEGRATION_NETLIFY_API_URL, + INTEGRATION_GITHUB_API_URL, + EVENT_PUSH_SECRETS, + EVENT_PULL_SECRETS, + INTEGRATION_OPTIONS, + SMTP_HOST_SENDGRID, + SMTP_HOST_MAILGUN, + PLAN_STARTER, + PLAN_PRO, +}; diff --git a/backend/src/variables/integration.ts b/backend/src/variables/integration.ts new file mode 100644 index 000000000..00e817c57 --- /dev/null +++ b/backend/src/variables/integration.ts @@ -0,0 +1,139 @@ +import { + CLIENT_ID_HEROKU, + CLIENT_ID_NETLIFY, + CLIENT_ID_GITHUB, + CLIENT_SLUG_VERCEL +} from '../config'; + +// integrations +const INTEGRATION_HEROKU = 'heroku'; +const INTEGRATION_VERCEL = 'vercel'; +const INTEGRATION_NETLIFY = 'netlify'; +const INTEGRATION_GITHUB = 'github'; +const INTEGRATION_SET = new Set([ + INTEGRATION_HEROKU, + INTEGRATION_VERCEL, + INTEGRATION_NETLIFY, + INTEGRATION_GITHUB +]); + +// integration types +const INTEGRATION_OAUTH2 = 'oauth2'; + +// integration oauth endpoints +const INTEGRATION_HEROKU_TOKEN_URL = 'https://id.heroku.com/oauth/token'; +const INTEGRATION_VERCEL_TOKEN_URL = + 'https://api.vercel.com/v2/oauth/access_token'; +const INTEGRATION_NETLIFY_TOKEN_URL = 'https://api.netlify.com/oauth/token'; +const INTEGRATION_GITHUB_TOKEN_URL = + 'https://github.com/login/oauth/access_token'; + +// integration apps endpoints +const INTEGRATION_HEROKU_API_URL = 'https://api.heroku.com'; +const INTEGRATION_VERCEL_API_URL = 'https://api.vercel.com'; +const INTEGRATION_NETLIFY_API_URL = 'https://api.netlify.com'; +const INTEGRATION_GITHUB_API_URL = 'https://api.github.com'; + +const INTEGRATION_OPTIONS = [ + { + name: 'Heroku', + slug: 'heroku', + image: 'Heroku', + isAvailable: true, + type: 'oauth2', + clientId: CLIENT_ID_HEROKU, + docsLink: '' + }, + { + name: 'Vercel', + slug: 'vercel', + image: 'Vercel', + isAvailable: true, + type: 'vercel', + clientId: '', + clientSlug: CLIENT_SLUG_VERCEL, + docsLink: '' + }, + { + name: 'Netlify', + slug: 'netlify', + image: 'Netlify', + isAvailable: true, + type: 'oauth2', + clientId: CLIENT_ID_NETLIFY, + docsLink: '' + }, + { + name: 'GitHub', + slug: 'github', + image: 'GitHub', + isAvailable: true, + type: 'oauth2', + clientId: CLIENT_ID_GITHUB, + docsLink: '' + + }, + { + name: 'Google Cloud Platform', + slug: 'gcp', + image: 'Google Cloud Platform', + isAvailable: false, + type: '', + clientId: '', + docsLink: '' + }, + { + name: 'Amazon Web Services', + slug: 'aws', + image: 'Amazon Web Services', + isAvailable: false, + type: '', + clientId: '', + docsLink: '' + }, + { + name: 'Microsoft Azure', + slug: 'azure', + image: 'Microsoft Azure', + isAvailable: false, + type: '', + clientId: '', + docsLink: '' + }, + { + name: 'Travis CI', + slug: 'travisci', + image: 'Travis CI', + isAvailable: false, + type: '', + clientId: '', + docsLink: '' + }, + { + name: 'Circle CI', + slug: 'circleci', + image: 'Circle CI', + isAvailable: false, + type: '', + clientId: '', + docsLink: '' + } +] + +export { + INTEGRATION_HEROKU, + INTEGRATION_VERCEL, + INTEGRATION_NETLIFY, + INTEGRATION_GITHUB, + INTEGRATION_SET, + INTEGRATION_OAUTH2, + INTEGRATION_HEROKU_TOKEN_URL, + INTEGRATION_VERCEL_TOKEN_URL, + INTEGRATION_NETLIFY_TOKEN_URL, + INTEGRATION_GITHUB_TOKEN_URL, + INTEGRATION_HEROKU_API_URL, + INTEGRATION_VERCEL_API_URL, + INTEGRATION_NETLIFY_API_URL, + INTEGRATION_GITHUB_API_URL, + INTEGRATION_OPTIONS +}; diff --git a/backend/src/variables/organization.ts b/backend/src/variables/organization.ts new file mode 100644 index 000000000..f91e1f5d3 --- /dev/null +++ b/backend/src/variables/organization.ts @@ -0,0 +1,24 @@ +// membership roles +const OWNER = 'owner'; +const ADMIN = 'admin'; +const MEMBER = 'member'; + +// membership statuses +const INVITED = 'invited'; + +// -- organization +const ACCEPTED = 'accepted'; + +// -- workspace +const COMPLETED = 'completed'; +const GRANTED = 'granted'; + +export { + OWNER, + ADMIN, + MEMBER, + INVITED, + ACCEPTED, + COMPLETED, + GRANTED +} \ No newline at end of file diff --git a/backend/src/variables/secret.ts b/backend/src/variables/secret.ts new file mode 100644 index 000000000..31cbcf951 --- /dev/null +++ b/backend/src/variables/secret.ts @@ -0,0 +1,8 @@ +// secrets +const SECRET_SHARED = 'shared'; +const SECRET_PERSONAL = 'personal'; + +export { + SECRET_SHARED, + SECRET_PERSONAL +} \ No newline at end of file diff --git a/backend/src/variables/smtp.ts b/backend/src/variables/smtp.ts new file mode 100644 index 000000000..4db7c9f12 --- /dev/null +++ b/backend/src/variables/smtp.ts @@ -0,0 +1,7 @@ +const SMTP_HOST_SENDGRID = 'smtp.sendgrid.net'; +const SMTP_HOST_MAILGUN = 'smtp.mailgun.org'; + +export { + SMTP_HOST_SENDGRID, + SMTP_HOST_MAILGUN +} \ No newline at end of file diff --git a/backend/src/variables/stripe.ts b/backend/src/variables/stripe.ts new file mode 100644 index 000000000..ecdbd98ae --- /dev/null +++ b/backend/src/variables/stripe.ts @@ -0,0 +1,7 @@ +const PLAN_STARTER = 'starter'; +const PLAN_PRO = 'pro'; + +export { + PLAN_STARTER, + PLAN_PRO +} \ No newline at end of file diff --git a/backend/test-resources/docker-compose.test.yml b/backend/test-resources/docker-compose.test.yml new file mode 100644 index 000000000..e9a8c519a --- /dev/null +++ b/backend/test-resources/docker-compose.test.yml @@ -0,0 +1,12 @@ +version: '3' + +services: + mongo-test: + image: mongo + container_name: infisical-test-mongo + restart: always + ports: + - 27018:27017 + environment: + - MONGO_INITDB_ROOT_USERNAME=test + - MONGO_INITDB_ROOT_PASSWORD=test1234 diff --git a/backend/test-resources/env-vars.js b/backend/test-resources/env-vars.js new file mode 100644 index 000000000..a7542728f --- /dev/null +++ b/backend/test-resources/env-vars.js @@ -0,0 +1,5 @@ +/* eslint-disable no-undef */ +process.env.MONGO_URL = + 'mongodb://test:test1234@localhost:27018/?authSource=admin'; +process.env.MONGO_USERNAME = 'test'; +process.env.MONGO_PASSWORD = 'test1234'; diff --git a/backend/tsconfig.json b/backend/tsconfig.json index d6d293372..0bfe3c372 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -8,16 +8,13 @@ "allowJs": true, "outDir": "build", "esModuleInterop": true, + "moduleResolution": "node", "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitAny": true, "skipLibCheck": true, - "typeRoots" : ["./src/types", "./node_modules/@types"] + "typeRoots": ["./src/types", "./node_modules/@types"] }, - "include": [ - "src/**/*" - ], - "exclude": [ - "node_modules" - ] + "include": ["src/**/*"], + "exclude": ["node_modules"] } diff --git a/cli/go.mod b/cli/go.mod index 1c175e2f0..86cc1763f 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -3,24 +3,37 @@ module github.com/Infisical/infisical-merge go 1.19 require ( + github.com/99designs/keyring v1.2.2 github.com/spf13/cobra v1.6.1 golang.org/x/crypto v0.3.0 + golang.org/x/term v0.3.0 ) require ( - github.com/alessio/shellescape v1.4.1 // indirect + github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect + github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect github.com/chzyer/readline v1.5.1 // indirect github.com/danieljoos/wincred v1.1.2 // indirect - github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/dvsekhvalnov/jose2go v1.5.0 // indirect + github.com/go-openapi/errors v0.20.2 // indirect + github.com/go-openapi/strfmt v0.21.3 // indirect + github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect + github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect + github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/mitchellh/mapstructure v1.3.3 // indirect + github.com/mtibben/percent v0.2.1 // indirect + github.com/oklog/ulid v1.3.1 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + go.mongodb.org/mongo-driver v1.10.0 // indirect golang.org/x/net v0.2.0 // indirect - golang.org/x/sys v0.2.0 // indirect + golang.org/x/sys v0.3.0 // indirect ) require ( github.com/go-resty/resty/v2 v2.7.0 github.com/inconshreveable/mousetrap v1.0.1 // indirect + github.com/jedib0t/go-pretty v4.3.0+incompatible github.com/manifoldco/promptui v0.9.0 github.com/sirupsen/logrus v1.9.0 github.com/spf13/pflag v1.0.5 // indirect - github.com/zalando/go-keyring v0.2.1 ) diff --git a/cli/go.sum b/cli/go.sum index 975332397..3419b8051 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -1,5 +1,9 @@ -github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= -github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= +github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs= +github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= +github.com/99designs/keyring v1.2.2 h1:pZd3neh/EmUzWONb35LxQfvuY7kiSXAq3HQd97+XBn0= +github.com/99designs/keyring v1.2.2/go.mod h1:wes/FrByc8j7lFOAGLGSNEg8f/PaI3cgTBqhFkHUrPk= +github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef h1:46PFijGLmAjMPwCCCo7Jf0W6f9slllCkkv7vyc1yOSg= +github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= @@ -10,23 +14,57 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dvsekhvalnov/jose2go v1.5.0 h1:3j8ya4Z4kMCwT5nXIKFSV84YS+HdqSSO0VsTQxaLAeM= +github.com/dvsekhvalnov/jose2go v1.5.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= +github.com/go-openapi/errors v0.20.2 h1:dxy7PGTqEh94zj2E3h1cUmQQWiM1+aeCROfAr02EmK8= +github.com/go-openapi/errors v0.20.2/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/strfmt v0.21.3 h1:xwhj5X6CjXEZZHMWy1zKJxvW9AfHC9pkyUjLvHtKG7o= +github.com/go-openapi/strfmt v0.21.3/go.mod h1:k+RzNO0Da+k3FrrynSNN8F7n/peCmQQqbbXjtDfvmGg= github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY= github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I= -github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= -github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= +github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU= +github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo= +github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mitchellh/mapstructure v1.3.3 h1:SzB1nHZ2Xi+17FP0zVQBHIZqvwRN9408fJO8h+eeNA8= +github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= +github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= @@ -34,32 +72,52 @@ github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/zalando/go-keyring v0.2.1 h1:MBRN/Z8H4U5wEKXiD67YbDAr5cj/DOStmSga70/2qKc= -github.com/zalando/go-keyring v0.2.1/go.mod h1:g63M2PPn0w5vjmEbwAX3ib5I+41zdm4esSETOn9Y6Dw= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= +github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +go.mongodb.org/mongo-driver v1.10.0 h1:UtV6N5k14upNp4LTduX0QCufG124fSu25Wz9tu94GLg= +go.mongodb.org/mongo-driver v1.10.0/go.mod h1:wsihk0Kdgv8Kqu1Anit4sfK+22vSFbUrAVEYRhCXrA8= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI= +golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/cli/packages/cmd/export.go b/cli/packages/cmd/export.go new file mode 100644 index 000000000..f25a726e7 --- /dev/null +++ b/cli/packages/cmd/export.go @@ -0,0 +1,151 @@ +/* +Copyright ยฉ 2022 NAME HERE +*/ +package cmd + +import ( + "encoding/csv" + "encoding/json" + "fmt" + "strings" + + "github.com/Infisical/infisical-merge/packages/models" + "github.com/Infisical/infisical-merge/packages/util" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +const ( + FormatDotenv string = "dotenv" + FormatJson string = "json" + FormatCSV string = "csv" + FormatYaml string = "yaml" +) + +// exportCmd represents the export command +var exportCmd = &cobra.Command{ + Use: "export", + Short: "Used to export environment variables to a file", + DisableFlagsInUseLine: true, + Example: "infisical export --env=prod --format=json > secrets.json", + Args: cobra.NoArgs, + PreRun: toggleDebug, + Run: func(cmd *cobra.Command, args []string) { + envName, err := cmd.Flags().GetString("env") + if err != nil { + log.Errorln("Unable to parse the environment flag") + log.Debugln(err) + return + } + + shouldExpandSecrets, err := cmd.Flags().GetBool("expand") + if err != nil { + log.Errorln("Unable to parse the substitute flag") + log.Debugln(err) + return + } + + projectId, err := cmd.Flags().GetString("projectId") + if err != nil { + log.Errorln("Unable to parse the project id flag") + log.Debugln(err) + return + } + + format, err := cmd.Flags().GetString("format") + if err != nil { + log.Errorln("Unable to parse the format flag") + log.Debugln(err) + return + } + + envsFromApi, err := util.GetAllEnvironmentVariables(projectId, envName) + if err != nil { + log.Errorln("Something went wrong when pulling secrets using your Infisical token. Double check the token, project id or environment name (dev, prod, ect.)") + log.Debugln(err) + return + } + + var output string + if shouldExpandSecrets { + substitutions := util.SubstituteSecrets(envsFromApi) + output, err = formatEnvs(substitutions, format) + if err != nil { + log.Errorln(err) + return + } + } else { + output, err = formatEnvs(envsFromApi, format) + if err != nil { + log.Errorln(err) + return + } + } + fmt.Print(output) + }, +} + +func init() { + rootCmd.AddCommand(exportCmd) + exportCmd.Flags().StringP("env", "e", "dev", "Set the environment (dev, prod, etc.) from which your secrets should be pulled from") + exportCmd.Flags().String("projectId", "", "The project ID from which your secrets should be pulled from") + exportCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets") + exportCmd.Flags().StringP("format", "f", "dotenv", "Set the format of the output file (dotenv, json, csv)") +} + +// Format according to the format flag +func formatEnvs(envs []models.SingleEnvironmentVariable, format string) (string, error) { + switch strings.ToLower(format) { + case FormatDotenv: + return formatAsDotEnv(envs), nil + case FormatJson: + return formatAsJson(envs), nil + case FormatCSV: + return formatAsCSV(envs), nil + case FormatYaml: + return formatAsYaml(envs), nil + default: + return "", fmt.Errorf("invalid format type: %s. Available format types are [%s]", format, []string{FormatDotenv, FormatJson, FormatCSV, FormatYaml}) + } +} + +// Format environment variables as a CSV file +func formatAsCSV(envs []models.SingleEnvironmentVariable) string { + csvString := &strings.Builder{} + writer := csv.NewWriter(csvString) + writer.Write([]string{"Key", "Value"}) + for _, env := range envs { + writer.Write([]string{env.Key, env.Value}) + } + writer.Flush() + return csvString.String() +} + +// Format environment variables as a dotenv file +func formatAsDotEnv(envs []models.SingleEnvironmentVariable) string { + var dotenv string + for _, env := range envs { + dotenv += fmt.Sprintf("%s='%s'\n", env.Key, env.Value) + } + return dotenv +} + +func formatAsYaml(envs []models.SingleEnvironmentVariable) string { + var dotenv string + for _, env := range envs { + dotenv += fmt.Sprintf("%s: %s\n", env.Key, env.Value) + } + return dotenv +} + +// Format environment variables as a JSON file +func formatAsJson(envs []models.SingleEnvironmentVariable) string { + // Dump as a json array + json, err := json.Marshal(envs) + if err != nil { + log.Errorln("Unable to marshal environment variables to JSON") + log.Debugln(err) + return "" + } + return string(json) +} diff --git a/cli/packages/cmd/init.go b/cli/packages/cmd/init.go index 55abe6d4f..2789f220d 100644 --- a/cli/packages/cmd/init.go +++ b/cli/packages/cmd/init.go @@ -36,7 +36,7 @@ var initCmd = &cobra.Command{ return } - if util.WorkspaceConfigFileExists() { + if util.WorkspaceConfigFileExistsInCurrentPath() { shouldOverride, err := shouldOverrideWorkspacePrompt() if err != nil { log.Errorln("Unable to parse your answer") diff --git a/cli/packages/cmd/login.go b/cli/packages/cmd/login.go index f84fad501..9c0c22930 100644 --- a/cli/packages/cmd/login.go +++ b/cli/packages/cmd/login.go @@ -91,7 +91,9 @@ var loginCmd = &cobra.Command{ err = util.StoreUserCredsInKeyRing(userCredentialsToBeStored) if err != nil { - log.Errorln("Unable to store your credentials in system key ring") + currentVault, _ := util.GetCurrentVaultBackend() + log.Errorf("Unable to store your credentials in system vault [%s]. Rerun with flag -d to see full logs", currentVault) + log.Errorln("To trouble shoot further, read https://infisical.com/docs/cli/faq") log.Debugln(err) return } @@ -114,7 +116,7 @@ func init() { func askForLoginCredentials() (email string, password string, err error) { validateEmail := func(input string) error { - matched, err := regexp.MatchString("^[\\w!#$%&'*+/=?`{|}~^-]+(?:\\.[\\w!#$%&'*+/=?`{|}~^-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,6}$", input) + matched, err := regexp.MatchString("^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$", input) if err != nil || !matched { return errors.New("this doesn't look like an email address") } diff --git a/cli/packages/cmd/root.go b/cli/packages/cmd/root.go index 42d85e5b2..6a0bdb80c 100644 --- a/cli/packages/cmd/root.go +++ b/cli/packages/cmd/root.go @@ -15,7 +15,7 @@ var rootCmd = &cobra.Command{ Short: "Infisical CLI is used to inject environment variables into any process", Long: `Infisical is a simple, end-to-end encrypted service that enables teams to sync and manage their environment variables across their development life cycle.`, CompletionOptions: cobra.CompletionOptions{DisableDefaultCmd: true}, - Version: "0.1.6", + Version: "0.1.14", } // Execute adds all child commands to the root command and sets flags appropriately. @@ -31,4 +31,6 @@ func init() { rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") rootCmd.PersistentFlags().BoolVarP(&debugLogging, "debug", "d", false, "Enable verbose logging") rootCmd.PersistentFlags().StringVar(&util.INFISICAL_URL, "domain", "https://app.infisical.com/api", "Point the CLI to your own backend") + // rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { + // } } diff --git a/cli/packages/cmd/run.go b/cli/packages/cmd/run.go index 6b44fa548..7518fe98d 100644 --- a/cli/packages/cmd/run.go +++ b/cli/packages/cmd/run.go @@ -8,6 +8,7 @@ import ( "os" "os/exec" "os/signal" + "runtime" "strings" "syscall" @@ -19,12 +20,38 @@ import ( // runCmd represents the run command var runCmd = &cobra.Command{ + Example: ` + infisical run --env=dev -- npm run dev + infisical run --command "first-command && second-command; more-commands..." + `, Use: "run [any infisical run command flags] -- [your application start command]", Short: "Used to inject environments variables into your application process", DisableFlagsInUseLine: true, - Example: "infisical run --env=prod -- npm run dev", - Args: cobra.MinimumNArgs(1), PreRun: toggleDebug, + Args: func(cmd *cobra.Command, args []string) error { + // Check if the --command flag has been set + commandFlagSet := cmd.Flags().Changed("command") + + // If the --command flag has been set, check if a value was provided + if commandFlagSet { + command := cmd.Flag("command").Value.String() + if command == "" { + return fmt.Errorf("you need to provide a command after the flag --command") + } + + // If the --command flag has been set, args should not be provided + if len(args) > 0 { + return fmt.Errorf("you cannot set any arguments after --command flag. --command only takes a string command") + } + } else { + // If the --command flag has not been set, at least one arg should be provided + if len(args) == 0 { + return fmt.Errorf("at least one argument is required after the run command, received %d", len(args)) + } + } + + return nil + }, Run: func(cmd *cobra.Command, args []string) { envName, err := cmd.Flags().GetString("env") if err != nil { @@ -47,53 +74,30 @@ var runCmd = &cobra.Command{ return } - var envsFromApi []models.SingleEnvironmentVariable - infisicalToken := os.Getenv(util.INFISICAL_TOKEN_NAME) - if infisicalToken == "" { - hasUserLoggedInbefore, loggedInUserEmail, err := util.IsUserLoggedIn() - if err != nil { - log.Info("Unexpected issue occurred while checking login status. To see more details, add flag --debug") - log.Debugln(err) - return - } - - if !hasUserLoggedInbefore { - log.Infoln("No logged in user. To login, please run command [infisical login]") - return - } - - userCreds, err := util.GetUserCredsFromKeyRing(loggedInUserEmail) - if err != nil { - log.Infoln("Unable to get user creds from key ring") - log.Debug(err) - return - } - - if !util.WorkspaceConfigFileExists() { - log.Infoln("Your project is not connected to a project yet. Run command [infisical init]") - return - } - - envsFromApi, err = util.GetSecretsFromAPIUsingCurrentLoggedInUser(envName, userCreds) - if err != nil { - log.Errorln("Something went wrong when pulling secrets using your logged in credentials. If the issue persists, double check your project id/try logging in again.") - log.Debugln(err) - return - } - } else { - envsFromApi, err = util.GetSecretsFromAPIUsingInfisicalToken(infisicalToken, envName, projectId) - if err != nil { - log.Errorln("Something went wrong when pulling secrets using your Infisical token. Double check the token, project id or environment name (dev, prod, ect.)") - log.Debugln(err) - return - } + secrets, err := util.GetAllEnvironmentVariables(projectId, envName) + if err != nil { + log.Debugln(err) + return } if shouldExpandSecrets { - substitutions := util.SubstituteSecrets(envsFromApi) - execCmd(args[0], args[1:], substitutions) + secrets = util.SubstituteSecrets(secrets) + } + + if cmd.Flags().Changed("command") { + command := cmd.Flag("command").Value.String() + err = executeMultipleCommandWithEnvs(command, secrets) + if err != nil { + log.Errorf("Something went wrong when executing your command [error=%s]", err) + return + } } else { - execCmd(args[0], args[1:], envsFromApi) + err = executeSingleCommandWithEnvs(args, secrets) + if err != nil { + log.Errorf("Something went wrong when executing your command [error=%s]", err) + return + } + return } }, @@ -104,19 +108,51 @@ func init() { runCmd.Flags().StringP("env", "e", "dev", "Set the environment (dev, prod, etc.) from which your secrets should be pulled from") runCmd.Flags().String("projectId", "", "The project ID from which your secrets should be pulled from") runCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets") + runCmd.Flags().StringP("command", "c", "", "chained commands to execute (e.g. \"npm install && npm run dev; echo ...\")") } -// Credit: inspired by AWS Valut -func execCmd(command string, args []string, envs []models.SingleEnvironmentVariable) error { - log.Infof("\x1b[%dm%s\x1b[0m", 32, "\u2713 Injected Infisical secrets into your application process successfully") - log.Debugln("Secrets to inject:", envs) - log.Debugf("executing command: %s %s \n", command, strings.Join(args, " ")) - cmd := exec.Command(command, args...) +// Will execute a single command and pass in the given secrets into the process +func executeSingleCommandWithEnvs(args []string, secrets []models.SingleEnvironmentVariable) error { + command := args[0] + argsForCommand := args[1:] + numberOfSecretsInjected := fmt.Sprintf("\u2713 Injected %v Infisical secrets into your application process successfully", len(secrets)) + log.Infof("\x1b[%dm%s\x1b[0m", 32, numberOfSecretsInjected) + log.Debugf("executing command: %s %s \n", command, strings.Join(argsForCommand, " ")) + log.Debugln("Secrets injected:", secrets) + + cmd := exec.Command(command, argsForCommand...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - cmd.Env = getAllEnvs(envs) + cmd.Env = getAllEnvs(secrets) + return execCmd(cmd) +} + +func executeMultipleCommandWithEnvs(fullCommand string, secrets []models.SingleEnvironmentVariable) error { + shell := [2]string{"sh", "-c"} + if runtime.GOOS == "windows" { + shell = [2]string{"cmd", "/C"} + } else { + shell[0] = os.Getenv("SHELL") + } + + cmd := exec.Command(shell[0], shell[1], fullCommand) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Env = getAllEnvs(secrets) + + numberOfSecretsInjected := fmt.Sprintf("\u2713 Injected %v Infisical secrets into your application process successfully", len(secrets)) + log.Infof("\x1b[%dm%s\x1b[0m", 32, numberOfSecretsInjected) + log.Debugf("executing command: %s %s %s \n", shell[0], shell[1], fullCommand) + log.Debugln("Secrets injected:", secrets) + + return execCmd(cmd) +} + +// Credit: inspired by AWS Valut +func execCmd(cmd *exec.Cmd) error { sigChannel := make(chan os.Signal, 1) signal.Notify(sigChannel) @@ -133,7 +169,7 @@ func execCmd(command string, args []string, envs []models.SingleEnvironmentVaria if err := cmd.Wait(); err != nil { _ = cmd.Process.Signal(os.Kill) - return fmt.Errorf("Failed to wait for command termination: %v", err) + return fmt.Errorf("failed to wait for command termination: %v", err) } waitStatus := cmd.ProcessState.Sys().(syscall.WaitStatus) diff --git a/cli/packages/cmd/vault.go b/cli/packages/cmd/vault.go new file mode 100644 index 000000000..a2b03dbec --- /dev/null +++ b/cli/packages/cmd/vault.go @@ -0,0 +1,98 @@ +/* +Copyright ยฉ 2022 NAME HERE +*/ +package cmd + +import ( + "github.com/99designs/keyring" + "github.com/Infisical/infisical-merge/packages/util" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +var vaultSetCmd = &cobra.Command{ + Example: `infisical vault set pass`, + Use: "set [vault-name]", + Short: "Used to set the vault backend to store your login details securely at rest", + DisableFlagsInUseLine: true, + PreRun: toggleDebug, + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + wantedVaultTypeName := args[0] + currentVaultBackend, err := util.GetCurrentVaultBackend() + if err != nil { + log.Errorf("Unable to set vault to [%s] because of [err=%s]", wantedVaultTypeName, err) + return + } + + if wantedVaultTypeName == string(currentVaultBackend) { + log.Errorf("You are already on vault backend [%s]", currentVaultBackend) + return + } + + if isVaultToSwitchToValid(wantedVaultTypeName) { + configFile, err := util.GetConfigFile() + if err != nil { + log.Errorf("Unable to set vault to [%s] because of [err=%s]", wantedVaultTypeName, err) + return + } + + configFile.VaultBackendType = keyring.BackendType(wantedVaultTypeName) // save selected vault + configFile.LoggedInUserEmail = "" // reset the logged in user to prompt them to re login + + err = util.WriteConfigFile(&configFile) + if err != nil { + log.Errorf("Unable to set vault to [%s] because an error occurred when saving the config file [err=%s]", wantedVaultTypeName, err) + return + } + + log.Infof("Successfully, switched vault backend from [%s] to [%s]. Please login in again to store your login details in the new vault with [infisical login]", currentVaultBackend, wantedVaultTypeName) + } else { + log.Errorf("The requested vault type [%s] is not available on this system. Only the following vault backends are available for you system: %s", wantedVaultTypeName, keyring.AvailableBackends()) + } + }, +} + +// runCmd represents the run command +var vaultCmd = &cobra.Command{ + Use: "vault", + Short: "Used to manage where your Infisical login token is saved on your machine", + DisableFlagsInUseLine: true, + PreRun: toggleDebug, + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + printAvailableVaultBackends() + }, +} + +func printAvailableVaultBackends() { + log.Infof("The following vaults are available on your system:") + for _, backend := range keyring.AvailableBackends() { + log.Infof("- %s", backend) + } + + currentVaultBackend, err := util.GetCurrentVaultBackend() + if err != nil { + log.Errorf("printAvailableVaultBackends: unable to print the available vault backend because of error [err=%s]", err) + } + + log.Infof("\nYou are currently using [%s] vault to store your login credentials", string(currentVaultBackend)) +} + +// Checks if the vault that the user wants to switch to is a valid available vault +func isVaultToSwitchToValid(vaultNameToSwitchTo string) bool { + isFound := false + for _, backend := range keyring.AvailableBackends() { + if vaultNameToSwitchTo == string(backend) { + isFound = true + break + } + } + + return isFound +} + +func init() { + vaultCmd.AddCommand(vaultSetCmd) + rootCmd.AddCommand(vaultCmd) +} diff --git a/cli/packages/models/cli.go b/cli/packages/models/cli.go index 708a9ed8e..0d1f96a05 100644 --- a/cli/packages/models/cli.go +++ b/cli/packages/models/cli.go @@ -1,5 +1,7 @@ package models +import "github.com/99designs/keyring" + type UserCredentials struct { Email string `json:"email"` PrivateKey string `json:"privateKey"` @@ -8,7 +10,8 @@ type UserCredentials struct { // The file struct for Infisical config file type ConfigFile struct { - LoggedInUserEmail string `json:"loggedInUserEmail"` + LoggedInUserEmail string `json:"loggedInUserEmail"` + VaultBackendType keyring.BackendType `json:"vaultBackendType"` } type SingleEnvironmentVariable struct { diff --git a/cli/packages/models/error.go b/cli/packages/models/error.go index 28e48d54d..f6d58cb5a 100644 --- a/cli/packages/models/error.go +++ b/cli/packages/models/error.go @@ -5,10 +5,13 @@ import log "github.com/sirupsen/logrus" // Custom error type so that we can give helpful messages in CLI type Error struct { Err error - DebugMessage string FriendlyMessage string } func (e *Error) printFriendlyMessage() { log.Infoln(e.FriendlyMessage) } + +func (e *Error) printDebuError() { + log.Debugln(e.Err) +} diff --git a/cli/packages/util/common.go b/cli/packages/util/common.go index 37c881994..f3ee274b3 100644 --- a/cli/packages/util/common.go +++ b/cli/packages/util/common.go @@ -19,6 +19,7 @@ func GetHomeDir() (string, error) { return directory, err } +// write file to given path. If path does not exist throw error func WriteToFile(fileName string, dataToWrite []byte, filePerm os.FileMode) error { err := os.WriteFile(fileName, dataToWrite, filePerm) if err != nil { diff --git a/cli/packages/util/config.go b/cli/packages/util/config.go index c42f26fb5..48bb57241 100644 --- a/cli/packages/util/config.go +++ b/cli/packages/util/config.go @@ -24,8 +24,15 @@ func WriteInitalConfig(userCredentials *models.UserCredentials) error { } } + // get existing config + existingConfigFile, err := GetConfigFile() + if err != nil { + return fmt.Errorf("writeInitalConfig: unable to write config file because [err=%s]", err) + } + configFile := models.ConfigFile{ LoggedInUserEmail: userCredentials.Email, + VaultBackendType: existingConfigFile.VaultBackendType, } configFileMarshalled, err := json.Marshal(configFile) @@ -56,7 +63,7 @@ func ConfigFileExists() bool { } } -func WorkspaceConfigFileExists() bool { +func WorkspaceConfigFileExistsInCurrentPath() bool { if _, err := os.Stat(INFISICAL_WORKSPACE_CONFIG_FILE_NAME); err == nil { return true } else { @@ -90,3 +97,124 @@ func GetFullConfigFilePath() (fullPathToFile string, fullPathToDirectory string, fullDirPath := fmt.Sprintf("%s/%s", homeDir, CONFIG_FOLDER_NAME) return fullPath, fullDirPath, err } + +// Given a path to a workspace config, unmarshal workspace config +func GetWorkspaceConfigByPath(path string) (workspaceConfig models.WorkspaceConfigFile, err error) { + workspaceConfigFileAsBytes, err := os.ReadFile(path) + if err != nil { + return models.WorkspaceConfigFile{}, fmt.Errorf("GetWorkspaceConfigByPath: Unable to read workspace config file because [%s]", err) + } + + var workspaceConfigFile models.WorkspaceConfigFile + err = json.Unmarshal(workspaceConfigFileAsBytes, &workspaceConfigFile) + if err != nil { + return models.WorkspaceConfigFile{}, fmt.Errorf("GetWorkspaceConfigByPath: Unable to unmarshal workspace config file because [%s]", err) + } + + return workspaceConfigFile, nil +} + +// Will get the list of .infisical.json files that are located +// within the root of each sub folder from where the CLI is ran from +func GetAllWorkSpaceConfigsStartingFromCurrentPath() (workspaces []models.WorkspaceConfigFile, err error) { + currentDir, err := os.Getwd() + if err != nil { + return nil, fmt.Errorf("GetAllProjectConfigs: unable to get the current directory because [%s]", err) + } + + files, err := os.ReadDir(currentDir) + if err != nil { + return nil, fmt.Errorf("GetAllProjectConfigs: unable to read the contents of the current directory because [%s]", err) + } + + listOfWorkSpaceConfigs := []models.WorkspaceConfigFile{} + for _, file := range files { + if !file.IsDir() && file.Name() == INFISICAL_WORKSPACE_CONFIG_FILE_NAME { + pathToWorkspaceConfigFile := currentDir + "/" + INFISICAL_WORKSPACE_CONFIG_FILE_NAME + + workspaceConfig, err := GetWorkspaceConfigByPath(pathToWorkspaceConfigFile) + if err != nil { + return nil, fmt.Errorf("GetAllProjectConfigs: Unable to get config file because [%s]", err) + } + + listOfWorkSpaceConfigs = append(listOfWorkSpaceConfigs, workspaceConfig) + + } else if file.IsDir() { + pathToSubFolder := currentDir + "/" + file.Name() + pathToMaybeWorkspaceConfigFile := pathToSubFolder + "/" + INFISICAL_WORKSPACE_CONFIG_FILE_NAME + + _, err := os.Stat(pathToMaybeWorkspaceConfigFile) + if err != nil { + continue // workspace config file doesn't exist + } + + workspaceConfig, err := GetWorkspaceConfigByPath(pathToMaybeWorkspaceConfigFile) + if err != nil { + return nil, fmt.Errorf("GetAllProjectConfigs: Unable to get config file because [%s]", err) + } + + listOfWorkSpaceConfigs = append(listOfWorkSpaceConfigs, workspaceConfig) + } + } + + return listOfWorkSpaceConfigs, nil +} + +// Get the infisical config file and if it doesn't exist, return empty config model, otherwise raise error +func GetConfigFile() (models.ConfigFile, error) { + fullConfigFilePath, _, err := GetFullConfigFilePath() + if err != nil { + return models.ConfigFile{}, err + } + + configFileAsBytes, err := os.ReadFile(fullConfigFilePath) + if err != nil { + if err, ok := err.(*os.PathError); ok { + return models.ConfigFile{}, nil + } else { + return models.ConfigFile{}, err + } + } + + var configFile models.ConfigFile + err = json.Unmarshal(configFileAsBytes, &configFile) + if err != nil { + return models.ConfigFile{}, err + } + + return configFile, nil +} + +// Write a ConfigFile to disk. Raise error if unable to save the model to ask +func WriteConfigFile(configFile *models.ConfigFile) error { + fullConfigFilePath, fullConfigFileDirPath, err := GetFullConfigFilePath() + if err != nil { + return fmt.Errorf("writeConfigFile: unable to write config file because an error occurred when getting config file path [err=%s]", err) + } + + configFileMarshalled, err := json.Marshal(configFile) + if err != nil { + return fmt.Errorf("writeConfigFile: unable to write config file because an error occurred when marshalling the config file [err=%s]", err) + } + + // check if config folder exists and if not create it + if _, err := os.Stat(fullConfigFileDirPath); errors.Is(err, os.ErrNotExist) { + err := os.Mkdir(fullConfigFileDirPath, os.ModePerm) + if err != nil { + return err + } + } + + // Create file in directory + err = os.WriteFile(fullConfigFilePath, configFileMarshalled, os.ModePerm) + if err != nil { + return fmt.Errorf("writeConfigFile: Unable to write to file [err=%s]", err) + } + + if err != nil { + return fmt.Errorf("writeConfigFile: unable to write config file because an error occurred when write the config to file [err=%s]", err) + + } + + return nil +} diff --git a/cli/packages/util/credentials.go b/cli/packages/util/credentials.go index c5396f698..80c98fa9a 100644 --- a/cli/packages/util/credentials.go +++ b/cli/packages/util/credentials.go @@ -3,12 +3,11 @@ package util import ( "encoding/json" "fmt" - "os" + "github.com/99designs/keyring" "github.com/Infisical/infisical-merge/packages/models" "github.com/go-resty/resty/v2" log "github.com/sirupsen/logrus" - "github.com/zalando/go-keyring" ) const SERVICE_NAME = "infisical" @@ -17,32 +16,48 @@ const SERVICE_NAME = "infisical" func StoreUserCredsInKeyRing(userCred *models.UserCredentials) error { userCredMarshalled, err := json.Marshal(userCred) if err != nil { - return fmt.Errorf("Something went wrong when marshalling user creds:", err) + return fmt.Errorf("StoreUserCredsInKeyRing: something went wrong when marshalling user creds [err=%s]", err) } - err = keyring.Set(SERVICE_NAME, userCred.Email, string(userCredMarshalled)) + // Get keyring + configuredKeyring, err := GetKeyRing() if err != nil { - return fmt.Errorf("Unable to store user credentials:", err) + return fmt.Errorf("StoreUserCredsInKeyRing: unable to get keyring instance with [err=%s]", err) + } + + err = configuredKeyring.Set(keyring.Item{ + Key: userCred.Email, + Data: []byte(string(userCredMarshalled)), + }) + + if err != nil { + return fmt.Errorf("StoreUserCredsInKeyRing: unable to store user credentials because [err=%s]", err) } return err } func GetUserCredsFromKeyRing(userEmail string) (credentials models.UserCredentials, err error) { - credentialsString, err := keyring.Get(SERVICE_NAME, userEmail) + // Get keyring + configuredKeyring, err := GetKeyRing() if err != nil { - return models.UserCredentials{}, fmt.Errorf("Unable to get key from Keyring:", err) + return models.UserCredentials{}, fmt.Errorf("GetUserCredsFromKeyRing: unable to get keyring instance with [err=%s]", err) + } + + credentialsValue, err := configuredKeyring.Get(userEmail) + if err != nil { + return models.UserCredentials{}, fmt.Errorf("GetUserCredsFromKeyRing: unable to get key from Keyring. could not find login credentials in your Keyring. This is common if you have switched vault backend recently. If so, please login in again and retry [err=%s]", err) } var userCredentials models.UserCredentials - err = json.Unmarshal([]byte(credentialsString), &userCredentials) + err = json.Unmarshal([]byte(credentialsValue.Data), &userCredentials) if err != nil { - return models.UserCredentials{}, fmt.Errorf("Something went wrong when unmarshalling user creds:", err) + return models.UserCredentials{}, fmt.Errorf("getUserCredsFromKeyRing: Something went wrong when unmarshalling user creds [err=%s]", err) } if err != nil { - return models.UserCredentials{}, fmt.Errorf("Unable to store user credentials", err) + return models.UserCredentials{}, fmt.Errorf("GetUserCredsFromKeyRing: Unable to store user credentials [err=%s]", err) } return userCredentials, err @@ -50,23 +65,13 @@ func GetUserCredsFromKeyRing(userEmail string) (credentials models.UserCredentia func IsUserLoggedIn() (hasUserLoggedIn bool, theUsersEmail string, err error) { if ConfigFileExists() { - fullConfigFilePath, _, err := GetFullConfigFilePath() + configFile, err := GetConfigFile() if err != nil { - log.Debugln("Error gettting full path:", err) - return false, "", err + return false, "", fmt.Errorf("IsUserLoggedIn: unable to get logged in user from config file [err=%s]", err) } - configFileAsBytes, err := os.ReadFile(fullConfigFilePath) - if err != nil { - log.Debugln("Unable to read config file:", err) - return false, "", err - } - - var configFile models.ConfigFile - err = json.Unmarshal(configFileAsBytes, &configFile) - if err != nil { - log.Debugln("Unable to unmarshal config file:", err) - return false, "", err + if configFile.LoggedInUserEmail == "" { + return false, "", nil } userCreds, err := GetUserCredsFromKeyRing(configFile.LoggedInUserEmail) @@ -89,7 +94,7 @@ func IsUserLoggedIn() (hasUserLoggedIn bool, theUsersEmail string, err error) { if response.StatusCode() > 299 { log.Infoln("Login expired, please login again.") - return false, "", fmt.Errorf("Login expired, please login again.") + return false, "", fmt.Errorf("GetUserCredsFromKeyRing: Login expired, please login again.") } return true, configFile.LoggedInUserEmail, nil diff --git a/cli/packages/util/crypto.go b/cli/packages/util/crypto.go index b308ea93d..c6eee2d0c 100644 --- a/cli/packages/util/crypto.go +++ b/cli/packages/util/crypto.go @@ -3,12 +3,9 @@ package util import ( "crypto/aes" "crypto/cipher" - - log "github.com/sirupsen/logrus" ) func DecryptSymmetric(key []byte, encryptedPrivateKey []byte, tag []byte, IV []byte) ([]byte, error) { - log.Debugln("Key:", key, "encryptedPrivateKey", encryptedPrivateKey, "tag", tag, "IV", IV) block, err := aes.NewCipher(key) if err != nil { return nil, err diff --git a/cli/packages/util/secrets.go b/cli/packages/util/secrets.go index 5cf76d48e..c127111b1 100644 --- a/cli/packages/util/secrets.go +++ b/cli/packages/util/secrets.go @@ -4,6 +4,7 @@ import ( "encoding/base64" "errors" "fmt" + "os" "regexp" "strings" @@ -13,19 +14,7 @@ import ( "golang.org/x/crypto/nacl/box" ) -func GetSecretsFromAPIUsingCurrentLoggedInUser(envName string, userCreds models.UserCredentials) ([]models.SingleEnvironmentVariable, error) { - log.Debugln("envName", envName, "userCreds", userCreds) - // check if user has configured a workspace - workspace, err := GetWorkSpaceFromFile() - if err != nil { - return nil, fmt.Errorf("Unable to read workspace file:", err) - } - - // create http client - httpClient := resty.New(). - SetAuthToken(userCreds.JTWToken). - SetHeader("Accept", "application/json") - +func getSecretsByWorkspaceIdAndEnvName(httpClient resty.Client, envName string, workspace models.WorkspaceConfigFile, userCreds models.UserCredentials) (listOfSecrets []models.SingleEnvironmentVariable, err error) { var pullSecretsRequestResponse models.PullSecretsResponse response, err := httpClient. R(). @@ -34,14 +23,11 @@ func GetSecretsFromAPIUsingCurrentLoggedInUser(envName string, userCreds models. SetResult(&pullSecretsRequestResponse). Get(fmt.Sprintf("%v/v1/secret/%v", INFISICAL_URL, workspace.WorkspaceId)) // need to change workspace id - log.Debugln("Response from get secrets:", response) - if err != nil { return nil, err } if response.StatusCode() > 299 { - log.Debugln(response) return nil, fmt.Errorf(response.Status()) } @@ -66,7 +52,7 @@ func GetSecretsFromAPIUsingCurrentLoggedInUser(envName string, userCreds models. return nil, err } - log.Debugln("workspaceKey", workspaceKey, "nonce", nonce, "senderPublicKey", senderPublicKey, "currentUsersPrivateKey", currentUsersPrivateKey) + // log.Debugln("workspaceKey", workspaceKey, "nonce", nonce, "senderPublicKey", senderPublicKey, "currentUsersPrivateKey", currentUsersPrivateKey) workspaceKeyInBytes, _ := box.Open(nil, workspaceKey, (*[24]byte)(nonce), (*[32]byte)(senderPublicKey), (*[32]byte)(currentUsersPrivateKey)) var listOfEnv []models.SingleEnvironmentVariable @@ -100,6 +86,32 @@ func GetSecretsFromAPIUsingCurrentLoggedInUser(envName string, userCreds models. return listOfEnv, nil } +func GetSecretsFromAPIUsingCurrentLoggedInUser(envName string, userCreds models.UserCredentials) ([]models.SingleEnvironmentVariable, error) { + log.Debugln("GetSecretsFromAPIUsingCurrentLoggedInUser", "envName", envName, "userCreds", userCreds) + // check if user has configured a workspace + workspaces, err := GetAllWorkSpaceConfigsStartingFromCurrentPath() + if err != nil { + return nil, fmt.Errorf("Unable to read workspace file(s):", err) + } + + // create http client + httpClient := resty.New(). + SetAuthToken(userCreds.JTWToken). + SetHeader("Accept", "application/json") + + secrets := []models.SingleEnvironmentVariable{} + for _, workspace := range workspaces { + secretsFromAPI, err := getSecretsByWorkspaceIdAndEnvName(*httpClient, envName, workspace, userCreds) + if err != nil { + return nil, fmt.Errorf("GetSecretsFromAPIUsingCurrentLoggedInUser: Unable to get secrets by workspace id and env name") + } + + secrets = append(secrets, secretsFromAPI...) + } + + return secrets, nil +} + func GetSecretsFromAPIUsingInfisicalToken(infisicalToken string, envName string, projectId string) ([]models.SingleEnvironmentVariable, error) { if infisicalToken == "" || projectId == "" || envName == "" { return nil, errors.New("infisical token, project id and or environment name cannot be empty") @@ -126,7 +138,6 @@ func GetSecretsFromAPIUsingInfisicalToken(infisicalToken string, envName string, } if response.StatusCode() > 299 { - log.Debugln(response) return nil, fmt.Errorf(response.Status()) } @@ -184,6 +195,60 @@ func GetSecretsFromAPIUsingInfisicalToken(infisicalToken string, envName string, return listOfEnv, nil } +func GetAllEnvironmentVariables(projectId string, envName string) ([]models.SingleEnvironmentVariable, error) { + infisicalToken := os.Getenv(INFISICAL_TOKEN_NAME) + + if infisicalToken == "" { + hasUserLoggedInbefore, loggedInUserEmail, err := IsUserLoggedIn() + if err != nil { + log.Info("Unexpected issue occurred while checking login status. To see more details, add flag --debug") + log.Debugln(err) + return nil, err + } + + if !hasUserLoggedInbefore { + log.Infoln("No logged in user. To login, please run command [infisical login]") + return nil, fmt.Errorf("user not logged in") + } + + userCreds, err := GetUserCredsFromKeyRing(loggedInUserEmail) + if err != nil { + log.Infoln("Unable to get user creds from key ring") + log.Debug(err) + return nil, err + } + + workspaceConfigs, err := GetAllWorkSpaceConfigsStartingFromCurrentPath() + if err != nil { + return nil, fmt.Errorf("unable to check if you have a %s file in your current directory", INFISICAL_WORKSPACE_CONFIG_FILE_NAME) + } + + if len(workspaceConfigs) == 0 { + log.Infoln("Your local project is not connected to a Infisical project yet. Run command [infisical init]") + return nil, fmt.Errorf("project not initialized") + } + + envsFromApi, err := GetSecretsFromAPIUsingCurrentLoggedInUser(envName, userCreds) + if err != nil { + log.Errorln("Something went wrong when pulling secrets using your logged in credentials. If the issue persists, double check your project id/try logging in again.") + log.Debugln(err) + return nil, err + } + + return envsFromApi, nil + + } else { + envsFromApi, err := GetSecretsFromAPIUsingInfisicalToken(infisicalToken, envName, projectId) + if err != nil { + log.Errorln("Something went wrong when pulling secrets using your Infisical token. Double check the token, project id or environment name (dev, prod, ect.)") + log.Debugln(err) + return nil, err + } + + return envsFromApi, nil + } +} + func GetWorkSpacesFromAPI(userCreds models.UserCredentials) (workspaces []models.Workspace, err error) { // create http client httpClient := resty.New(). diff --git a/cli/packages/util/vault.go b/cli/packages/util/vault.go new file mode 100644 index 000000000..03d03b360 --- /dev/null +++ b/cli/packages/util/vault.go @@ -0,0 +1,66 @@ +package util + +import ( + "fmt" + "os" + + "github.com/99designs/keyring" + "golang.org/x/term" +) + +func GetCurrentVaultBackend() (keyring.BackendType, error) { + configFile, err := GetConfigFile() + if err != nil { + return "", fmt.Errorf("getCurrentVaultBackend: unable to get config file [err=%s]", err) + } + + if configFile.VaultBackendType == "" { + return keyring.AvailableBackends()[0], nil + } + + return configFile.VaultBackendType, nil +} + +func GetKeyRing() (keyring.Keyring, error) { + currentVaultBackend, err := GetCurrentVaultBackend() + if err != nil { + return nil, fmt.Errorf("GetKeyRing: unable to get the current vault backend, [err=%s]", err) + } + + keyringInstanceConfig := keyring.Config{ + FilePasswordFunc: fileKeyringPassphrasePrompt, + ServiceName: SERVICE_NAME, + LibSecretCollectionName: SERVICE_NAME, + KWalletAppID: SERVICE_NAME, + KWalletFolder: SERVICE_NAME, + KeychainTrustApplication: true, + WinCredPrefix: SERVICE_NAME, + FileDir: fmt.Sprintf("~/%s-file-vault", SERVICE_NAME), + KeychainAccessibleWhenUnlocked: true, + } + + // if the user explicitly sets a vault backend, then only use that + if currentVaultBackend != "" { + keyringInstanceConfig.AllowedBackends = []keyring.BackendType{keyring.BackendType(currentVaultBackend)} + } + + keyringInstance, err := keyring.Open(keyringInstanceConfig) + if err != nil { + return nil, fmt.Errorf("GetKeyRing: Unable to create instance of Keyring because of [err=%s]", err) + } + + return keyringInstance, nil +} + +func fileKeyringPassphrasePrompt(prompt string) (string, error) { + if password, ok := os.LookupEnv("INFISICAL_VAULT_FILE_PASSPHRASE"); ok { + return password, nil + } + + fmt.Fprintf(os.Stderr, "%s: ", prompt) + b, err := term.ReadPassword(int(os.Stdin.Fd())) + if err != nil { + return "", err + } + return string(b), nil +} diff --git a/cli/packages/visualize/visualize.go b/cli/packages/visualize/visualize.go new file mode 100644 index 000000000..85d946a32 --- /dev/null +++ b/cli/packages/visualize/visualize.go @@ -0,0 +1,35 @@ +package visualize + +import ( + "os" + + "github.com/jedib0t/go-pretty/table" +) + +// Given headers and rows, this function will print out a table +func Table(headers []string, rows [][]string) { + t := table.NewWriter() + t.SetOutputMirror(os.Stdout) + t.SetStyle(table.StyleLight) + + // t.SetTitle("Title") + t.Style().Options.DrawBorder = true + t.Style().Options.SeparateHeader = true + t.Style().Options.SeparateColumns = true + + tableHeaders := table.Row{} + for _, header := range headers { + tableHeaders = append(tableHeaders, header) + } + + t.AppendHeader(tableHeaders) + for _, row := range rows { + tableRow := table.Row{} + for _, val := range row { + tableRow = append(tableRow, val) + } + t.AppendRow(tableRow) + } + + t.Render() +} diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index b9da96715..6cf75ad47 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -20,10 +20,10 @@ services: restart: unless-stopped depends_on: - mongo + - smtp-server build: context: ./backend dockerfile: Dockerfile - image: infisical/backend volumes: - ./backend/src:/app/src - ./backend/nodemon.json:/app/nodemon.json @@ -34,7 +34,7 @@ services: - NODE_ENV=development networks: - infisical-dev - + frontend: container_name: infisical-dev-frontend restart: unless-stopped @@ -43,7 +43,6 @@ services: build: context: ./frontend dockerfile: Dockerfile.dev - image: infisical/frontend volumes: - ./frontend/pages:/app/pages - ./frontend/public:/app/public @@ -54,12 +53,9 @@ services: env_file: .env environment: - NEXT_PUBLIC_ENV=development - - NEXT_PUBLIC_WEBSITE_URL=${SITE_URL} - - NEXT_PUBLIC_POSTHOG_HOST=${POSTHOG_HOST} - - NEXT_PUBLIC_POSTHOG_API_KEY=${POSTHOG_PROJECT_API_KEY} + - INFISICAL_TELEMETRY_ENABLED=${TELEMETRY_ENABLED} - NEXT_PUBLIC_STRIPE_PRODUCT_PRO=${STRIPE_PRODUCT_PRO} - NEXT_PUBLIC_STRIPE_PRODUCT_STARTER=${STRIPE_PRODUCT_STARTER} - - NEXT_PUBLIC_TELEMETRY_ENABLED=${TELEMETRY_ENABLED} networks: - infisical-dev @@ -80,6 +76,8 @@ services: container_name: infisical-dev-mongo-express image: mongo-express restart: always + depends_on: + - mongo env_file: .env environment: - ME_CONFIG_MONGODB_ADMINUSERNAME=${MONGO_USERNAME} @@ -90,6 +88,18 @@ services: networks: - infisical-dev + smtp-server: + container_name: infisical-dev-smtp-server + image: mailhog/mailhog + restart: always + logging: + driver: 'none' # disable saving logs + ports: + - 1025:1025 # SMTP server + - 8025:8025 # Web UI + networks: + - infisical-dev + volumes: mongo-data: driver: local diff --git a/docker-compose.yml b/docker-compose.yml index 3204f9257..bd9022cef 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,16 +15,12 @@ services: - backend networks: - infisical - + backend: - platform: linux/amd64 container_name: infisical-backend restart: unless-stopped depends_on: - mongo - build: - context: ./backend - dockerfile: Dockerfile image: infisical/backend command: npm run start env_file: .env @@ -32,26 +28,19 @@ services: - NODE_ENV=production networks: - infisical - + frontend: - platform: linux/amd64 container_name: infisical-frontend restart: unless-stopped depends_on: - backend - build: - context: ./frontend - dockerfile: Dockerfile.prod image: infisical/frontend env_file: .env environment: - - NEXT_PUBLIC_ENV=production - - NEXT_PUBLIC_WEBSITE_URL=${SITE_URL} - - NEXT_PUBLIC_POSTHOG_HOST=${POSTHOG_HOST} - - NEXT_PUBLIC_POSTHOG_API_KEY=${POSTHOG_PROJECT_API_KEY} + # - NEXT_PUBLIC_POSTHOG_API_KEY=${POSTHOG_PROJECT_API_KEY} + - INFISICAL_TELEMETRY_ENABLED=${TELEMETRY_ENABLED} - NEXT_PUBLIC_STRIPE_PRODUCT_PRO=${STRIPE_PRODUCT_PRO} - NEXT_PUBLIC_STRIPE_PRODUCT_STARTER=${STRIPE_PRODUCT_STARTER} - - NEXT_PUBLIC_TELEMETRY_ENABLED=${TELEMETRY_ENABLED} networks: - infisical @@ -73,4 +62,4 @@ volumes: driver: local networks: - infisical: \ No newline at end of file + infisical: diff --git a/docs/cli/reference/commands.mdx b/docs/cli/commands/commands.mdx similarity index 91% rename from docs/cli/reference/commands.mdx rename to docs/cli/commands/commands.mdx index acefdfa7e..7c1deeb1b 100644 --- a/docs/cli/reference/commands.mdx +++ b/docs/cli/commands/commands.mdx @@ -9,7 +9,7 @@ title: "Commands" | `login` | Used to authenticate and set the logged in user. | | `init` | Used to link a local project to the platform. | | `run` | Used to inject envars from the platform into an application process. | - +| `vault` | Used to manage where your login credentials are stored at rest | ## Global options | Option | Description | diff --git a/docs/cli/commands/export.mdx b/docs/cli/commands/export.mdx new file mode 100644 index 000000000..b80fb7470 --- /dev/null +++ b/docs/cli/commands/export.mdx @@ -0,0 +1,36 @@ +--- +title: "infisical export" +--- + +```bash +infisical export [options] +``` + +## Description + +Export environment variables from the platform into a file format. + +## Options + +| Option | Description | Default value | +| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | +| `--env` | Used to set the environment that secrets are pulled from. Accepted values: `dev`, `staging`, `test`, `prod` | `dev` | +| `--projectId` | Only required if injecting via the [service token method](../token). If you are not using service token, the project id will be automatically retrieved from the `.infisical.json` located at the root of your local project. | `None` | +| `--expand` | Parse shell parameter expansions in your secrets (e.g., `${DOMAIN}`) | `true` | +| `--format` | Format of the output file. Accepted values: `dotenv`, `csv` and `json` | `dotenv` | + +## Examples + +```bash +# Export variables to a .env file +infisical export > .env + +# Export variables to a CSV file +infisical export --format=csv > secrets.csv + +# Export variables to a JSON file +infisical export --format=json > secrets.json + +# Export variables to a YAML file +infisical export --format=yaml > secrets.yaml +``` diff --git a/docs/cli/reference/init.mdx b/docs/cli/commands/init.mdx similarity index 100% rename from docs/cli/reference/init.mdx rename to docs/cli/commands/init.mdx diff --git a/docs/cli/commands/login.mdx b/docs/cli/commands/login.mdx new file mode 100644 index 000000000..de004c97f --- /dev/null +++ b/docs/cli/commands/login.mdx @@ -0,0 +1,11 @@ +--- +title: "infisical login" +--- + +```bash +infisical login +``` + +## Description +The CLI uses authentication to verify your identity. When you enter the correct email and password for your account, a token is generated and saved in your system Keyring to allow you to make future interactions with the CLI. +If you want to change where the login credentials are stored, visit the [vaults command](./vault) \ No newline at end of file diff --git a/docs/cli/reference/run.mdx b/docs/cli/commands/run.mdx similarity index 58% rename from docs/cli/reference/run.mdx rename to docs/cli/commands/run.mdx index 7fb207612..2c65ef53e 100644 --- a/docs/cli/reference/run.mdx +++ b/docs/cli/commands/run.mdx @@ -2,9 +2,25 @@ title: "infisical run" --- -```bash -infisical run [options] -- [your application start command] -``` + + + ```bash + infisical run [options] -- [your application start command] + + # Example + infisical run [options] -- npm run dev + ``` + + + + ```bash + infisical run [options] --command [string command] + + # Example + infisical run [options] --command "npm run bootstrap && npm run dev start; other-bash-command" + ``` + + ## Description @@ -15,5 +31,6 @@ Inject environment variables from the platform into an application process. | Option | Description | Default value | | -------------- | ----------------------------------------------------------------------------------------------------------- | ------------- | | `--env` | Used to set the environment that secrets are pulled from. Accepted values: `dev`, `staging`, `test`, `prod` | `dev` | -| `--projectId` | Used to link a local project to the platform (required only if injecting via the service token method) | `None` | +| `--projectId` | Used to link a local project to the platform (required only if injecting via the service token method) | None | | `--expand` | Parse shell parameter expansions in your secrets (e.g., `${DOMAIN}`) | `true` | +| `--command` | Pass secrets into chained commands (e.g., `"first-command && second-command; more-commands..."`) | None | diff --git a/docs/cli/commands/vault.mdx b/docs/cli/commands/vault.mdx new file mode 100644 index 000000000..f973cb727 --- /dev/null +++ b/docs/cli/commands/vault.mdx @@ -0,0 +1,50 @@ +--- +title: "infisical vault" +--- + + + + ```bash + infisical vault + + # Example output + The following vaults are available on your system: + - keychain + - pass + - file + + You are currently using [keychain] vault to store your login credentials + ``` + + + + ```bash + infisical vault set + + # Example + infisical vault set keychain + ``` + + + + +## Description + +To ensure secure storage of your login credentials when using the CLI, Infisical stores login credentials securely in a system vault or encrypted text file with a passphrase known only by the user. + + + By default, the most appropriate vault is chosen to store your login credentials. + For example, if you are on macOS, KeyChain will be automatically selected. + +- [macOS Keychain](https://support.apple.com/en-au/guide/keychain-access/welcome/mac) +- [Windows Credential Manager](https://support.microsoft.com/en-au/help/4026814/windows-accessing-credential-manager) +- Secret Service ([Gnome Keyring](https://wiki.gnome.org/Projects/GnomeKeyring), [KWallet](https://kde.org/applications/system/org.kde.kwalletmanager5)) +- [KWallet](https://kde.org/applications/system/org.kde.kwalletmanager5) +- [Pass](https://www.passwordstore.org/) +- [KeyCtl]() +- Encrypted file (JWT) + + +To avoid constantly entering your passphrase when using the `file` vault type, set the `INFISICAL_VAULT_FILE_PASSPHRASE` environment variable with your password in your shell + + diff --git a/docs/cli/faq.mdx b/docs/cli/faq.mdx new file mode 100644 index 000000000..600386e81 --- /dev/null +++ b/docs/cli/faq.mdx @@ -0,0 +1,15 @@ +--- +title: "FAQ" +--- + +Frequently asked questions about the CLI can be found on this page. +If you can't find the answer you're looking for, please create an issue on our GitHub repository or join our Slack channel for additional support. + + +By default, the CLI will choose the most suitable store available on your system. +If you experience issues with the default store, you can switch to a different one. +If none of the available stores work for you, you can try using the `file` store type by running `infisical vault set file`, which should work in most cases. +If you are still experiencing trouble, please seek support. + +[Learn more about vault command](./commands/vault) + \ No newline at end of file diff --git a/docs/cli/overview.mdx b/docs/cli/overview.mdx index 8411b8e75..144259023 100644 --- a/docs/cli/overview.mdx +++ b/docs/cli/overview.mdx @@ -1,5 +1,5 @@ --- -title: "Overview" +title: 'Install' --- Prerequisite: Set up an account with [Infisical Cloud](https://app.infisical.com) or via a [self-hosted installation](/self-hosting/overview). @@ -13,11 +13,7 @@ The Infisical CLI provides a way to inject environment variables from the platfo Use [brew](https://brew.sh/) package manager ```bash - # install brew install infisical/get-cli/infisical - - # check version - infisical --version ``` ## Updates @@ -31,14 +27,13 @@ The Infisical CLI provides a way to inject environment variables from the platfo Use [Scoop](https://scoop.sh/) package manager ```bash - # install scoop bucket add org https://github.com/Infisical/scoop-infisical.git - scoop install infisical - - # check version - infisical --version ``` + ```bash + scoop install infisical + ``` + ## Updates ```bash @@ -49,33 +44,33 @@ The Infisical CLI provides a way to inject environment variables from the platfo Install prerequisite ```bash - $ sudo apk add --no-cache bash sudo + sudo apk add --no-cache bash sudo ``` Add Infisical repository ```bash - $ curl -1sLf \ + curl -1sLf \ 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' \ | sudo -E bash ``` Then install CLI ```bash - $ sudo apk update && sudo apk add infisical + sudo apk update && sudo apk add infisical ``` Add Infisical repository ```bash - $ curl -1sLf \ + curl -1sLf \ 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.rpm.sh' \ | sudo -E bash ``` Then install CLI ```bash - $ sudo yum install infisical + sudo yum install infisical ``` @@ -83,14 +78,14 @@ The Infisical CLI provides a way to inject environment variables from the platfo Add Infisical repository ```bash - $ curl -1sLf \ + curl -1sLf \ 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' \ | sudo -E bash ``` Then install CLI ```bash - $ sudo apt-get update && sudo apt-get install -y infisical + sudo apt-get update && sudo apt-get install -y infisical ``` diff --git a/docs/cli/reference/login.mdx b/docs/cli/reference/login.mdx deleted file mode 100644 index 32c1bedba..000000000 --- a/docs/cli/reference/login.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: "infisical login" ---- - -```bash -infisical login -``` - -## Description - -Verify a user and save credentials to the system keyring. - -To change the logged in user, run the command again to overwrite the previous login. diff --git a/docs/cli/usage.mdx b/docs/cli/usage.mdx index 4237f8154..aac5c1b74 100644 --- a/docs/cli/usage.mdx +++ b/docs/cli/usage.mdx @@ -4,10 +4,27 @@ title: "Usage" Prerequisite: [Install the CLI](/cli/overview) +## Authenticate + + + To use the Infisical CLI in your development environment, you can run the command below. + This will allow you to access the features and functionality provided by the CLI. + + ```bash + infisical login + ``` + + + + To use Infisical CLI in environments where you cannot run the `infisical login` command, you can authenticate via a + Infisical Token instead. Learn more about [Infisical Token](../getting-started/dashboard/token). + + + ## Initialize Infisical for your project ```bash -# move to your project +# navigate to your project cd /path/to/project # initialize infisical diff --git a/docs/contributing/FAQ.mdx b/docs/contributing/FAQ.mdx deleted file mode 100644 index be65f5ca7..000000000 --- a/docs/contributing/FAQ.mdx +++ /dev/null @@ -1,16 +0,0 @@ ---- -title: "Frequently Asked Questions" -description: "Have any questions? [Join our Slack community](https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g)." ---- - -## Problem with SMTP - -You can normally populate `SMTP_USERNAME` and `SMTP_PASSWORD` with your usual login and password (you could also create a 'burner' email). Sometimes, there still are problems. - -You can go to your Gmail account settings > security and enable โ€œless secure appsโ€. This would allow Infisical to use your Gmail to send emails. - -If it still doesn't work, [this](https://stackoverflow.com/questions/72547853/unable-to-send-email-in-c-sharp-less-secure-app-access-not-longer-available/72553362#72553362) should help. - -## `MONGO_URL` issues - -Your `MONGO_URL` should be something like `mongodb://root:example@mongo:27017/?authSource=admin`. If you want to change it (not recommended), you should make sure that you keep this URL in line with `MONGO_USERNAME=root` and `MONGO_PASSWORD=example`. \ No newline at end of file diff --git a/docs/contributing/developing.mdx b/docs/contributing/developing.mdx index b6c25861c..423b83da2 100644 --- a/docs/contributing/developing.mdx +++ b/docs/contributing/developing.mdx @@ -1,6 +1,6 @@ --- -title: "Developing" -description: "This guide will help you set up and run Infisical in development mode." +title: 'Developing' +description: 'This guide will help you set up and run Infisical in development mode.' --- ## Clone the repo @@ -16,17 +16,50 @@ cd infisical ## Set up environment variables -Tweak the `.env` according to your preferences. Refer to the available [environment variables](/self-hosting/configuration/envars). +Start by creating a .env file at the root of the Infisical directory. It's best to start with the provided [`.env.example`](https://github.com/Infisical/infisical/blob/main/.env.example) template containing the necessary envars to fill out your .env file โ€” you only have to modify the SMTP parameters. + + + The pre-populated environment variable values in the `.env.example` file are meant to be used in development only. + You'll want to fill in your own values in production, especially concerning encryption keys, secrets, and SMTP parameters. + + +Refer to the [environment variable list](https://infisical.com/docs/self-hosting/configuration/envars) for guidance on each envar. + +### Helpful tips for developing with Infisical: + + +Use the `ENCRYPTION_KEY`, JWT-secret envars, `MONGO_URL`, `MONGO_USERNAME`, `MONGO_PASSWORD` provided in the `.env.example` file. + +If setting your own values: + +- `ENCRYPTION_KEY` should be a [32-byte random hex](https://www.browserling.com/tools/random-hex) +- `MONGO_URL` should take the form: `mongodb://[MONGO_USERNAME]:[MONGO_PASSWORD]@mongo:27017/?authSource=admin`. + + + +Bring and configure your own SMTP server by following our [email configuration guide](https://infisical.com/docs/self-hosting/configuration/email) (we recommend using either SendGrid or Mailgun). + +Alternatively, you can use the provided development (Mailhog) SMTP server to send and browse emails sent by the backend on http://localhost:8025; to use this option, set the following `SMTP_HOST`, `SMTP_PORT`, `SMTP_FROM_NAME`, `SMTP_USERNAME`, `SMTP_PASSWORD` below. + -```bash -cp .env.example .env ``` +SMTP_HOST=smtp-server +SMTP_PORT=1025 +SMTP_FROM_ADDRESS=team@infisical.com +SMTP_FROM_NAME=Infisical +SMTP_USERNAME=team@infisical.com +SMTP_PASSWORD= +``` + + + If using Mailhog, make sure to leave the `SMTP_PASSWORD` blank so the backend can connect to MailHog. + ## Docker for development ```bash # build and start the services -docker-compose -f docker-compose.dev.yml up --build +docker-compose -f docker-compose.dev.yml up --build --force-recreate ``` Then browse http://localhost:8080 @@ -36,12 +69,4 @@ Then browse http://localhost:8080 docker-compose -f docker-compose.dev.yml down # start services docker-compose -f docker-compose.dev.yml up -``` - -The docker-compose development environment consists of: - -- nginx -- frontend -- backend -- mongo -- mongo-express +``` \ No newline at end of file diff --git a/docs/getting-started/dashboard/integrations.mdx b/docs/getting-started/dashboard/integrations.mdx index 95541ca38..de25fa861 100644 --- a/docs/getting-started/dashboard/integrations.mdx +++ b/docs/getting-started/dashboard/integrations.mdx @@ -4,8 +4,11 @@ title: "Integrations" Integrations allow environment variables to be synced across your entire infrastructure from local development to CI/CD and production. -We're still early with integrations, but expect more soon. +We're still early with integrations, but expect more soon. + + + View all available integrations and their guide + ![integrations](../../images/project-integrations.png) -Check out our [integrations](/integrations/overview). diff --git a/docs/getting-started/dashboard/token.mdx b/docs/getting-started/dashboard/token.mdx index 85f46b799..455a57929 100644 --- a/docs/getting-started/dashboard/token.mdx +++ b/docs/getting-started/dashboard/token.mdx @@ -4,13 +4,22 @@ title: "Infisical Token" An Infisical Token is needed to authenticate the CLI when there isn't an easy way to input your login credentials. -It's useful for the [Docker](/integrations/platforms/docker) and [Docker Compose](/integrations/platforms/docker-compose) integrations. +It's useful for your CI/CD environments and integrations such as [Docker](/integrations/platforms/docker) and [Docker Compose](/integrations/platforms/docker-compose). + +To generate the the token, head over to your project settings as shown below. -It's possible to generate the token in the settings of a project. ![token add](../../images/project-token-add.png) +## Feeding Infisical Token to the CLI + +The Infisical CLI checks for the presence of an environment variable called `INFISICAL_TOKEN`. +If it detects this variable in the terminal where it is being run, it will use it to authenticate and retrieve the environment variables that the token is authorized to access. +This allows you to use the CLI in environments where you are unable to run the `infisical login` command. + The token grants read-only access to a particular environment and project for - a specified amount of time. + a specified amount of time. Once the token is expired, the CLI using it will no longer be able to make + requests with it. + diff --git a/docs/getting-started/quickstart.mdx b/docs/getting-started/quickstart.mdx index 307127ab2..3c559cdcb 100644 --- a/docs/getting-started/quickstart.mdx +++ b/docs/getting-started/quickstart.mdx @@ -8,7 +8,7 @@ Note that the Infisical CLI is platform-agnostic and can inject environment vari ## Set up Infisical Cloud -1. Login or create an accout at `app.infisical.com`. +1. Login or create an account at `app.infisical.com`. 2. Create a new project. 3. Populate your environment variables as in the image below. diff --git a/docs/images/email-mailhog-credentials.png b/docs/images/email-mailhog-credentials.png new file mode 100644 index 000000000..8d5a11295 Binary files /dev/null and b/docs/images/email-mailhog-credentials.png differ diff --git a/docs/images/email-sendgrid-create-key.png b/docs/images/email-sendgrid-create-key.png new file mode 100644 index 000000000..1caa977a8 Binary files /dev/null and b/docs/images/email-sendgrid-create-key.png differ diff --git a/docs/images/email-sendgrid-restrictions.png b/docs/images/email-sendgrid-restrictions.png new file mode 100644 index 000000000..a70891a60 Binary files /dev/null and b/docs/images/email-sendgrid-restrictions.png differ diff --git a/docs/images/integrations-github-auth.png b/docs/images/integrations-github-auth.png new file mode 100644 index 000000000..92d7158ac Binary files /dev/null and b/docs/images/integrations-github-auth.png differ diff --git a/docs/images/integrations-github.png b/docs/images/integrations-github.png new file mode 100644 index 000000000..d34ea2690 Binary files /dev/null and b/docs/images/integrations-github.png differ diff --git a/docs/images/integrations-heroku-auth.png b/docs/images/integrations-heroku-auth.png new file mode 100644 index 000000000..da9b4cf52 Binary files /dev/null and b/docs/images/integrations-heroku-auth.png differ diff --git a/docs/images/integrations-heroku.png b/docs/images/integrations-heroku.png new file mode 100644 index 000000000..cc225f286 Binary files /dev/null and b/docs/images/integrations-heroku.png differ diff --git a/docs/images/integrations-netlify-auth.png b/docs/images/integrations-netlify-auth.png new file mode 100644 index 000000000..fe25d7acf Binary files /dev/null and b/docs/images/integrations-netlify-auth.png differ diff --git a/docs/images/integrations-netlify.png b/docs/images/integrations-netlify.png new file mode 100644 index 000000000..60261043e Binary files /dev/null and b/docs/images/integrations-netlify.png differ diff --git a/docs/images/integrations-vercel-auth.png b/docs/images/integrations-vercel-auth.png new file mode 100644 index 000000000..d8f3d2d18 Binary files /dev/null and b/docs/images/integrations-vercel-auth.png differ diff --git a/docs/images/integrations-vercel.png b/docs/images/integrations-vercel.png new file mode 100644 index 000000000..f3a814c7a Binary files /dev/null and b/docs/images/integrations-vercel.png differ diff --git a/docs/images/integrations.png b/docs/images/integrations.png new file mode 100644 index 000000000..7359aa198 Binary files /dev/null and b/docs/images/integrations.png differ diff --git a/docs/images/project-integrations.png b/docs/images/project-integrations.png index 90f50a8c4..7c2c35bd4 100644 Binary files a/docs/images/project-integrations.png and b/docs/images/project-integrations.png differ diff --git a/docs/integrations/cicd/circleci.mdx b/docs/integrations/cicd/circleci.mdx new file mode 100644 index 000000000..7ded52d8a --- /dev/null +++ b/docs/integrations/cicd/circleci.mdx @@ -0,0 +1,5 @@ +--- +title: "Circle CI" +--- + +Coming soon. diff --git a/docs/integrations/cicd/githubactions.mdx b/docs/integrations/cicd/githubactions.mdx new file mode 100644 index 000000000..23dbc419e --- /dev/null +++ b/docs/integrations/cicd/githubactions.mdx @@ -0,0 +1,34 @@ +--- +title: "GitHub Actions" +--- + + + Infisical can sync secrets to GitHub repo secrets only. If your repo uses environment secrets, then stay tuned with this [issue](https://github.com/Infisical/infisical/issues/54). + + +Prerequisites: + +- Set up and add envars to [Infisical Cloud](https://app.infisical.com) +- Ensure you have admin privileges to the repo you want to sync secrets to. + +## Navigate to your project's integrations tab + +![integrations](../../images/integrations.png) + +## Authorize Infisical for GitHub + +Press on the GitHub tile and grant Infisical access to your GitHub account (repo privileges only). + +![integrations github authorization](../../images/integrations-github-auth.png) + + + If this is your project's first cloud integration, then you'll have to grant Infisical access to your project's environment variables. + Although this step breaks E2EE, it's necessary for Infisical to sync the environment variables to the cloud platform. + + +## Start integration + +Select which Infisical environment secrets you want to sync to which GitHub repo and press start integration to start syncing secrets to the repo. + +![integrations github](../../images/integrations-github.png) + diff --git a/docs/integrations/cloud/flyio.mdx b/docs/integrations/cloud/flyio.mdx new file mode 100644 index 000000000..b53a52404 --- /dev/null +++ b/docs/integrations/cloud/flyio.mdx @@ -0,0 +1,5 @@ +--- +title: "Fly.io" +--- + +Coming soon. diff --git a/docs/integrations/cloud/heroku.mdx b/docs/integrations/cloud/heroku.mdx index 5f0debd3e..e16cd0d54 100644 --- a/docs/integrations/cloud/heroku.mdx +++ b/docs/integrations/cloud/heroku.mdx @@ -1,26 +1,29 @@ --- title: "Heroku" -description: "With this integration, you can automatically sync your secrets to Heroku as soon as you update secrets in Infisical." --- -## Instructions +Prerequisites: -### Step 1: Open the integrations console +- Set up and add envars to [Infisical Cloud](https://app.infisical.com) -Open the Infisical Dashboard. Choose the project in which you want to set up the intergation. Go to the integrations tab in the left sidebar. +## Navigate to your project's integrations tab -### Step 2: Authenticate with Heroku +![integrations](../../images/integrations.png) -Click on "Heroku" tile. Log in if required and provide the necessary permissions to Infisical. You will afterwards be redirected back to the integrations page. +## Authorize Infisical for Heroku -Note: during an integration with Heroku, for security reasons, it is impossible to maintain end-to-end encryption. In theory, this lets Infisical decrypt yor environment variables. In practice, we can assure you that this will never be done, and it allows us to protect your secrets from bad actors online. With any questions, reach out support@infisical.com. +Press on the Heroku tile and grant Infisical access to your Heroku account. -### Step 3: Start integration +![integrations heroku authorization](../../images/integrations-heroku-auth.png) -Choose a Heroku App that you want to sync the secrets to, and the Infisical project environment that you want to sync the secrets from. Start the integration. - -The integration should now show status 'In Sync'. Every time you edit secrets, they will be automatically pushed to Heroku. - - - If you need to update your integration, you will have to delete the current one and create a new one. + + If this is your project's first cloud integration, then you'll have to grant Infisical access to your project's environment variables. + Although this step breaks E2EE, it's necessary for Infisical to sync the environment variables to the cloud platform. + +## Start integration + +Select which Infisical environment secrets you want to sync to which Heroku app and press start integration to start syncing secrets to Heroku. + +![integrations heroku](../../images/integrations-heroku.png) + diff --git a/docs/integrations/cloud/netlify.mdx b/docs/integrations/cloud/netlify.mdx new file mode 100644 index 000000000..e78f01368 --- /dev/null +++ b/docs/integrations/cloud/netlify.mdx @@ -0,0 +1,32 @@ +--- +title: "Netlify" +--- + + + Infisical integrates with Netlify's new environment variable experience. If your site uses Netlify's old environment variable experience, you'll have to upgrade it to the new one to use this integration. + + +Prerequisites: + +- Set up and add envars to [Infisical Cloud](https://app.infisical.com) + +## Navigate to your project's integrations tab + +![integrations](../../images/integrations.png) + +## Authorize Infisical for Netlify + +Press on the Netlify tile and grant Infisical access to your Netlify account. + +![integrations netlify authorization](../../images/integrations-netlify-auth.png) + + + If this is your project's first cloud integration, then you'll have to grant Infisical access to your project's environment variables. + Although this step breaks E2EE, it's necessary for Infisical to sync the environment variables to the cloud platform. + + +## Start integration + +Select which Infisical environment secrets you want to sync to which Netlify app and context. Lastly, press start integration to start syncing secrets to Netlify. + +![integrations netlify](../../images/integrations-netlify.png) \ No newline at end of file diff --git a/docs/integrations/cloud/render.mdx b/docs/integrations/cloud/render.mdx new file mode 100644 index 000000000..895bf01d1 --- /dev/null +++ b/docs/integrations/cloud/render.mdx @@ -0,0 +1,5 @@ +--- +title: "Render" +--- + +Coming soon. diff --git a/docs/integrations/cloud/vercel.mdx b/docs/integrations/cloud/vercel.mdx new file mode 100644 index 000000000..59b416c44 --- /dev/null +++ b/docs/integrations/cloud/vercel.mdx @@ -0,0 +1,23 @@ +--- +title: "Vercel" +--- + +Prerequisites: + +- Set up and add envars to [Infisical Cloud](https://app.infisical.com) + +## Navigate to your project's integrations tab + +![integrations](../../images/integrations.png) + +## Authorize Infisical for Vercel + +Press on the Vercel tile and grant Infisical access to your Vercel account. + +![integrations vercel authorization](../../images/integrations-vercel-auth.png) + +## Start integration + +Select which Infisical environment secrets you want to sync to which Vercel app and environment. Lastly, press start integration to start syncing secrets to Vercel. + +![integrations vercel](../../images/integrations-vercel.png) \ No newline at end of file diff --git a/docs/integrations/frameworks/django.mdx b/docs/integrations/frameworks/django.mdx index 0fa785dcc..ea786a0f7 100644 --- a/docs/integrations/frameworks/django.mdx +++ b/docs/integrations/frameworks/django.mdx @@ -10,15 +10,18 @@ Prerequisites: ## Initialize Infisical for your [Django](https://www.djangoproject.com) project ```bash -# move to your Django project +# navigate to the root of your of your project cd /path/to/project -# initialize infisical +# then initialize Infisical infisical init ``` -## Start your server with environment variables injected +## Start your application as usual but with Infisical ```bash +infisical run -- + +# Example infisical run -- python manage.py runserver ``` diff --git a/docs/integrations/frameworks/express.mdx b/docs/integrations/frameworks/express.mdx index c681babb3..363decf09 100644 --- a/docs/integrations/frameworks/express.mdx +++ b/docs/integrations/frameworks/express.mdx @@ -16,30 +16,18 @@ The steps apply to the following non-exhaustive list of frameworks: ## Initialize Infisical for your app ```bash -# move to your app +# navigate to the root of your of your project cd /path/to/project -# initialize infisical +# then initialize Infisical infisical init ``` -## Modify the start script in your `package.json` - -```json -... -"scripts": { - "start": "infisical run -- node index.js" - "dev": "infisical run -- nodemon index.js" // if using nodemon for dev -} -... -``` - -## Start your server with environment variables injected +## Start your application as usual but with Infisical ```bash -npm run start +infisical run -- -# or start development server - -npm run dev +# Example +infisical run -- npm run dev ``` diff --git a/docs/integrations/frameworks/fiber.mdx b/docs/integrations/frameworks/fiber.mdx index ed022c644..0c7d6aab2 100644 --- a/docs/integrations/frameworks/fiber.mdx +++ b/docs/integrations/frameworks/fiber.mdx @@ -7,4 +7,21 @@ Prerequisites: - Set up and add envars to [Infisical Cloud](https://app.infisical.com) - [Install the CLI](/cli/overview) -Coming soon. +## Initialize Infisical for your [Fiber](https://gofiber.io/) app + +```bash +# navigate to the root of your of your project +cd /path/to/project + +# then initialize Infisical +infisical init +``` + +## Start your application as usual but with Infisical + +```bash +infisical run -- + +# Example +infisical run -- go run server.go +``` \ No newline at end of file diff --git a/docs/integrations/frameworks/flask.mdx b/docs/integrations/frameworks/flask.mdx index 941b62b2e..e80a993e8 100644 --- a/docs/integrations/frameworks/flask.mdx +++ b/docs/integrations/frameworks/flask.mdx @@ -10,15 +10,18 @@ Prerequisites: ## Initialize Infisical for your [Flask](https://flask.palletsprojects.com/en/2.2.x) app ```bash -# move to your Flask app +# navigate to the root of your of your project cd /path/to/project -# initialize infisical +# then initialize Infisical infisical init ``` -## Start your server with environment variables injected +## Start your application as usual but with Infisical ```bash +infisical run -- + +# Example infisical run -- flask run ``` diff --git a/docs/integrations/frameworks/gatsby.mdx b/docs/integrations/frameworks/gatsby.mdx index ee80be1d6..4e12265de 100644 --- a/docs/integrations/frameworks/gatsby.mdx +++ b/docs/integrations/frameworks/gatsby.mdx @@ -10,31 +10,20 @@ Prerequisites: ## Initialize Infisical for your [Gatsby](https://www.gatsbyjs.com) app ```bash -# move to your app +# navigate to the root of your of your project cd /path/to/project -# initialize infisical +# then initialize Infisical infisical init ``` -## Modify the start script in your `package.json` - -```json -... -"scripts": { - "develop": "infisical run -- gatsby develop", - "start": "infisical run -- gatsby develop", - "build": "infisical run -- gatsby build", - "serve": "infisical run -- gatsby serve", - "clean": "infisical run -- gatsby clean" -} -... -``` - -## Start your development server with environment variables injected +## Start your application as usual but with Infisical ```bash -npm run develop +infisical run -- + +# Example +infisical run -- npm run develop ``` diff --git a/docs/integrations/frameworks/laravel.mdx b/docs/integrations/frameworks/laravel.mdx index 0dfb219aa..0152e7c66 100644 --- a/docs/integrations/frameworks/laravel.mdx +++ b/docs/integrations/frameworks/laravel.mdx @@ -7,4 +7,21 @@ Prerequisites: - Set up and add envars to [Infisical Cloud](https://app.infisical.com) - [Install the CLI](/cli/overview) -Instructions coming soon. +## Initialize Infisical for your [Laravel](https://laravel.com/) app + +```bash +# navigate to the root of your of your project +cd /path/to/project + +# then initialize Infisical +infisical init +``` + +## Start your application as usual but with Infisical + +```bash +infisical run -- + +# Example +infisical run -- php artisan serve +``` diff --git a/docs/integrations/frameworks/nestjs.mdx b/docs/integrations/frameworks/nestjs.mdx index 8283b1175..5e224587e 100644 --- a/docs/integrations/frameworks/nestjs.mdx +++ b/docs/integrations/frameworks/nestjs.mdx @@ -10,41 +10,18 @@ Prerequisites: ## Initialize Infisical for your [NestJS](https://nestjs.com) app ```bash -# move to your Next.js app +# navigate to the root of your of your project cd /path/to/project -# initialize infisical +# then initialize infisical infisical init ``` -## Modify the start script in your `package.json` - -```json -... -"scripts": { - "prebuild": "rimraf dist", - "build": "nest build", - "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", - "start": "infisical run -- nest start", - "start:dev": "infisical run -- nest start --watch", - "start:debug": "nest start --debug --watch", - "start:prod": "node dist/main", - "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", - "test": "jest", - "test:watch": "jest --watch", - "test:cov": "jest --coverage", - "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "jest --config ./test/jest-e2e.json" -} -... -``` - -## Start your server with environment variables injected +## Start your application as usual but with Infisical ```bash -npm run start +infisical run -- -# or start development server - -npm run start:dev +# Example +infisical run -- npm run start:dev ``` diff --git a/docs/integrations/frameworks/nextjs.mdx b/docs/integrations/frameworks/nextjs.mdx index a70cf6e41..d6e46f54e 100644 --- a/docs/integrations/frameworks/nextjs.mdx +++ b/docs/integrations/frameworks/nextjs.mdx @@ -10,33 +10,20 @@ Prerequisites: ## Initialize Infisical for your [Next.js](https://nextjs.org) app ```bash -# move to your Next.js app +# navigate to the root of your of your project cd /path/to/project -# initialize infisical +# then initialize infisical infisical init ``` -## Modify the start script in your `package.json` - -```json -... -"scripts": { - "dev": "infisical run -- next dev", - "build": "infisical run -- next build", - "start": "infisical run -- next start" -} -... -``` - -## Start your server with environment variables injected +## Start your application as usual but with Infisical ```bash -npm run build && npm run start +infisical run -- -# or start development server - -npm run dev +# Example +infisical run -- npm run dev ``` diff --git a/docs/integrations/frameworks/nuxt.mdx b/docs/integrations/frameworks/nuxt.mdx index 901ec35a5..8105940ee 100644 --- a/docs/integrations/frameworks/nuxt.mdx +++ b/docs/integrations/frameworks/nuxt.mdx @@ -10,27 +10,18 @@ Prerequisites: ## Initialize Infisical for your [Nuxt](https://nuxtjs.org) app ```bash -# move to your Nuxt app +# navigate to the root of your of your project cd /path/to/project -# initialize infisical +# then initialize infisical infisical init ``` -## Modify the start script in your `package.json` - -```json -... -"scripts": { - "dev": "infisical run -- nuxt", - "build": "infisical run -- nuxt build", - "start": "infisical run -- nuxt start" -} -... -``` - -## Start your development server with environment variables injected +## Start your application as usual but with Infisical ```bash -npm run dev +infisical run -- + +# Example +infisical run -- npm run dev ``` diff --git a/docs/integrations/frameworks/rails.mdx b/docs/integrations/frameworks/rails.mdx index 7e08bffa7..32e7cbd57 100644 --- a/docs/integrations/frameworks/rails.mdx +++ b/docs/integrations/frameworks/rails.mdx @@ -10,15 +10,18 @@ Prerequisites: ## Initialize Infisical for your [Rails](https://rubyonrails.org) app ```bash -# move to your Rails app +# navigate to the root of your of your project cd /path/to/project -# initialize infisical +# then initialize Infisical infisical init ``` -## Start your server with environment variables injected +## Start your application as usual but with Infisical ```bash +infisical run -- + +# Example infisical run -- bin/rails server ``` diff --git a/docs/integrations/frameworks/react.mdx b/docs/integrations/frameworks/react.mdx index 950505432..0c394ac7e 100644 --- a/docs/integrations/frameworks/react.mdx +++ b/docs/integrations/frameworks/react.mdx @@ -10,28 +10,18 @@ Prerequisites: ## Initialize Infisical for your [Create React App](https://create-react-app.dev) ```bash -# move to your React app +# navigate to the root of your of your project cd /path/to/project -# initialize infisical +# then initialize infisical infisical init ``` -## Modify the start script in your `package.json` - -```json -... -"scripts": { - "start": "infisical run -- react-scripts start", - "build": "react-scripts build", - "test": "react-scripts test", - "eject": "react-scripts eject" -} -... -``` - -## Start your server with environment variables injected +## Start your application as usual but with Infisical ```bash -npm start +infisical run -- + +# Example +infisical run -- npm run dev ``` diff --git a/docs/integrations/frameworks/remix.mdx b/docs/integrations/frameworks/remix.mdx index aa9956005..cc37a263e 100644 --- a/docs/integrations/frameworks/remix.mdx +++ b/docs/integrations/frameworks/remix.mdx @@ -10,32 +10,18 @@ Prerequisites: ## Initialize Infisical for your [Remix](https://remix.run) app ```bash -# move to your Vue app +# navigate to the root of your of your project cd /path/to/project -# initialize infisical +# then initialize Infisical infisical init ``` -## Modify the start script in your `package.json` - -```json -... -"scripts": { - ... - "dev": "infisical run -- run-p dev:*", - "start": "infisical run -- remix-serve build" - ... -} -... -``` - -## Start your server with environment variables injected +## Start your application as usual but with Infisical ```bash -npm run build && npm run start +infisical run -- -# or start development server - -npm run dev +# Example +infisical run -- npm run dev ``` diff --git a/docs/integrations/frameworks/vite.mdx b/docs/integrations/frameworks/vite.mdx index 251924ca7..2c6e80ff0 100644 --- a/docs/integrations/frameworks/vite.mdx +++ b/docs/integrations/frameworks/vite.mdx @@ -10,29 +10,20 @@ Prerequisites: ## Initialize Infisical for your [Vite](https://vitejs.dev) app ```bash -# move to your Vite app +# navigate to the root of your of your project cd /path/to/project -# initialize infisical +# then initialize Infisical infisical init ``` -## Modify the start script in your `package.json` - -```json -... -"scripts": { - "dev": "infisical run -- vite", - "build": "infisical run -- vite build", - "preview": "infisical run -- vite preview" -} -... -``` - -## Start your development server with environment variables injected +## Start your application as usual but with Infisical ```bash -npm run dev +infisical run -- + +# Example +infisical run -- npm run dev ``` diff --git a/docs/integrations/frameworks/vue.mdx b/docs/integrations/frameworks/vue.mdx index 15c4a12cf..9bc17255c 100644 --- a/docs/integrations/frameworks/vue.mdx +++ b/docs/integrations/frameworks/vue.mdx @@ -10,29 +10,20 @@ Prerequisites: ## Initialize Infisical for your [Vue](https://vuejs.org) app ```bash -# move to your Vue app +# navigate to the root of your of your project cd /path/to/project -# initialize infisical +# then initialize infisical infisical init ``` -## Modify the start script in your `package.json` - -```json -... -"scripts": { - "serve": "infisical run -- vue-cli-service serve", - "build": "infisical run -- vue-cli-service build", - "lint": "infisical run -- vue-cli-service lint" -} -... -``` - -## Start your development server with environment variables injected +## Start your application as usual but with Infisical ```bash -npm run serve +infisical run -- + +# Example +infisical run -- npm run dev ``` diff --git a/docs/integrations/overview.mdx b/docs/integrations/overview.mdx index 17c7a7f72..0ccb9da26 100644 --- a/docs/integrations/overview.mdx +++ b/docs/integrations/overview.mdx @@ -1,5 +1,5 @@ --- -title: "Overview" +title: 'Overview' --- Integrations allow environment variables to be synced from Infisical into your local development workflow, CI/CD pipelines, and production infrastructure. @@ -10,18 +10,11 @@ Missing an integration? Throw in a [request](https://github.com/Infisical/infisi | -------------------------------------------------------- | --------- | ----------- | | [Docker](/integrations/platforms/docker) | Platform | Available | | [Docker-Compose](/integrations/platforms/docker-compose) | Platform | Available | -| Kubernetes | Platform | Coming soon | +| [Kubernetes](/integrations/platforms/kubernetes) | Platform | Available | | [Heroku](/integrations/cloud/heroku) | Cloud | Available | -| Vercel | Cloud | Coming soon | -| AWS | Cloud | Coming soon | -| GCP | Cloud | Coming soon | -| Azure | Cloud | Coming soon | -| DigitalOcean | Cloud | Coming soon | -| GitLab | CI/CD | Coming soon | -| CircleCI | CI/CD | Coming soon | -| TravisCI | CI/CD | Coming soon | -| GitHub Actions | CI/CD | Coming soon | -| Jenkins | CI/CD | Coming soon | +| [Vercel](/integrations/cloud/vercel) | Cloud | Available | +| [Netlify](/integrations/cloud/netlify) | Cloud | Available | +| [GitHub Actions](/integrations/cicd/githubactions) | CI/CD | Available | | [React](/integrations/frameworks/react) | Framework | Available | | [Vue](/integrations/frameworks/vue) | Framework | Available | | [Express](/integrations/frameworks/express) | Framework | Available | @@ -31,8 +24,19 @@ Missing an integration? Throw in a [request](https://github.com/Infisical/infisi | [Gatsby](/integrations/frameworks/gatsby) | Framework | Available | | [Remix](/integrations/frameworks/remix) | Framework | Available | | [Vite](/integrations/frameworks/vite) | Framework | Available | -| [Fiber](/integrations/frameworks/fiber) | Framework | Coming soon | +| [Fiber](/integrations/frameworks/fiber) | Framework | Available | | [Django](/integrations/frameworks/django) | Framework | Available | | [Flask](/integrations/frameworks/flask) | Framework | Available | -| [Laravel](/integrations/frameworks/laravel) | Framework | Coming soon | +| [Laravel](/integrations/frameworks/laravel) | Framework | Available | | [Ruby on Rails](/integrations/frameworks/rails) | Framework | Available | +| [Render](/integrations/cloud/render) | Cloud | Coming soon | +| [Fly.io](/integrations/cloud/flyio) | Cloud | Coming soon | +| AWS | Cloud | Coming soon | +| GCP | Cloud | Coming soon | +| Azure | Cloud | Coming soon | +| DigitalOcean | Cloud | Coming soon | +| GitLab | CI/CD | Coming soon | +| [CircleCI](/integrations/cicd/circleci) | CI/CD | Coming soon | +| TravisCI | CI/CD | Coming soon | +| GitHub Actions | CI/CD | Coming soon | +| Jenkins | CI/CD | Coming soon | diff --git a/docs/integrations/platforms/docker-compose.mdx b/docs/integrations/platforms/docker-compose.mdx index 9e0f90e55..f2cdd60c8 100644 --- a/docs/integrations/platforms/docker-compose.mdx +++ b/docs/integrations/platforms/docker-compose.mdx @@ -4,14 +4,14 @@ title: "Docker Compose" The Docker Compose integration enables you to inject environment variables from Infisical into the containers defined in your compose file. -## Add the CLI to your Dockerfile(s) +## Add the CLI to your Dockerfile(s) start command -Follow steps 1 through 3 on our [guide to configure Infisical CLI](../integrations/platforms/docker) in your Dockerfile. +Follow the [guide to configure Infisical CLI](./docker) in your your Dockerfile first. ## Generate Infisical Token In order for Infisical CLI to authenticate and retrieve your project's secrets without exposing your login credentials, you must generate a Infisical Token. -To learn how, visit [Infisical Token](../getting-started/cli/infisical-token). Once you have generated the token, keep it handy. +To learn how, visit [Infisical Token](../../getting-started/dashboard/token). Once you have generated the token, keep it handy. If you have multiple services and they do not use the same secrets, you will diff --git a/docs/integrations/platforms/kubernetes.mdx b/docs/integrations/platforms/kubernetes.mdx new file mode 100644 index 000000000..51a9d7ec4 --- /dev/null +++ b/docs/integrations/platforms/kubernetes.mdx @@ -0,0 +1,161 @@ +--- +title: 'Kubernetes' +--- + +The Infisical Secrets Operator is a custom Kubernetes controller that helps keep secrets in a cluster up to date by synchronizing them. +It is installed in its own namespace within the cluster and follows strict RBAC policies. +The operator uses InfisicalSecret custom resources to identify which secrets to sync and where to store them. +It is responsible for continuously updating managed secrets, and in the future may also automatically reload deployments that use them as needed. + +## Install Operator + +The operator can be install via [Helm](helm.sh) or [kubectl](https://github.com/kubernetes/kubectl) + + + + Install Infisical Helm repository + ```bash + helm repo add infisical-helm-charts 'https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/' + + helm repo update + ``` + + Install the Helm chart + ```bash + helm install --generate-name infisical-helm-charts/secrets-operator + ``` + + + + The operator will be installed in `infisical-operator-system` namespace + ``` + kubectl apply -f https://raw.githubusercontent.com/Infisical/infisical/main/k8-operator/kubectl-install/install-secrets-operator.yaml + ``` + + + +## Sync Infisical Secrets to your cluster + +To retrieve secrets from an Infisical project and store them in your Kubernetes cluster, you can use the InfisicalSecret custom resource. +This resource is available after installing the Infisical operator. In order to specify the Infisical Token location and the location where the retrieved secrets should be stored, you can use the `tokenSecretReference` and `managedSecretReference` fields within the InfisicalSecret resource. + + + The `tokenSecretReference` field in the InfisicalSecret resource is used to specify the location of the Infisical Token, which is required for authenticating and retrieving secrets from an Infisical project. + + To create a Kubernetes secret containing an [Infisical Token](../../getting-started/dashboard/token), you can run the following command. + ``` bash + kubectl create secret generic service-token --from-literal=infisicalToken= + ``` + +Once the secret is created, add the name and namespace of the secret under `tokenSecretReference` field in the InfisicalSecret custom resource. + +{' '} + + + No matter what the name of the secret is or its namespace, it must contain a + key named `infisicalToken` with a valid Infisical Token as the value + + + + + +The `managedSecretReference` field in the InfisicalSecret resource is used to specify the location where secrets retrieved from an Infisical project should be stored. +You should specify the name and namespace of the Kubernetes secret that will hold these secrets. The operator will create the secret for you, you just need to provide its name and namespace. + +It is recommended that the managed secret be created in the same namespace as the deployment that will use it. + + + +```yaml +apiVersion: secrets.infisical.com/v1alpha1 +kind: InfisicalSecret +metadata: + # Name of of this InfisicalSecret resource + name: infisicalsecret-sample +spec: + # The host that should be used to pull secrets from. The default value is https://infisical.com/api. + hostAPI: https://infisical.com/api + + # The Infisical project from which to pull secrets from + projectId: 62faf98ae0b05e8529b5da46 + + # The environment (dev, prod, testing, etc.) of the above project from where secrets should be pulled from + environment: dev + + # The Kubernetes secret the stores the Infisical token + tokenSecretReference: + # Kubernetes secret name + secretName: service-token + # The secret namespace + secretNamespace: default + + # The Kubernetes secret that Infisical Operator will create and populate with secrets from the above project + managedSecretReference: + # The name of managed Kubernetes secret that should be created + secretName: managed-secret + # The namespace the managed secret should be installed in + secretNamespace: default +``` + +## Verify + +To use the InfisicalSecret custom resource in your deployment, you can simply reference the managed secret specified in the `managedSecretReference` field as you would any other Kubernetes secret. +To verify that the operator has successfully created the managed secret, you can check the secrets in the namespace that was specified. + +```bash +# Verify managed secret is created +kubectl get secrets -n +``` + + + The Infisical secrets will be synced and stored into the managed secret every + 5 minutes. + + +## Troubleshoot + +If the operator is unable to fetch secrets from the API, it will not affect the managed Kubernetes secret. +It will continue attempting to reconnect to the API indefinitely. +The InfisicalSecret resource uses the `status.conditions` field to report its current state and any errors encountered. + +```yaml +$ kubectl get infisicalSecrets +NAME AGE +infisicalsecret-sample 12s + +$ kubectl describe infisicalSecret infisicalsecret-sample +... +Spec: +... +Status: + Conditions: + Last Transition Time: 2022-12-18T04:29:09Z + Message: Infisical controller has located the Infisical token in provided Kubernetes secret + Reason: OK + Status: True + Type: secrets.infisical.com/LoadedInfisicalToken + Last Transition Time: 2022-12-18T04:29:10Z + Message: Failed to update secret because: 400 Bad Request + Reason: Error + Status: False + Type: secrets.infisical.com/ReadyToSyncSecrets +Events: +``` + +## Uninstall Operator + +The managed secret created by the operator will not be deleted when the operator is uninstalled. + + + + Install Infisical Helm repository + ```bash + helm uninstall add + ``` + + + ``` + kubectl delete -f https://raw.githubusercontent.com/Infisical/infisical/main/k8-operator/kubectl-install/install-secrets-operator.yaml + ``` + + diff --git a/docs/mint.json b/docs/mint.json index 1d5246680..cbac56d5a 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -21,7 +21,9 @@ "to": "#F8B7BD" } }, - "topbarLinks": [{ "name": "Log In", "url": "https://app.infisical.com/login" }], + "topbarLinks": [ + { "name": "Log In", "url": "https://app.infisical.com/login" } + ], "topbarCtaButton": { "name": "Start for Free", "url": "https://app.infisical.com/signup" @@ -87,50 +89,65 @@ "cli/overview", "cli/usage", { - "group": "Reference", + "group": "Commands", "pages": [ - "cli/reference/commands", - "cli/reference/login", - "cli/reference/init", - "cli/reference/run" + "cli/commands/login", + "cli/commands/init", + "cli/commands/run", + "cli/commands/export", + "cli/commands/vault" ] - } + }, + "cli/faq" ] }, { "group": "Self-hosting", "pages": [ - "self-hosting/overview", - { - "group": "Deployments options", - "pages": [ - "self-hosting/deployments/linux", - "self-hosting/deployments/kubernetes" - ] - }, - { - "group": "Configuration", - "pages": ["self-hosting/configuration/envars"] - } + "self-hosting/overview" ] - }, + }, + { + "group": "Deployment options", + "pages": [ + "self-hosting/deployments/linux", + "self-hosting/deployments/kubernetes" + ] + }, + { + "group": "Configuration", + "pages": [ + "self-hosting/configuration/envars", + "self-hosting/configuration/email" + ] + }, { "group": "Integrations", - "pages": [ - "integrations/overview" - ] + "pages": ["integrations/overview"] }, { "group": "Platforms", "pages": [ "integrations/platforms/docker", - "integrations/platforms/docker-compose" + "integrations/platforms/docker-compose", + "integrations/platforms/kubernetes" ] }, { "group": "Cloud", "pages": [ - "integrations/cloud/heroku" + "integrations/cloud/heroku", + "integrations/cloud/vercel", + "integrations/cloud/netlify", + "integrations/cloud/render", + "integrations/cloud/flyio" + ] + }, + { + "group": "CI/CD", + "pages": [ + "integrations/cicd/githubactions", + "integrations/cicd/circleci" ] }, { @@ -165,10 +182,12 @@ "pages": [ "contributing/overview", "contributing/code-of-conduct", - "contributing/developing", - "contributing/FAQ" + "contributing/developing" ] } ], - "backgroundImage": "/images/background.png" + "backgroundImage": "/images/background.png", + "integrations": { + "intercom": "hsg644ru" + } } diff --git a/docs/self-hosting/configuration/email.mdx b/docs/self-hosting/configuration/email.mdx new file mode 100644 index 000000000..b1e911fb6 --- /dev/null +++ b/docs/self-hosting/configuration/email.mdx @@ -0,0 +1,75 @@ +--- +title: "Email" +description: "" +--- + +Infisical requires you to configure your own SMTP server for certain functionality like: + +- Sending email confirmation links to sign up. +- Sending invite links for projects. +- Sending alerts. + +We strongly recommend using an email service to act as your email server and provide examples for common providers. + +## General configuration + +By default, you need to configure the following SMTP [environment variables](https://infisical.com/docs/self-hosting/configuration/envars): + +- `SMTP_HOST`: Hostname to connect to for establishing SMTP connections. +- `SMTP_USERNAME`: Credential to connect to host (e.g. team@infisical.com) +- `SMTP_PASSWORD`: Credential to connect to host. +- `SMTP_PORT`: Port to connect to for establishing SMTP connections. +- `SMTP_SECURE`: If `true`, the connection will use TLS when connecting to server with special configs for SendGrid and Mailgun. If `false` (the default) then TLS is used if server supports the STARTTLS extension. +- `SMTP_FROM_ADDRESS`: Email address to be used for sending emails (e.g. team@infisical.com). +- `SMTP_FROM_NAME`: Name label to be used in `From` field (e.g. Team). + +Below you will find details on how to configure common email providers (not in any particular order). + +## Twilio SendGrid + +1. Create an account and configure [SendGrid](https://sendgrid.com) to send emails. +2. Create a SendGrid API Key under Settings > [API Keys](https://app.sendgrid.com/settings/api_keys) +3. Set a name for your API Key, we recommend using "Infisical," and select the "Restricted Key" option. You will need to enable the "Mail Send" permission as shown below: + +![creating sendgrid api key](../../images/email-sendgrid-create-key.png) + +![setting sendgrid api key restriction](../../images/email-sendgrid-restrictions.png) + +4. With the API Key, you can now set your SMTP environment variables: + +``` +SMTP_HOST=smtp.sendgrid.net +SMTP_USERNAME=apikey +SMTP_PASSWORD=SG.rqFsfjxYPiqE1lqZTgD_lz7x8IVLx # your SendGrid API Key from step above +SMTP_PORT=587 +SMTP_SECURE=true +SMTP_FROM_ADDRESS=hey@example.com # your email address being used to send out emails +SMTP_FROM_NAME=Infisical +``` + + + Remember that you will need to restart Infisical for this to work properly. + + +## Mailgun + +1. Create an account and configure [Mailgun](https://www.mailgun.com) to send emails. +2. Obtain your Mailgun credentials in Sending > Overview > SMTP + +![obtain mailhog api key estriction](../../images/email-mailhog-credentials.png) + +3. With your Mailgun credentials, you can now set up your SMTP environment variables: + +``` +SMTP_HOST=smtp.mailgun.org # obtained from credentials page +SMTP_USERNAME=postmaster@example.mailgun.org # obtained from credentials page +SMTP_PASSWORD=password # obtained from credentials page +SMTP_PORT=587 +SMTP_SECURE=true +SMTP_FROM_ADDRESS=hey@example.com # your email address being used to send out emails +SMTP_FROM_NAME=Infisical +``` + + + Remember that you will need to restart Infisical for this to work properly. + \ No newline at end of file diff --git a/docs/self-hosting/configuration/envars.mdx b/docs/self-hosting/configuration/envars.mdx index bea36da32..0b9fd5e71 100644 --- a/docs/self-hosting/configuration/envars.mdx +++ b/docs/self-hosting/configuration/envars.mdx @@ -3,31 +3,38 @@ title: "Environment Variables" description: "" --- -## The .env file - -Configuring Infisical requires setting some environment variables. There is a file called `.env.example` at the root directory of our main repo that you can use to create a `.env` before you start the server. +Configuring Infisical requires setting some environment variables. There is a file called [`.env.example`](https://github.com/Infisical/infisical/blob/main/.env.example) at the root directory of our main repo that you can use to create a `.env` file before you start the server. | Variable | Description | Default Value | | ---------------------------- | ----------------------------------------------------------------------------------------------------------- | ---------------- | -| `PRIVATE_KEY` | โ—๏ธ NaCl-generated server secret key | `None` | -| `PUBLIC_KEY` | โ—๏ธ NaCl-generated server public key | `None` | -| `ENCRYPTION_KEY` | โ—๏ธ Strong hex encryption key | `None` | -| `JWT_SIGNUP_SECRET` | โ—๏ธJWT token secret | `None` | -| `JWT_REFRESH_SECRET` | โ—๏ธ JWT token secret | `None` | -| `JWT_AUTH_SECRET` | โ—๏ธ JWT token secret | `None` | +| `ENCRYPTION_KEY` | โ—๏ธ Strong hex encryption key | `None` | +| `JWT_SIGNUP_SECRET` | โ—๏ธ JWT token secret | `None` | +| `JWT_REFRESH_SECRET` | โ—๏ธ JWT token secret | `None` | +| `JWT_AUTH_SECRET` | โ—๏ธ JWT token secret | `None` | +| `JWT_SERVICE_SECRET` | โ—๏ธ JWT token secret | `None` | | `JWT_SIGNUP_LIFETIME` | JWT token lifetime expressed in seconds or a string describing a time span (e.g. 60, "2 days", "10h", "7d") | `15m` | | `JWT_REFRESH_LIFETIME` | JWT token lifetime expressed in seconds or a string describing a time span (e.g. 60, "2 days", "10h", "7d") | `90d` | | `JWT_AUTH_LIFETIME` | JWT token lifetime expressed in seconds or a string describing a time span (e.g. 60, "2 days", "10h", "7d") | `10d` | | `EMAIL_TOKEN_LIFETIME` | Email OTP/magic-link lifetime expressed in seconds | `86400` | -| `MONGO_URL` | โ—๏ธ MongoDB instance connection string either to container instance or MongoDB Cloud | `None` | +| `MONGO_URL` | โ—๏ธ MongoDB instance connection string either to container instance or MongoDB Cloud | `None` | | `MONGO_USERNAME` | MongoDB username if using container | `None` | | `MONGO_PASSWORD` | MongoDB password if using container | `None` | -| `SITE_URL` | โ—๏ธ Site URL - should be an absolute URL including the protocol (e.g. `https://app.infisical.com`) | `None` | -| `SMT_HOST` | Whether the user joined the community | `smtp.gmail.com` | -| `SMTP_NAME` | Hostname to connect to for establishing SMTP connections (e.g. `Team`) | `None` | -| `SMTP_USERNAME` | โ—๏ธ Credential to connect to host (e.g. `team@infisical.com`) | `None` | -| `SMTP_PASSWORD` | โ—๏ธ Credential to connect to host | `None` | +| `SITE_URL` | โ—๏ธ Site URL - should be an absolute URL including the protocol (e.g. `https://app.infisical.com`) | `None` | +| `SMTP_HOST` | โ—๏ธ Hostname to connect to for establishing SMTP connections | `None` | +| `SMTP_USERNAME` | โ—๏ธ Credential to connect to host (e.g. `team@infisical.com`) | `None` | +| `SMTP_PASSWORD` | โ—๏ธ Credential to connect to host | `None` | +| `SMTP_PORT` | Port to connect to for establishing SMTP connections | `587` | +| `SMTP_SECURE` | If true, use TLS when connecting to host. If false, TLS will be used if STARTTLS is supported | `false` | +| `SMTP_FROM_ADDRESS` | โ—๏ธ Email address to be used for sending emails (e.g. `team@infisical.com`) | `None` | +| `SMTP_FROM_NAME` | Name label to be used in From field (e.g. `Team`) | `Infisical` | | `TELEMETRY_ENABLED` | `true` or `false`. [More](../overview). | `true` | -| `OAUTH_CLIENT_SECRET_HEROKU` | OAuth client secret for Heroku integration | `None` | -| `OAUTH_TOKEN_URL_HEROKU` | OAuth token URL for Heroku integration | `None` | +| `CLIENT_ID_HEROKU` | OAuth2 client ID for Heroku integration | `None` | +| `CLIENT_ID_VERCEL` | OAuth2 client ID for Vercel integration | `None` | +| `CLIENT_ID_NETLIFY` | OAuth2 client ID for Netlify integration | `None` | +| `CLIENT_ID_GITHUB` | OAuth2 client ID for GitHub integration | `None` | +| `CLIENT_SECRET_HEROKU` | OAuth2 client secret for Heroku integration | `None` | +| `CLIENT_SECRET_VERCEL` | OAuth2 client secret for Vercel integration | `None` | +| `CLIENT_SECRET_NETLIFY` | OAuth2 client secret for Netlify integration | `None` | +| `CLIENT_SECRET_GITHUB` | OAuth2 client secret for GitHub integration | `None` | +| `CLIENT_SLUG_VERCEL` | OAuth2 slug for Netlify integration | `None` | | `SENTRY_DSN` | DSN for error-monitoring with Sentry | `None` | diff --git a/docs/self-hosting/deployments/kubernetes.mdx b/docs/self-hosting/deployments/kubernetes.mdx index 8ed2c0e05..19499d8ae 100644 --- a/docs/self-hosting/deployments/kubernetes.mdx +++ b/docs/self-hosting/deployments/kubernetes.mdx @@ -42,7 +42,7 @@ that by adding the `--namespace ` to your `helm install ```bash ## Installs to default namespace -helm install infisical-helm-charts/infisical --values +helm install infisical-helm-charts/infisical --generate-name --values ``` @@ -50,5 +50,4 @@ If you have not filled out all of the required environment variables, you will s do so. -4. Your Infisical installation is complete and should be running on the host name you specified in Ingress in `values.yaml`. -Note: Please allow an additional time (2 minutes) for the frontend pods to be fully ready. \ No newline at end of file +#### 4. Your Infisical installation is complete and should be running on the host name you specified in Ingress in `values.yaml`. \ No newline at end of file diff --git a/frontend/.eslintrc b/frontend/.eslintrc index b562aaa1f..d5f35096c 100644 --- a/frontend/.eslintrc +++ b/frontend/.eslintrc @@ -9,13 +9,13 @@ "plugins": ["simple-import-sort", "@typescript-eslint"], "rules": { "react-hooks/exhaustive-deps": "off", - "no-unused-vars": "off", + "no-unused-vars": "warn", + "@typescript-eslint/ban-ts-comment": "warn", "@typescript-eslint/no-unused-vars": "off", "@typescript-eslint/no-var-requires": "off", "@typescript-eslint/no-empty-function": "off", "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-non-null-assertion": "off", - "simple-import-sort/exports": "warn", "simple-import-sort/imports": [ "warn", diff --git a/frontend/.prettierrc b/frontend/.prettierrc deleted file mode 100644 index 222861c34..000000000 --- a/frontend/.prettierrc +++ /dev/null @@ -1,4 +0,0 @@ -{ - "tabWidth": 2, - "useTabs": false -} diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 000000000..520f0fb7f --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,67 @@ +ARG POSTHOG_HOST=https://app.posthog.com +ARG POSTHOG_API_KEY=posthog-api-key + +FROM node:16-alpine AS deps +# Install dependencies only when needed. Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. +# RUN apk add --no-cache libc6-compat +WORKDIR /app + +# Copy over dependency files +COPY package.json package-lock.json next.config.js ./ + +# Install dependencies +RUN npm ci --only-production --ignore-scripts + + +# Rebuild the source code only when needed +FROM node:16-alpine AS builder +WORKDIR /app + +# Copy dependencies +COPY --from=deps /app/node_modules ./node_modules +# Copy all files +COPY . . + +ENV NODE_ENV production +ENV NEXT_PUBLIC_ENV production +ARG POSTHOG_HOST +ENV NEXT_PUBLIC_POSTHOG_HOST $POSTHOG_HOST +ARG POSTHOG_API_KEY +ENV NEXT_PUBLIC_POSTHOG_API_KEY $POSTHOG_API_KEY + +# Build +RUN npm run build + + +# Production image +FROM node:16-alpine AS runner +WORKDIR /app + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +RUN mkdir -p /app/.next/cache/images && chown nextjs:nodejs /app/.next/cache/images +VOLUME /app/.next/cache/images + +ARG POSTHOG_API_KEY +ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \ + BAKED_NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY + +COPY --chown=nextjs:nodejs --chmod=555 scripts ./scripts +COPY --from=builder /app/public ./public +RUN chown nextjs:nodejs ./public/data +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +USER nextjs + +EXPOSE 3000 + +ENV PORT 3000 +ENV NEXT_TELEMETRY_DISABLED 1 + +HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \ + CMD node scripts/healthcheck.js + + +CMD ["/app/scripts/start.sh"] diff --git a/frontend/Dockerfile.dev b/frontend/Dockerfile.dev index 2bae23823..cb462bbc4 100644 --- a/frontend/Dockerfile.dev +++ b/frontend/Dockerfile.dev @@ -9,7 +9,7 @@ COPY package.json ./ COPY package-lock.json ./ # Install -RUN npm install +RUN npm install --ignore-scripts # Copy over next.js config COPY next.config.js ./next.config.js @@ -17,4 +17,4 @@ COPY next.config.js ./next.config.js # Copy all files COPY . . -CMD ["npm", "run", "dev"] \ No newline at end of file +CMD ["npm", "run", "dev"] diff --git a/frontend/Dockerfile.prod b/frontend/Dockerfile.prod deleted file mode 100644 index d95c00883..000000000 --- a/frontend/Dockerfile.prod +++ /dev/null @@ -1,20 +0,0 @@ -# Base layer -FROM node:16-alpine - -# Set the working directory -WORKDIR /app - -# Copy over dependency files -COPY package.json ./ -COPY package-lock.json ./ - -# Install -RUN npm install - -# Copy over next.js config -COPY next.config.js ./next.config.js - -# Copy all files -COPY . . - -CMD ["npm", "run", "start:docker"] diff --git a/frontend/components/RouteGuard.js b/frontend/components/RouteGuard.js index a21c0c86a..d08972b99 100644 --- a/frontend/components/RouteGuard.js +++ b/frontend/components/RouteGuard.js @@ -1,9 +1,9 @@ -import { useEffect, useState } from "react"; -import Image from "next/image"; -import { useRouter } from "next/router"; +import { useEffect, useState } from 'react'; +import Image from 'next/image'; +import { useRouter } from 'next/router'; -import { publicPaths } from "~/const"; -import checkAuth from "~/pages/api/auth/CheckAuth"; +import { publicPaths } from '~/const'; +import checkAuth from '~/pages/api/auth/CheckAuth'; // #TODO: finish spinner only when the data loads fully // #TODO: Redirect somewhere if the page does not exist @@ -22,16 +22,16 @@ export default function RouteGuard({ children }) { // #TODO: add the loading page when not yet authorized. const hideContent = () => setAuthorized(false); // const onError = () => setAuthorized(true) - router.events.on("routeChangeStart", hideContent); + router.events.on('routeChangeStart', hideContent); // router.events.on("routeChangeError", onError); // on route change complete - run auth check - router.events.on("routeChangeComplete", authCheck); + router.events.on('routeChangeComplete', authCheck); // unsubscribe from events in useEffect return function return () => { - router.events.off("routeChangeStart", hideContent); - router.events.off("routeChangeComplete", authCheck); + router.events.off('routeChangeStart', hideContent); + router.events.off('routeChangeComplete', authCheck); // router.events.off("routeChangeError", onError); }; // eslint-disable-next-line react-hooks/exhaustive-deps @@ -43,7 +43,7 @@ export default function RouteGuard({ children }) { */ async function authCheck(url) { // Make sure that we don't redirect when the user is on the following pages. - const path = "/" + url.split("?")[0].split("/")[1]; + const path = '/' + url.split('?')[0].split('/')[1]; // Check if the user is authenticated const response = await checkAuth(); @@ -51,16 +51,16 @@ export default function RouteGuard({ children }) { if (!publicPaths.includes(path)) { try { if (response.status !== 200) { - router.push("/login"); - console.log("Unauthorized to access."); + router.push('/login'); + console.log('Unauthorized to access.'); setAuthorized(false); } else { setAuthorized(true); - console.log("Authorized to access."); + console.log('Authorized to access.'); } } catch (error) { console.log( - "Error (probably the authCheck route is stuck again...):", + 'Error (probably the authCheck route is stuck again...):', error ); } diff --git a/frontend/components/analytics/posthog.js b/frontend/components/analytics/posthog.js index c8d51ad1b..8b8e88a22 100644 --- a/frontend/components/analytics/posthog.js +++ b/frontend/components/analytics/posthog.js @@ -1,17 +1,13 @@ -import posthog from "posthog-js"; +import posthog from 'posthog-js'; -import { - ENV, - POSTHOG_API_KEY, - POSTHOG_HOST, - TELEMETRY_ENABLED, -} from "../utilities/config"; +import { ENV, POSTHOG_API_KEY, POSTHOG_HOST } from '../utilities/config'; export const initPostHog = () => { - if (typeof window !== "undefined") { - if (ENV == "production" && TELEMETRY_ENABLED) { + if (typeof window !== 'undefined') { + // eslint-disable-next-line + if (ENV == 'production' && TELEMETRY_CAPTURING_ENABLED) { posthog.init(POSTHOG_API_KEY, { - api_host: POSTHOG_HOST, + api_host: POSTHOG_HOST }); } } diff --git a/frontend/components/analytics/posthog.ts b/frontend/components/analytics/posthog.ts new file mode 100644 index 000000000..e0d6e7c09 --- /dev/null +++ b/frontend/components/analytics/posthog.ts @@ -0,0 +1,18 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +/* eslint-disable no-undef */ +import posthog from 'posthog-js'; + +import { ENV, POSTHOG_API_KEY, POSTHOG_HOST } from '../utilities/config'; + +export const initPostHog = () => { + if (typeof window !== 'undefined') { + // @ts-ignore + if (ENV == 'production' && TELEMETRY_CAPTURING_ENABLED) { + posthog.init(POSTHOG_API_KEY, { + api_host: POSTHOG_HOST + }); + } + } + + return posthog; +}; diff --git a/frontend/components/basic/InputField.tsx b/frontend/components/basic/InputField.tsx index 02bb8a8cc..08afa975d 100644 --- a/frontend/components/basic/InputField.tsx +++ b/frontend/components/basic/InputField.tsx @@ -1,9 +1,8 @@ -import React, { useState } from "react"; -import { useRouter } from "next/router"; -import { faCircle, faEye, faEyeSlash } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import React, { useState } from 'react'; +import { faCircle, faEye, faEyeSlash } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import guidGenerator from "../utilities/randomId"; +import guidGenerator from '../utilities/randomId'; interface InputFieldProps { static?: boolean; @@ -21,9 +20,11 @@ interface InputFieldProps { onChangeHandler: (value: string) => void; } -const InputField = (props: InputFieldProps) => { +const InputField = ( + props: InputFieldProps & + Pick +) => { const [passwordVisible, setPasswordVisible] = useState(false); - const router = useRouter(); if (props.static === true) { return ( @@ -43,6 +44,8 @@ const InputField = (props: InputFieldProps) => { className="bg-bunker-800 text-gray-400 border border-gray-600 rounded-md text-md p-2 w-full min-w-16 outline-none" name={props.name} readOnly + autoComplete={props.autoComplete} + id={props.id} /> ); @@ -70,27 +73,30 @@ const InputField = (props: InputFieldProps) => {
props.onChangeHandler(e.target.value)} - type={passwordVisible === false ? props.type : "text"} + type={passwordVisible === false ? props.type : 'text'} placeholder={props.placeholder} value={props.value} required={props.isRequired} className={`${ props.blurred - ? "text-bunker-800 group-hover:text-gray-400 focus:text-gray-400 active:text-gray-400" - : "" + ? 'text-bunker-800 group-hover:text-gray-400 focus:text-gray-400 active:text-gray-400' + : '' } ${ - props.error ? "focus:ring-red/50" : "focus:ring-primary/50" + props.error ? 'focus:ring-red/50' : 'focus:ring-primary/50' } relative peer bg-bunker-800 rounded-md text-gray-400 text-md p-2 w-full min-w-16 outline-none focus:ring-4 duration-200`} name={props.name} spellCheck="false" + autoComplete={props.autoComplete} + id={props.id} /> - {props.label?.includes("Password") && ( + {props.label?.includes('Password') && (
+ + + + + + + + ); +} + +export default ActivateBotDialog; \ No newline at end of file diff --git a/frontend/components/basic/dialog/AddServiceTokenDialog.js b/frontend/components/basic/dialog/AddServiceTokenDialog.js index 59eecc29a..b02a804b7 100644 --- a/frontend/components/basic/dialog/AddServiceTokenDialog.js +++ b/frontend/components/basic/dialog/AddServiceTokenDialog.js @@ -1,6 +1,5 @@ import { Fragment, useState } from "react"; -import { useRouter } from "next/router"; -import useTranslate from "next-translate/useTranslation"; +import { useTranslation } from "next-i18next"; import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Dialog, Transition } from "@headlessui/react"; @@ -22,6 +21,8 @@ const expiryMapping = { "1 day": 86400, "7 days": 604800, "1 month": 2592000, + "6 months": 15552000, + "12 months": 31104000, }; const AddServiceTokenDialog = ({ @@ -30,13 +31,12 @@ const AddServiceTokenDialog = ({ workspaceId, workspaceName, }) => { - const router = useRouter(); const [serviceToken, setServiceToken] = useState(""); const [serviceTokenName, setServiceTokenName] = useState(""); const [serviceTokenEnv, setServiceTokenEnv] = useState("Development"); const [serviceTokenExpiresIn, setServiceTokenExpiresIn] = useState("1 day"); const [serviceTokenCopied, setServiceTokenCopied] = useState(false); - const { t } = useTranslate(); + const { t } = useTranslation(); const generateServiceToken = async () => { const latestFileKey = await getLatestFileKey({ workspaceId }); @@ -169,7 +169,13 @@ const AddServiceTokenDialog = ({ @@ -205,7 +211,7 @@ const AddServiceTokenDialog = ({
-
+
{ + + const submit = async () => { + try { + // 1. activate bot + await handleBotActivate(); + + // 2. start integration + await handleIntegrationOption({ + integrationOption: selectedIntegrationOption + }); + } catch (err) { + console.log(err); + } + + closeModal(); + } + + return ( +
+ + + +
+ +
+
+ + + + Grant Infisical access to your secrets + +
+

+ Most cloud integrations require Infisical to be able to decrypt your secrets so they can be forwarded over. +

+
+
+ {/*
+
+
+
+
+
+
+
+ ); +} + +export default IntegrationAccessTokenDialog; \ No newline at end of file diff --git a/frontend/components/basic/table/ServiceTokenTable.js b/frontend/components/basic/table/ServiceTokenTable.js index f67b7e4c7..fda94e477 100644 --- a/frontend/components/basic/table/ServiceTokenTable.js +++ b/frontend/components/basic/table/ServiceTokenTable.js @@ -1,10 +1,10 @@ -import React, { useEffect, useState } from "react"; -import { useRouter } from "next/router"; -import { faX } from "@fortawesome/free-solid-svg-icons"; +import React, { useEffect, useState } from 'react'; +import { useRouter } from 'next/router'; +import { faX } from '@fortawesome/free-solid-svg-icons'; -import { reverseEnvMapping } from "../../../public/data/frequentConstants"; -import guidGenerator from "../../utilities/randomId"; -import Button from "../buttons/Button"; +import { reverseEnvMapping } from '../../../public/data/frequentConstants'; +import guidGenerator from '../../utilities/randomId'; +import Button from '../buttons/Button'; /** * This is the component that we utilize for the user table - in future, can reuse it for some other purposes too. diff --git a/frontend/components/basic/table/UserTable.js b/frontend/components/basic/table/UserTable.js index 881556b70..99ec0ee75 100644 --- a/frontend/components/basic/table/UserTable.js +++ b/frontend/components/basic/table/UserTable.js @@ -1,25 +1,25 @@ -import React, { useEffect, useMemo, useState } from "react"; -import { useRouter } from "next/router"; -import { faX } from "@fortawesome/free-solid-svg-icons"; +import React, { useEffect, useMemo, useState } from 'react'; +import { useRouter } from 'next/router'; +import { faX } from '@fortawesome/free-solid-svg-icons'; -import deleteUserFromOrganization from "~/pages/api/organization/deleteUserFromOrganization"; -import changeUserRoleInWorkspace from "~/pages/api/workspace/changeUserRoleInWorkspace"; -import deleteUserFromWorkspace from "~/pages/api/workspace/deleteUserFromWorkspace"; -import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey"; -import uploadKeys from "~/pages/api/workspace/uploadKeys"; +import deleteUserFromOrganization from '~/pages/api/organization/deleteUserFromOrganization'; +import changeUserRoleInWorkspace from '~/pages/api/workspace/changeUserRoleInWorkspace'; +import deleteUserFromWorkspace from '~/pages/api/workspace/deleteUserFromWorkspace'; +import getLatestFileKey from '~/pages/api/workspace/getLatestFileKey'; +import uploadKeys from '~/pages/api/workspace/uploadKeys'; -import guidGenerator from "../../utilities/randomId"; -import Button from "../buttons/Button"; -import Listbox from "../Listbox"; +import guidGenerator from '../../utilities/randomId'; +import Button from '../buttons/Button'; +import Listbox from '../Listbox'; const { decryptAssymmetric, - encryptAssymmetric, -} = require("../../utilities/cryptography/crypto"); -const nacl = require("tweetnacl"); -nacl.util = require("tweetnacl-util"); + encryptAssymmetric +} = require('../../utilities/cryptography/crypto'); +const nacl = require('tweetnacl'); +nacl.util = require('tweetnacl-util'); -const roles = ["admin", "user"]; +const roles = ['admin', 'user']; /** * This is the component that we utilize for the user table - in future, can reuse it for some other purposes too. @@ -36,13 +36,13 @@ const UserTable = ({ isOrg, onClick, deleteUser, - setUserIdToBeDeleted, + setUserIdToBeDeleted }) => { const [roleSelected, setRoleSelected] = useState( Array(userData?.length).fill(userData.map((user) => user.role)) ); const router = useRouter(); - const [myRole, setMyRole] = useState("member"); + const [myRole, setMyRole] = useState('member'); // Delete the row in the table (e.g. a user) // #TODO: Add a pop-up that warns you that the user is going to be deleted. @@ -57,7 +57,7 @@ const UserTable = ({ changeData(userData.filter((v, i) => i !== index)); setRoleSelected([ ...roleSelected.slice(0, index), - ...roleSelected.slice(index + 1, userData?.length), + ...roleSelected.slice(index + 1, userData?.length) ]); }; @@ -76,10 +76,10 @@ const UserTable = ({ status: userData[index].status, userId: userData[index].userId, membershipId: userData[index].membershipId, - publicKey: userData[index].publicKey, - }, + publicKey: userData[index].publicKey + } ], - ...userData.slice(index + 1, userData?.length), + ...userData.slice(index + 1, userData?.length) ]); }; @@ -88,22 +88,22 @@ const UserTable = ({ }, [userData, myUser]); const grantAccess = async (id, publicKey) => { - let result = await getLatestFileKey({workspaceId: router.query.id}); + let result = await getLatestFileKey({ workspaceId: router.query.id }); - const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY"); + const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY'); // assymmetrically decrypt symmetric key with local private key const key = decryptAssymmetric({ ciphertext: result.latestKey.encryptedKey, nonce: result.latestKey.nonce, publicKey: result.latestKey.sender.publicKey, - privateKey: PRIVATE_KEY, + privateKey: PRIVATE_KEY }); const { ciphertext, nonce } = encryptAssymmetric({ plaintext: key, publicKey: publicKey, - privateKey: PRIVATE_KEY, + privateKey: PRIVATE_KEY }); uploadKeys(router.query.id, id, ciphertext, nonce); @@ -158,24 +158,24 @@ const UserTable = ({
- {row.status == "granted" && - ((myRole == "admin" && row.role != "owner") || - myRole == "owner") && + {row.status == 'granted' && + ((myRole == 'admin' && row.role != 'owner') || + myRole == 'owner') && myUser !== row.email ? ( handleRoleUpdate(index, e)} data={ - myRole == "owner" - ? ["owner", "admin", "member"] - : ["admin", "member"] + myRole == 'owner' + ? ['owner', 'admin', 'member'] + : ['admin', 'member'] } text="Role: " membershipId={row.membershipId} /> ) : ( - row.status != "invited" && - row.status != "verified" && ( + row.status != 'invited' && + row.status != 'verified' && ( ) )} - {(row.status == "invited" || - row.status == "verified") && ( + {(row.status == 'invited' || + row.status == 'verified') && (
)} - {row.status == "completed" && myUser !== row.email && ( + {row.status == 'completed' && myUser !== row.email && (
{myUser !== row.email && // row.role != "admin" && - myRole != "member" ? ( + myRole != 'member' ? (
)} - +
{plan.buttonTextSecondary}
@@ -70,9 +88,9 @@ export default function Plan({ plan }) { ) : (

CURRENT PLAN

diff --git a/frontend/components/context/Notifications/Notification.tsx b/frontend/components/context/Notifications/Notification.tsx index 974843cf6..ad556a6f5 100644 --- a/frontend/components/context/Notifications/Notification.tsx +++ b/frontend/components/context/Notifications/Notification.tsx @@ -1,35 +1,64 @@ -import { faXmarkCircle } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import classnames from "classnames"; +import { useEffect, useRef } from 'react'; +import { faX } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Notification as NotificationType } from "./NotificationProvider"; +import { Notification as NotificationType } from './NotificationProvider'; interface NotificationProps { - notification: NotificationType; - clearNotification: (text?: string) => void; + notification: Required; + clearNotification: (text: string) => void; } const Notification = ({ notification, - clearNotification, + clearNotification }: NotificationProps) => { + const timeout = useRef(); + + const handleClearNotification = () => clearNotification(notification.text); + + const setNotifTimeout = () => { + timeout.current = window.setTimeout( + handleClearNotification, + notification.timeoutMs + ); + }; + + const cancelNotifTimeout = () => { + clearTimeout(timeout.current); + }; + + useEffect(() => { + setNotifTimeout(); + + return cancelNotifTimeout; + }, []); + return (
-

{notification.text}

+ {notification.type === 'error' && ( +
+ )} + {notification.type === 'success' && ( +
+ )} + {notification.type === 'info' && ( +
+ )} +

+ {notification.text} +

); diff --git a/frontend/components/context/Notifications/NotificationProvider.tsx b/frontend/components/context/Notifications/NotificationProvider.tsx index 723319615..05f9eee19 100644 --- a/frontend/components/context/Notifications/NotificationProvider.tsx +++ b/frontend/components/context/Notifications/NotificationProvider.tsx @@ -1,20 +1,21 @@ -import { createContext, ReactNode, useContext, useState } from "react"; +import { createContext, ReactNode, useContext, useState } from 'react'; -import Notifications from "./Notifications"; +import Notifications from './Notifications'; -type NotificationType = "success" | "error"; +type NotificationType = 'success' | 'error' | 'info'; export type Notification = { text: string; - type: NotificationType; + type?: NotificationType; + timeoutMs?: number; }; type NotificationContextState = { - createNotification: ({ text, type }: Notification) => void; + createNotification: (newNotification: Notification) => void; }; const NotificationContext = createContext({ - createNotification: () => console.log("createNotification not set!"), + createNotification: () => console.log('createNotification not set!') }); export const useNotificationContext = () => useContext(NotificationContext); @@ -24,32 +25,36 @@ interface NotificationProviderProps { } const NotificationProvider = ({ children }: NotificationProviderProps) => { - const [notifications, setNotifications] = useState([]); + const [notifications, setNotifications] = useState[]>( + [] + ); - const clearNotification = (text?: string) => { - if (text) { - return setNotifications((state) => - state.filter((notif) => notif.text !== text) - ); - } - - return setNotifications([]); + const clearNotification = (text: string) => { + return setNotifications((state) => + state.filter((notif) => notif.text !== text) + ); }; - const createNotification = ({ text, type = "success" }: Notification) => { + const createNotification = ({ + text, + type = 'success', + timeoutMs = 5000 + }: Notification) => { const doesNotifExist = notifications.some((notif) => notif.text === text); if (doesNotifExist) { return; } - return setNotifications((state) => [...state, { text, type }]); + const newNotification: Required = { text, type, timeoutMs }; + + return setNotifications((state) => [...state, newNotification]); }; return ( void; + notifications: Required[]; + clearNotification: (text: string) => void; } const Notifications = ({ notifications, - clearNotification, + clearNotification }: NoticationsProps) => { + if (!notifications.length) { + return null; + } + return ( -
-
- {notifications.map((notif) => ( - - ))} -
+
+ {notifications.map((notif) => ( + + ))}
); }; diff --git a/frontend/components/dashboard/DashboardInputField.tsx b/frontend/components/dashboard/DashboardInputField.tsx index 7a5b4a048..cb75dcf8c 100644 --- a/frontend/components/dashboard/DashboardInputField.tsx +++ b/frontend/components/dashboard/DashboardInputField.tsx @@ -1,16 +1,16 @@ -import React, { SyntheticEvent, useRef } from "react"; -import { faCircle } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import React, { SyntheticEvent, useRef } from 'react'; +import { faCircle } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import guidGenerator from "../utilities/randomId"; +import guidGenerator from '../utilities/randomId'; const REGEX = /([$]{.*?})/g; interface DashboardInputFieldProps { - index: number; - onChangeHandler: (value: string, index: number) => void; + position: number; + onChangeHandler: (value: string, position: number) => void; value: string; - type: "varName" | "value"; + type: 'varName' | 'value'; blurred: boolean; duplicates: string[]; } @@ -18,7 +18,7 @@ interface DashboardInputFieldProps { /** * This component renders the input fields on the dashboard * @param {object} obj - the order number of a keyPair - * @param {number} obj.index - the order number of a keyPair + * @param {number} obj.pos - the order number of a keyPair * @param {function} obj.onChangeHandler - what happens when the input is modified * @param {string} obj.type - whether the input field is for a Key Name or for a Key Value * @param {string} obj.value - value of the InputField @@ -28,12 +28,12 @@ interface DashboardInputFieldProps { */ const DashboardInputField = ({ - index, + position, onChangeHandler, type, value, blurred, - duplicates, + duplicates }: DashboardInputFieldProps) => { const ref = useRef(null); const syncScroll = (e: SyntheticEvent) => { @@ -43,8 +43,8 @@ const DashboardInputField = ({ ref.current.scrollLeft = e.currentTarget.scrollLeft; }; - if (type === "varName") { - const startsWithNumber = !isNaN(Number(value.charAt(0))) && value != ""; + if (type === 'varName') { + const startsWithNumber = !isNaN(Number(value.charAt(0))) && value != ''; const hasDuplicates = duplicates?.includes(value); const error = startsWithNumber || hasDuplicates; @@ -52,17 +52,17 @@ const DashboardInputField = ({
- onChangeHandler(e.target.value.toUpperCase(), index) + onChangeHandler(e.target.value.toUpperCase(), position) } type={type} value={value} className={`z-10 peer font-mono ph-no-capture bg-bunker-800 rounded-md caret-white text-gray-400 text-md px-2 py-1.5 w-full min-w-16 outline-none focus:ring-2 ${ - error ? "focus:ring-red/50" : "focus:ring-primary/50" + error ? 'focus:ring-red/50' : 'focus:ring-primary/50' } duration-200`} spellCheck="false" /> @@ -79,7 +79,7 @@ const DashboardInputField = ({ )}
); - } else if (type === "value") { + } else if (type === 'value') { return (
onChangeHandler(e.target.value, index)} + onChange={(e) => onChangeHandler(e.target.value, position)} onScroll={syncScroll} className={`${ blurred - ? "text-transparent group-hover:text-transparent focus:text-transparent active:text-transparent" - : "" + ? 'text-transparent group-hover:text-transparent focus:text-transparent active:text-transparent' + : '' } z-10 peer font-mono ph-no-capture bg-transparent rounded-md caret-white text-transparent text-md px-2 py-1.5 w-full min-w-16 outline-none focus:ring-2 focus:ring-primary/50 duration-200 no-scrollbar no-scrollbar::-webkit-scrollbar`} spellCheck="false" /> @@ -100,8 +100,8 @@ const DashboardInputField = ({ ref={ref} className={`${ blurred - ? "text-bunker-800 group-hover:text-gray-400 peer-focus:text-gray-400 peer-active:text-gray-400" - : "" + ? 'text-bunker-800 group-hover:text-gray-400 peer-focus:text-gray-400 peer-active:text-gray-400' + : '' } absolute flex flex-row whitespace-pre font-mono z-0 ph-no-capture max-w-2xl overflow-x-scroll bg-bunker-800 h-9 rounded-md text-gray-400 text-md px-2 py-1.5 w-full min-w-16 outline-none focus:ring-2 focus:ring-primary/50 duration-100 no-scrollbar no-scrollbar::-webkit-scrollbar`} > {value.split(REGEX).map((word, id) => { @@ -112,7 +112,7 @@ const DashboardInputField = ({ {word.slice(2, word.length - 1)} - {word.slice(word.length - 1, word.length) == "}" ? ( + {word.slice(word.length - 1, word.length) == '}' ? ( {word.slice(word.length - 1, word.length)} @@ -135,7 +135,7 @@ const DashboardInputField = ({ {blurred && (
- {value.split("").map(() => ( + {value.split('').map(() => ( [ - guidGenerator(), - numCurrentRows + index, - key, - keyPairs[key as keyof typeof keyPairs], - "shared", - ]); + const newData = Object.keys(keyPairs).map((key, index) => { + return { + id: guidGenerator(), + pos: numCurrentRows + index, + key: key, + value: keyPairs[key as keyof typeof keyPairs], + type: "shared", + }; + }); setData(newData); setButtonReady(true); }; @@ -100,13 +102,15 @@ const DropZone = ({ if (typeof result === "string") { const newData = result .split("\n") - .map((line: string, index: number) => [ - guidGenerator(), - numCurrentRows + index, - line.split("=")[0], - line.split("=").slice(1, line.split("=").length).join("="), - "shared", - ]); + .map((line: string, index: number) => { + return { + id: guidGenerator(), + pos: numCurrentRows + index, + key: line.split("=")[0], + value: line.split("=").slice(1, line.split("=").length).join("="), + type: "shared", + }; + }); setData(newData); setButtonReady(true); } diff --git a/frontend/components/integrations/CloudIntegration.tsx b/frontend/components/integrations/CloudIntegration.tsx new file mode 100644 index 000000000..75a8019a5 --- /dev/null +++ b/frontend/components/integrations/CloudIntegration.tsx @@ -0,0 +1,121 @@ +import React from "react"; +import Image from "next/image"; +import { useRouter } from "next/router"; +import { + faCheck, + faX, + } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import deleteIntegrationAuth from "../../pages/api/integrations/DeleteIntegrationAuth"; + +interface CloudIntegrationOption { + isAvailable: boolean; + name: string; + type: string; + clientId: string; + docsLink: string; + slug: string; +} + +interface IntegrationAuth { + _id: string; + integration: string; +} + +interface Props { + cloudIntegrationOption: CloudIntegrationOption; + setSelectedIntegrationOption: (cloudIntegration: CloudIntegrationOption) => void; + integrationOptionPress: (cloudIntegrationOption: CloudIntegrationOption) => void; + integrationAuths: IntegrationAuth[]; +} + +const CloudIntegration = ({ + cloudIntegrationOption, + setSelectedIntegrationOption, + integrationOptionPress, + integrationAuths +}: Props) => { + const router = useRouter(); + return integrationAuths ? ( +
{ + if (!cloudIntegrationOption.isAvailable) return; + setSelectedIntegrationOption(cloudIntegrationOption); + integrationOptionPress(cloudIntegrationOption); + }} + key={cloudIntegrationOption.name} + > + integration logo + {cloudIntegrationOption.name.split(" ").length > 2 ? ( +
+
{cloudIntegrationOption.name.split(" ")[0]}
+
+ {cloudIntegrationOption.name.split(" ")[1]}{" "} + {cloudIntegrationOption.name.split(" ")[2]} +
+
+ ) : ( +
+ {cloudIntegrationOption.name} +
+ )} + {cloudIntegrationOption.isAvailable && + integrationAuths + .map((authorization) => authorization.integration) + .includes(cloudIntegrationOption.name.toLowerCase()) && ( +
+
{ + event.stopPropagation(); + deleteIntegrationAuth({ + integrationAuthId: integrationAuths + .filter( + (authorization) => + authorization.integration == + cloudIntegrationOption.name.toLowerCase() + ) + .map((authorization) => authorization._id)[0], + }); + + router.reload(); + }} + className="cursor-pointer w-max bg-red py-0.5 px-2 rounded-b-md text-xs flex flex-row items-center opacity-0 group-hover:opacity-100 duration-200" + > + + Revoke +
+
+ + Authorized +
+
+ )} + {!cloudIntegrationOption.isAvailable && ( +
+
+ Coming Soon +
+
+ )} +
+ ) :
+} + +export default CloudIntegration; \ No newline at end of file diff --git a/frontend/components/integrations/CloudIntegrationSection.tsx b/frontend/components/integrations/CloudIntegrationSection.tsx new file mode 100644 index 000000000..58fb92cb2 --- /dev/null +++ b/frontend/components/integrations/CloudIntegrationSection.tsx @@ -0,0 +1,50 @@ +import React from "react"; + +import CloudIntegration from "./CloudIntegration"; + +interface CloudIntegrationOption { + isAvailable: boolean; + name: string; + type: string; + clientId: string; + docsLink: string; + slug: string; +} + +interface Props { + cloudIntegrationOptions: CloudIntegrationOption[]; + setSelectedIntegrationOption: () => void; + integrationOptionPress: () => void; + integrationAuths: any; +} + +const CloudIntegrationSection = ({ + cloudIntegrationOptions, + setSelectedIntegrationOption, + integrationOptionPress, + integrationAuths +}: Props) => { + return ( + <> +
+

Cloud Integrations

+

+ Click on an integration to begin syncing secrets to it. +

+
+
+ {cloudIntegrationOptions.map((cloudIntegrationOption) => ( + + ))} +
+ + ); +} + +export default CloudIntegrationSection; \ No newline at end of file diff --git a/frontend/components/integrations/FrameworkIntegration.tsx b/frontend/components/integrations/FrameworkIntegration.tsx new file mode 100644 index 000000000..432dbad51 --- /dev/null +++ b/frontend/components/integrations/FrameworkIntegration.tsx @@ -0,0 +1,36 @@ +import React from "react"; +import Image from "next/image"; + +interface Framework { + name: string; + slug: string; + image: string; + docsLink: string; +} + +const FrameworkIntegration = ({ + framework +}: { + framework: Framework; +}) => { + return ( +
+
1 ? "text-sm px-1" : "text-xl px-2"} text-center w-full max-w-xs`}> + {framework?.image && integration logo} + {framework?.name && framework?.image &&
} + {framework?.name && framework.name} +
+
+ ); +} + +export default FrameworkIntegration; diff --git a/frontend/components/integrations/FrameworkIntegrationSection.tsx b/frontend/components/integrations/FrameworkIntegrationSection.tsx new file mode 100644 index 000000000..8535c595b --- /dev/null +++ b/frontend/components/integrations/FrameworkIntegrationSection.tsx @@ -0,0 +1,39 @@ +import React from "react"; + +import FrameworkIntegration from "./FrameworkIntegration"; + +interface Framework { + name: string; + image: string; + link: string; + slug: string; + docsLink: string; +} + +interface Props { + frameworks: [Framework] +} + +const FrameworkIntegrationSection = ({ frameworks }: Props) => { + return ( + <> +
+

Framework Integrations

+

+ Click on a framework to get the setup instructions. +

+
+
+ {frameworks.map((framework) => ( + + ))} +
+ + ); +} + +export default FrameworkIntegrationSection; + diff --git a/frontend/components/integrations/Integration.tsx b/frontend/components/integrations/Integration.tsx new file mode 100644 index 000000000..3bba41534 --- /dev/null +++ b/frontend/components/integrations/Integration.tsx @@ -0,0 +1,231 @@ +import React, { useEffect, useState } from "react"; +import { useRouter } from "next/router"; +import { + faArrowRight, + faRotate, + faX, +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import Button from "~/components/basic/buttons/Button"; +import ListBox from "~/components/basic/Listbox"; + +import deleteIntegration from "../../pages/api/integrations/DeleteIntegration" +import getIntegrationApps from "../../pages/api/integrations/GetIntegrationApps"; +import updateIntegration from "../../pages/api/integrations/updateIntegration" +import { + contextNetlifyMapping, + envMapping, + reverseContextNetlifyMapping, + reverseEnvMapping, +} from "../../public/data/frequentConstants"; + +interface Integration { + _id: string; + app?: string; + environment: string; + integration: string; + integrationAuth: string; + isActive: boolean; + context: string; +} + +interface IntegrationApp { + name: string; + siteId: string; +} + +const Integration = ({ + integration +}: { + integration: Integration; +}) => { + const [integrationEnvironment, setIntegrationEnvironment] = useState( + reverseEnvMapping[integration.environment] + ); + const [fileState, setFileState] = useState([]); + const router = useRouter(); + const [apps, setApps] = useState([]); // integration app objects + const [integrationApp, setIntegrationApp] = useState(""); // integration app name + const [integrationTarget, setIntegrationTarget] = useState(""); // vercel-specific integration param + const [integrationContext, setIntegrationContext] = useState(""); // netlify-specific integration param + + useEffect(() => { + + const loadIntegration = async () => { + interface App { + name: string; + siteId?: string; + } + + const tempApps: [IntegrationApp] = await getIntegrationApps({ + integrationAuthId: integration.integrationAuth, + }); + + setApps(tempApps); + setIntegrationApp( + integration.app ? integration.app : tempApps[0].name + ); + + switch (integration.integration) { + case "vercel": + setIntegrationTarget("Development"); + break; + case "netlify": + setIntegrationContext(integration?.context ? contextNetlifyMapping[integration.context] : "Local development"); + break; + default: + break; + } + } + + loadIntegration(); + }, []); + + const renderIntegrationSpecificParams = (integration: Integration) => { + try { + switch (integration.integration) { + case "vercel": + return ( +
+
+ ENVIRONMENT +
+ +
+ ); + case "netlify": + return ( +
+
+ CONTEXT +
+ +
+ ); + default: + return
; + } + } catch (err) { + console.error(err); + } + } + + if (!integrationApp || apps.length === 0) return
+ + return ( +
+
+
+

ENVIRONMENT

+ { + setIntegrationEnvironment(environment); + }} + isFull={true} + /> +
+
+ +
+
+

+ INTEGRATION +

+
+ {integration.integration.charAt(0).toUpperCase() + + integration.integration.slice(1)} +
+
+
+
+ APP +
+ app.name) : null} + selected={integrationApp} + onChange={(app) => { + setIntegrationApp(app); + }} + /> +
+ {renderIntegrationSpecificParams(integration)} +
+
+ {integration.isActive ? ( +
+ +
In Sync
+
+ ) : ( +
+
+
+ ); + }; + +export default Integration; \ No newline at end of file diff --git a/frontend/components/integrations/IntegrationSection.tsx b/frontend/components/integrations/IntegrationSection.tsx new file mode 100644 index 000000000..52d5565ff --- /dev/null +++ b/frontend/components/integrations/IntegrationSection.tsx @@ -0,0 +1,42 @@ +import React from "react"; + +import guidGenerator from "~/utilities/randomId"; + +import Integration from "./Integration"; + +interface Props { + integrations: any +} + +interface IntegrationType { + _id: string; + app?: string; + environment: string; + integration: string; + integrationAuth: string; + isActive: boolean; + context: string; +} + +const ProjectIntegrationSection = ({ + integrations +}: Props) => { + return integrations.length > 0 ? ( +
+
+

Current Integrations

+

+ Manage your integrations of Infisical with third-party services. +

+
+ {integrations.map((integration: IntegrationType) => ( + + ))} +
+ ) :
+} + +export default ProjectIntegrationSection; \ No newline at end of file diff --git a/frontend/components/navigation/NavBarDashboard.tsx b/frontend/components/navigation/NavBarDashboard.tsx index 6712eb30a..d79bd75b2 100644 --- a/frontend/components/navigation/NavBarDashboard.tsx +++ b/frontend/components/navigation/NavBarDashboard.tsx @@ -23,9 +23,7 @@ import getOrganization from "../../pages/api/organization/GetOrg"; import getOrganizations from "../../pages/api/organization/getOrgs"; import getUser from "../../pages/api/user/getUser"; import guidGenerator from "../utilities/randomId"; -/** - * @param {(key: string) => string} t - */ + const supportOptions = (t: TFunction) => [ [ , diff --git a/frontend/components/utilities/SecurityClient.js b/frontend/components/utilities/SecurityClient.js deleted file mode 100644 index 78eebdebe..000000000 --- a/frontend/components/utilities/SecurityClient.js +++ /dev/null @@ -1,24 +0,0 @@ -import token from "~/pages/api/auth/Token"; - -export default class SecurityClient { - static #token = ""; - - constructor() {} - - static setToken(token) { - this.#token = token; - } - - static async fetchCall(resource, options) { - let req = new Request(resource, options); - - if (this.#token == "") { - this.setToken(await token()); - } - - if (this.#token) { - req.headers.set("Authorization", "Bearer " + this.#token); - return fetch(req); - } - } -} diff --git a/frontend/components/utilities/SecurityClient.ts b/frontend/components/utilities/SecurityClient.ts new file mode 100644 index 000000000..ea2664c70 --- /dev/null +++ b/frontend/components/utilities/SecurityClient.ts @@ -0,0 +1,27 @@ +import token from '~/pages/api/auth/Token'; + +export default class SecurityClient { + static #token = ''; + + constructor() {} + + static setToken(token: string) { + this.#token = token; + } + + static async fetchCall( + resource: RequestInfo, + options?: RequestInit | undefined + ) { + const req = new Request(resource, options); + + if (this.#token == '') { + this.setToken(await token()); + } + + if (this.#token) { + req.headers.set('Authorization', 'Bearer ' + this.#token); + return fetch(req); + } + } +} diff --git a/frontend/components/utilities/attemptLogin.js b/frontend/components/utilities/attemptLogin.js index 7a8cbf96f..6bf575bd1 100644 --- a/frontend/components/utilities/attemptLogin.js +++ b/frontend/components/utilities/attemptLogin.js @@ -1,18 +1,17 @@ -import Aes256Gcm from "~/components/utilities/cryptography/aes-256-gcm"; -import login1 from "~/pages/api/auth/Login1"; -import login2 from "~/pages/api/auth/Login2"; -import getOrganizations from "~/pages/api/organization/getOrgs"; -import getOrganizationUserProjects from "~/pages/api/organization/GetOrgUserProjects"; +import Aes256Gcm from '~/components/utilities/cryptography/aes-256-gcm'; +import login1 from '~/pages/api/auth/Login1'; +import login2 from '~/pages/api/auth/Login2'; +import getOrganizations from '~/pages/api/organization/getOrgs'; +import getOrganizationUserProjects from '~/pages/api/organization/GetOrgUserProjects'; -import { initPostHog } from "../analytics/posthog"; -import pushKeys from "./secrets/pushKeys"; -import { ENV } from "./config"; -import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage"; -import SecurityClient from "./SecurityClient"; +import pushKeys from './secrets/pushKeys'; +import Telemetry from './telemetry/Telemetry'; +import { saveTokenToLocalStorage } from './saveTokenToLocalStorage'; +import SecurityClient from './SecurityClient'; -const nacl = require("tweetnacl"); -nacl.util = require("tweetnacl-util"); -const jsrp = require("jsrp"); +const nacl = require('tweetnacl'); +nacl.util = require('tweetnacl-util'); +const jsrp = require('jsrp'); const client = new jsrp.client(); /** @@ -33,17 +32,19 @@ const attemptLogin = async ( isLogin ) => { try { + const telemetry = new Telemetry().getInstance(); + client.init( { username: email, - password: password, + password: password }, async () => { const clientPublicKey = client.getPublicKey(); - const { serverPublicKey, salt } = await login1(email, clientPublicKey); - try { + const { serverPublicKey, salt } = await login1(email, clientPublicKey); + client.setSalt(salt); client.setServerPublicKey(serverPublicKey); const clientProof = client.getProof(); // called M1 @@ -53,54 +54,53 @@ const attemptLogin = async ( await login2(email, clientProof); SecurityClient.setToken(token); - const privateKey = Aes256Gcm.decrypt( - encryptedPrivateKey, + const privateKey = Aes256Gcm.decrypt({ + ciphertext: encryptedPrivateKey, iv, tag, - password + secret: password .slice(0, 32) .padStart( 32 + (password.slice(0, 32).length - new Blob([password]).size), - "0" + '0' ) - ); + }); saveTokenToLocalStorage({ - token, publicKey, encryptedPrivateKey, iv, tag, - privateKey, + privateKey }); - + const userOrgs = await getOrganizations(); const userOrgsData = userOrgs.map((org) => org._id); let orgToLogin; - if (userOrgsData.includes(localStorage.getItem("orgData.id"))) { - orgToLogin = localStorage.getItem("orgData.id"); + if (userOrgsData.includes(localStorage.getItem('orgData.id'))) { + orgToLogin = localStorage.getItem('orgData.id'); } else { orgToLogin = userOrgsData[0]; - localStorage.setItem("orgData.id", orgToLogin); + localStorage.setItem('orgData.id', orgToLogin); } let orgUserProjects = await getOrganizationUserProjects({ - orgId: orgToLogin, + orgId: orgToLogin }); orgUserProjects = orgUserProjects?.map((project) => project._id); let projectToLogin; if ( - orgUserProjects.includes(localStorage.getItem("projectData.id")) + orgUserProjects.includes(localStorage.getItem('projectData.id')) ) { - projectToLogin = localStorage.getItem("projectData.id"); + projectToLogin = localStorage.getItem('projectData.id'); } else { try { projectToLogin = orgUserProjects[0]; - localStorage.setItem("projectData.id", projectToLogin); + localStorage.setItem('projectData.id', projectToLogin); } catch (error) { - console.log("ERROR: User likely has no projects. ", error); + console.log('ERROR: User likely has no projects. ', error); } } @@ -109,45 +109,35 @@ const attemptLogin = async ( await pushKeys({ obj: { DATABASE_URL: [ - "mongodb+srv://${DB_USERNAME}:${DB_PASSWORD}@mongodb.net", - "personal", + 'mongodb+srv://${DB_USERNAME}:${DB_PASSWORD}@mongodb.net', + 'personal' ], - DB_USERNAME: ["user1234", "personal"], - DB_PASSWORD: ["ah8jak3hk8dhiu4dw7whxwe1l", "personal"], - TWILIO_AUTH_TOKEN: [ - "hgSIwDAKvz8PJfkj6xkzYqzGmAP3HLuG", - "shared", - ], - WEBSITE_URL: ["http://localhost:3000", "shared"], - STRIPE_SECRET_KEY: ["sk_test_7348oyho4hfq398HIUOH78", "shared"], + DB_USERNAME: ['user1234', 'personal'], + DB_PASSWORD: ['example_password', 'personal'], + TWILIO_AUTH_TOKEN: ['example_twillion_token', 'shared'], + WEBSITE_URL: ['http://localhost:3000', 'shared'], + STRIPE_SECRET_KEY: ['sk_test_7348oyho4hfq398HIUOH78', 'shared'] }, workspaceId: projectToLogin, - env: "Development", + env: 'Development' }); } - try { - if (email) { - if (ENV == "production") { - const posthog = initPostHog(); - posthog.identify(email); - posthog.capture("User Logged In"); - } - } - } catch (error) { - console.log("posthog", error); + if (email) { + telemetry.identify(email); + telemetry.capture('User Logged In'); } if (isLogin) { - router.push("/dashboard/"); + router.push('/dashboard/'); } } catch (error) { setErrorLogin(true); - console.log("Login response not available"); + console.log('Login response not available'); } } ); } catch (error) { - console.log("Something went wrong during authentication"); + console.log('Something went wrong during authentication'); } return true; }; diff --git a/frontend/components/utilities/checks/OnboardingCheck.ts b/frontend/components/utilities/checks/OnboardingCheck.ts new file mode 100644 index 000000000..343efadb5 --- /dev/null +++ b/frontend/components/utilities/checks/OnboardingCheck.ts @@ -0,0 +1,71 @@ +import getOrganizationUsers from '~/pages/api/organization/GetOrgUsers'; +import checkUserAction from '~/pages/api/userActions/checkUserAction'; + +interface OnboardingCheckProps { + setTotalOnboardingActionsDone?: (value: number) => void; + setHasUserClickedSlack?: (value: boolean) => void; + setHasUserClickedIntro?: (value: boolean) => void; + setHasUserStarred?: (value: boolean) => void; + setHasUserPushedSecrets?: (value: boolean) => void; + setUsersInOrg?: (value: boolean) => void; +} + +/** + * This function checks which onboarding steps a user has already finished. + */ +const onboardingCheck = async ({ + setTotalOnboardingActionsDone, + setHasUserClickedSlack, + setHasUserClickedIntro, + setHasUserStarred, + setHasUserPushedSecrets, + setUsersInOrg +}: OnboardingCheckProps) => { + let countActions = 0; + const userActionSlack = await checkUserAction({ + action: 'slack_cta_clicked' + }); + if (userActionSlack) { + countActions = countActions + 1; + } + setHasUserClickedSlack && + setHasUserClickedSlack(userActionSlack ? true : false); + + const userActionSecrets = await checkUserAction({ + action: 'first_time_secrets_pushed' + }); + if (userActionSecrets) { + countActions = countActions + 1; + } + setHasUserPushedSecrets && + setHasUserPushedSecrets(userActionSecrets ? true : false); + + const userActionIntro = await checkUserAction({ + action: 'intro_cta_clicked' + }); + if (userActionIntro) { + countActions = countActions + 1; + } + setHasUserClickedIntro && + setHasUserClickedIntro(userActionIntro ? true : false); + + const userActionStar = await checkUserAction({ + action: 'star_cta_clicked' + }); + if (userActionStar) { + countActions = countActions + 1; + } + setHasUserStarred && setHasUserStarred(userActionStar ? true : false); + + const orgId = localStorage.getItem('orgData.id'); + const orgUsers = await getOrganizationUsers({ + orgId: orgId ? orgId : '' + }); + if (orgUsers.length > 1) { + countActions = countActions + 1; + } + setUsersInOrg && setUsersInOrg(orgUsers.length > 1); + setTotalOnboardingActionsDone && setTotalOnboardingActionsDone(countActions); +}; + +export default onboardingCheck; diff --git a/frontend/components/utilities/config/index.ts b/frontend/components/utilities/config/index.ts index 62549725c..7570785ba 100644 --- a/frontend/components/utilities/config/index.ts +++ b/frontend/components/utilities/config/index.ts @@ -4,14 +4,11 @@ const POSTHOG_HOST = process.env.NEXT_PUBLIC_POSTHOG_HOST! || "https://app.posthog.com"; const STRIPE_PRODUCT_PRO = process.env.NEXT_PUBLIC_STRIPE_PRODUCT_PRO!; const STRIPE_PRODUCT_STARTER = process.env.NEXT_PUBLIC_STRIPE_PRODUCT_STARTER!; -const TELEMETRY_ENABLED = - process.env.NEXT_PUBLIC_TELEMETRY_ENABLED! !== "false"; export { ENV, POSTHOG_API_KEY, POSTHOG_HOST, STRIPE_PRODUCT_PRO, - STRIPE_PRODUCT_STARTER, - TELEMETRY_ENABLED, -}; + STRIPE_PRODUCT_STARTER +}; \ No newline at end of file diff --git a/frontend/components/utilities/cryptography/aes-256-gcm.js b/frontend/components/utilities/cryptography/aes-256-gcm.js deleted file mode 100644 index 0616813df..000000000 --- a/frontend/components/utilities/cryptography/aes-256-gcm.js +++ /dev/null @@ -1,63 +0,0 @@ -/** - * @fileoverview Provides easy encryption/decryption methods using AES 256 GCM. - */ - -"use strict"; - -const crypto = require("crypto"); - -const ALGORITHM = "aes-256-gcm"; -const BLOCK_SIZE_BYTES = 16; // 128 bit - -/** - * Provides easy encryption/decryption methods using AES 256 GCM. - */ -class Aes256Gcm { - /** - * No need to run the constructor. The class only has static methods. - */ - constructor() {} - - /** - * Encrypts text with AES 256 GCM. - * @param {string} text - Cleartext to encode. - * @param {string} secret - Shared secret key, must be 32 bytes. - * @returns {object} - */ - static encrypt(text, secret) { - const iv = crypto.randomBytes(BLOCK_SIZE_BYTES); - const cipher = crypto.createCipheriv(ALGORITHM, secret, iv); - - let ciphertext = cipher.update(text, "utf8", "base64"); - ciphertext += cipher.final("base64"); - return { - ciphertext, - iv: iv.toString("base64"), - tag: cipher.getAuthTag().toString("base64"), - }; - } - - /** - * Decrypts AES 256 CGM encrypted text. - * @param {string} ciphertext - Base64-encoded ciphertext. - * @param {string} iv - The base64-encoded initialization vector. - * @param {string} tag - The base64-encoded authentication tag generated by getAuthTag(). - * @param {string} secret - Shared secret key, must be 32 bytes. - * @returns {string} - */ - static decrypt(ciphertext, iv, tag, secret) { - const decipher = crypto.createDecipheriv( - ALGORITHM, - secret, - Buffer.from(iv, "base64") - ); - decipher.setAuthTag(Buffer.from(tag, "base64")); - - let cleartext = decipher.update(ciphertext, "base64", "utf8"); - cleartext += decipher.final("utf8"); - - return cleartext; - } -} - -module.exports = Aes256Gcm; diff --git a/frontend/components/utilities/cryptography/aes-256-gcm.ts b/frontend/components/utilities/cryptography/aes-256-gcm.ts new file mode 100644 index 000000000..aa4986dc4 --- /dev/null +++ b/frontend/components/utilities/cryptography/aes-256-gcm.ts @@ -0,0 +1,82 @@ +/** + * @fileoverview Provides easy encryption/decryption methods using AES 256 GCM. + */ + +import crypto from 'crypto'; + +const ALGORITHM = 'aes-256-gcm'; +const BLOCK_SIZE_BYTES = 16; // 128 bit + +interface EncryptProps { + text: string; + secret: string; +} + +interface DecryptProps { + ciphertext: string; + iv: string; + tag: string; + secret: string; +} + +interface EncryptOutputProps { + ciphertext: string; + iv: string; + tag: string; +} + +/** + * Provides easy encryption/decryption methods using AES 256 GCM. + */ +class Aes256Gcm { + /** + * No need to run the constructor. The class only has static methods. + */ + constructor() {} + + /** + * Encrypts text with AES 256 GCM. + * @param {object} obj + * @param {string} obj.text - Cleartext to encode. + * @param {string} obj.secret - Shared secret key, must be 32 bytes. + * @returns {object} + */ + // { ciphertext: string; iv: string; tag: string; } + static encrypt({ text, secret }: EncryptProps): EncryptOutputProps { + const iv = crypto.randomBytes(BLOCK_SIZE_BYTES); + const cipher = crypto.createCipheriv(ALGORITHM, secret, iv); + + let ciphertext = cipher.update(text, 'utf8', 'base64'); + ciphertext += cipher.final('base64'); + return { + ciphertext, + iv: iv.toString('base64'), + tag: cipher.getAuthTag().toString('base64') + }; + } + + /** + * Decrypts AES 256 CGM encrypted text. + * @param {object} obj + * @param {string} obj.ciphertext - Base64-encoded ciphertext. + * @param {string} obj.iv - The base64-encoded initialization vector. + * @param {string} obj.tag - The base64-encoded authentication tag generated by getAuthTag(). + * @param {string} obj.secret - Shared secret key, must be 32 bytes. + * @returns {string} + */ + static decrypt({ ciphertext, iv, tag, secret }: DecryptProps): string { + const decipher = crypto.createDecipheriv( + ALGORITHM, + secret, + Buffer.from(iv, 'base64') + ); + decipher.setAuthTag(Buffer.from(tag, 'base64')); + + let cleartext = decipher.update(ciphertext, 'base64', 'utf8'); + cleartext += decipher.final('utf8'); + + return cleartext; + } +} + +export default Aes256Gcm; diff --git a/frontend/components/utilities/cryptography/changePassword.js b/frontend/components/utilities/cryptography/changePassword.js index de0fd9c3a..c173b3cdf 100644 --- a/frontend/components/utilities/cryptography/changePassword.js +++ b/frontend/components/utilities/cryptography/changePassword.js @@ -1,11 +1,11 @@ -import changePassword2 from "~/pages/api/auth/ChangePassword2"; -import SRP1 from "~/pages/api/auth/SRP1"; +import changePassword2 from '~/pages/api/auth/ChangePassword2'; +import SRP1 from '~/pages/api/auth/SRP1'; -import Aes256Gcm from "./aes-256-gcm"; +import Aes256Gcm from './aes-256-gcm'; -const nacl = require("tweetnacl"); -nacl.util = require("tweetnacl-util"); -const jsrp = require("jsrp"); +const nacl = require('tweetnacl'); +nacl.util = require('tweetnacl-util'); +const jsrp = require('jsrp'); const clientOldPassword = new jsrp.client(); const clientNewPassword = new jsrp.client(); @@ -34,7 +34,7 @@ const changePassword = async ( clientOldPassword.init( { username: email, - password: currentPassword, + password: currentPassword }, async () => { const clientPublicKey = clientOldPassword.getPublicKey(); @@ -42,13 +42,13 @@ const changePassword = async ( let serverPublicKey, salt; try { const res = await SRP1({ - clientPublicKey: clientPublicKey, + clientPublicKey: clientPublicKey }); serverPublicKey = res.serverPublicKey; salt = res.salt; } catch (err) { setCurrentPasswordError(true); - console.log("Wrong current password", err, 1); + console.log('Wrong current password', err, 1); } clientOldPassword.setSalt(salt); @@ -58,27 +58,27 @@ const changePassword = async ( clientNewPassword.init( { username: email, - password: newPassword, + password: newPassword }, async () => { clientNewPassword.createVerifier(async (err, result) => { // The Blob part here is needed to account for symbols that count as 2+ bytes (e.g., รฉ, รฅ, รธ) - let { ciphertext, iv, tag } = Aes256Gcm.encrypt( - localStorage.getItem("PRIVATE_KEY"), - newPassword + const { ciphertext, iv, tag } = Aes256Gcm.encrypt({ + text: localStorage.getItem('PRIVATE_KEY'), + secret: newPassword .slice(0, 32) .padStart( 32 + (newPassword.slice(0, 32).length - new Blob([newPassword]).size), - "0" + '0' ) - ); + }); if (ciphertext) { - localStorage.setItem("encryptedPrivateKey", ciphertext); - localStorage.setItem("iv", iv); - localStorage.setItem("tag", tag); + localStorage.setItem('encryptedPrivateKey', ciphertext); + localStorage.setItem('iv', iv); + localStorage.setItem('tag', tag); let res; try { @@ -88,14 +88,14 @@ const changePassword = async ( tag, salt: result.salt, verifier: result.verifier, - clientProof, + clientProof }); if (res.status == 400) { setCurrentPasswordError(true); } else if (res.status == 200) { setPasswordChanged(true); - setCurrentPassword(""); - setNewPassword(""); + setCurrentPassword(''); + setNewPassword(''); } } catch (err) { setCurrentPasswordError(true); @@ -108,7 +108,7 @@ const changePassword = async ( } ); } catch (error) { - console.log("Something went wrong during changing the password"); + console.log('Something went wrong during changing the password'); } return true; }; diff --git a/frontend/components/utilities/cryptography/crypto.ts b/frontend/components/utilities/cryptography/crypto.ts index a50ee6a0a..409b447fc 100644 --- a/frontend/components/utilities/cryptography/crypto.ts +++ b/frontend/components/utilities/cryptography/crypto.ts @@ -1,12 +1,12 @@ -const nacl = require("tweetnacl"); -nacl.util = require("tweetnacl-util"); -const aes = require("./aes-256-gcm"); +const nacl = require('tweetnacl'); +nacl.util = require('tweetnacl-util'); +import aes from './aes-256-gcm'; type encryptAsymmetricProps = { plaintext: string; publicKey: string; privateKey: string; -} +}; /** * Return assymmetrically encrypted [plaintext] using [publicKey] where @@ -19,7 +19,11 @@ type encryptAsymmetricProps = { * @returns {String} ciphertext - base64-encoded ciphertext * @returns {String} nonce - base64-encoded nonce */ -const encryptAssymmetric = ({ plaintext, publicKey, privateKey }: encryptAsymmetricProps): object => { +const encryptAssymmetric = ({ + plaintext, + publicKey, + privateKey +}: encryptAsymmetricProps): object => { const nonce = nacl.randomBytes(24); const ciphertext = nacl.box( nacl.util.decodeUTF8(plaintext), @@ -30,7 +34,7 @@ const encryptAssymmetric = ({ plaintext, publicKey, privateKey }: encryptAsymmet return { ciphertext: nacl.util.encodeBase64(ciphertext), - nonce: nacl.util.encodeBase64(nonce), + nonce: nacl.util.encodeBase64(nonce) }; }; @@ -39,7 +43,7 @@ type decryptAsymmetricProps = { nonce: string; publicKey: string; privateKey: string; -} +}; /** * Return assymmetrically decrypted [ciphertext] using [privateKey] where @@ -49,9 +53,13 @@ type decryptAsymmetricProps = { * @param {String} obj.nonce - nonce * @param {String} obj.publicKey - base64-encoded public key of the sender * @param {String} obj.privateKey - base64-encoded private key of the receiver (current user) - * @param {String} plaintext - UTF8 plaintext */ -const decryptAssymmetric = ({ ciphertext, nonce, publicKey, privateKey }: decryptAsymmetricProps): string => { +const decryptAssymmetric = ({ + ciphertext, + nonce, + publicKey, + privateKey +}: decryptAsymmetricProps): string => { const plaintext = nacl.box.open( nacl.util.decodeBase64(ciphertext), nacl.util.decodeBase64(nonce), @@ -65,7 +73,7 @@ const decryptAssymmetric = ({ ciphertext, nonce, publicKey, privateKey }: decryp type encryptSymmetricProps = { plaintext: string; key: string; -} +}; /** * Return symmetrically encrypted [plaintext] using [key]. @@ -73,15 +81,18 @@ type encryptSymmetricProps = { * @param {String} obj.plaintext - plaintext to encrypt * @param {String} obj.key - 16-byte hex key */ -const encryptSymmetric = ({ plaintext, key }: encryptSymmetricProps): object => { +const encryptSymmetric = ({ + plaintext, + key +}: encryptSymmetricProps): object => { let ciphertext, iv, tag; try { - const obj = aes.encrypt(plaintext, key); + const obj = aes.encrypt({ text: plaintext, secret: key }); ciphertext = obj.ciphertext; iv = obj.iv; tag = obj.tag; } catch (err) { - console.log("Failed to perform encryption"); + console.log('Failed to perform encryption'); console.log(err); process.exit(1); } @@ -89,7 +100,7 @@ const encryptSymmetric = ({ plaintext, key }: encryptSymmetricProps): object => return { ciphertext, iv, - tag, + tag }; }; @@ -98,7 +109,7 @@ type decryptSymmetricProps = { iv: string; tag: string; key: string; -} +}; /** * Return symmetrically decypted [ciphertext] using [iv], [tag], @@ -110,12 +121,17 @@ type decryptSymmetricProps = { * @param {String} obj.key - 32-byte hex key * */ -const decryptSymmetric = ({ ciphertext, iv, tag, key }: decryptSymmetricProps): string => { +const decryptSymmetric = ({ + ciphertext, + iv, + tag, + key +}: decryptSymmetricProps): string => { let plaintext; try { - plaintext = aes.decrypt(ciphertext, iv, tag, key); + plaintext = aes.decrypt({ ciphertext, iv, tag, secret: key }); } catch (err) { - console.log("Failed to perform decryption"); + console.log('Failed to perform decryption'); process.exit(1); } @@ -126,5 +142,5 @@ export { decryptAssymmetric, decryptSymmetric, encryptAssymmetric, - encryptSymmetric, + encryptSymmetric }; diff --git a/frontend/components/utilities/cryptography/issueBackupKey.js b/frontend/components/utilities/cryptography/issueBackupKey.js deleted file mode 100644 index 19d7ebefa..000000000 --- a/frontend/components/utilities/cryptography/issueBackupKey.js +++ /dev/null @@ -1,98 +0,0 @@ -import issueBackupPrivateKey from "~/pages/api/auth/IssueBackupPrivateKey"; -import SRP1 from "~/pages/api/auth/SRP1"; - -import generateBackupPDF from "../generateBackupPDF"; -import Aes256Gcm from "./aes-256-gcm"; - -const nacl = require("tweetnacl"); -nacl.util = require("tweetnacl-util"); -const jsrp = require("jsrp"); -const clientPassword = new jsrp.client(); -const clientKey = new jsrp.client(); -const crypto = require("crypto"); - -/** - * This function loggs in the user (whether it's right after signup, or a normal login) - * @param {*} email - * @param {*} password - * @param {*} setErrorLogin - * @param {*} router - * @param {*} isSignUp - * @returns - */ -const issueBackupKey = async ({ - email, - password, - personalName, - setBackupKeyError, - setBackupKeyIssued, -}) => { - try { - setBackupKeyError(false); - setBackupKeyIssued(false); - clientPassword.init( - { - username: email, - password: password, - }, - async () => { - const clientPublicKey = clientPassword.getPublicKey(); - - let serverPublicKey, salt; - try { - const res = await SRP1({ - clientPublicKey: clientPublicKey, - }); - serverPublicKey = res.serverPublicKey; - salt = res.salt; - } catch (err) { - setBackupKeyError(true); - console.log("Wrong current password", err, 1); - } - - clientPassword.setSalt(salt); - clientPassword.setServerPublicKey(serverPublicKey); - const clientProof = clientPassword.getProof(); // called M1 - - const generatedKey = crypto.randomBytes(16).toString("hex"); - - clientKey.init( - { - username: email, - password: generatedKey, - }, - async () => { - clientKey.createVerifier(async (err, result) => { - let { ciphertext, iv, tag } = Aes256Gcm.encrypt( - localStorage.getItem("PRIVATE_KEY"), - generatedKey - ); - - const res = await issueBackupPrivateKey({ - encryptedPrivateKey: ciphertext, - iv, - tag, - salt: result.salt, - verifier: result.verifier, - clientProof, - }); - - if (res.status == 400) { - setBackupKeyError(true); - } else if (res.status == 200) { - generateBackupPDF(personalName, email, generatedKey); - setBackupKeyIssued(true); - } - }); - } - ); - } - ); - } catch (error) { - setBackupKeyError(true); - console.log("Failed to issue a backup key"); - } - return true; -}; - -export default issueBackupKey; diff --git a/frontend/components/utilities/cryptography/issueBackupKey.ts b/frontend/components/utilities/cryptography/issueBackupKey.ts new file mode 100644 index 000000000..147377b77 --- /dev/null +++ b/frontend/components/utilities/cryptography/issueBackupKey.ts @@ -0,0 +1,113 @@ +import issueBackupPrivateKey from '~/pages/api/auth/IssueBackupPrivateKey'; +import SRP1 from '~/pages/api/auth/SRP1'; + +import generateBackupPDF from '../generateBackupPDF'; +import Aes256Gcm from './aes-256-gcm'; + +const nacl = require('tweetnacl'); +nacl.util = require('tweetnacl-util'); +const jsrp = require('jsrp'); +const clientPassword = new jsrp.client(); +const clientKey = new jsrp.client(); +const crypto = require('crypto'); + +interface BackupKeyProps { + email: string; + password: string; + personalName: string; + setBackupKeyError: (value: boolean) => void; + setBackupKeyIssued: (value: boolean) => void; +} + +/** + * This function issue a backup key for a user + * @param {obkect} obj + * @param {string} obj.email - email of a user issuing a backup key + * @param {string} obj.password - password of a user issuing a backup key + * @param {string} obj.personalName - name of a user issuing a backup key + * @param {function} obj.setBackupKeyError - state function that turns true if there is an erorr with a backup key + * @param {function} obj.setBackupKeyIssued - state function that turns true if a backup key was issued correctly + * @returns + */ +const issueBackupKey = async ({ + email, + password, + personalName, + setBackupKeyError, + setBackupKeyIssued +}: BackupKeyProps) => { + try { + setBackupKeyError(false); + setBackupKeyIssued(false); + clientPassword.init( + { + username: email, + password: password + }, + async () => { + const clientPublicKey = clientPassword.getPublicKey(); + + let serverPublicKey, salt; + try { + const res = await SRP1({ + clientPublicKey: clientPublicKey + }); + serverPublicKey = res.serverPublicKey; + salt = res.salt; + } catch (err) { + setBackupKeyError(true); + console.log('Wrong current password', err, 1); + } + + clientPassword.setSalt(salt); + clientPassword.setServerPublicKey(serverPublicKey); + const clientProof = clientPassword.getProof(); // called M1 + + const generatedKey = crypto.randomBytes(16).toString('hex'); + + clientKey.init( + { + username: email, + password: generatedKey + }, + async () => { + clientKey.createVerifier( + async (err: any, result: { salt: string; verifier: string }) => { + const { ciphertext, iv, tag } = Aes256Gcm.encrypt({ + text: String(localStorage.getItem('PRIVATE_KEY')), + secret: generatedKey + }); + + const res = await issueBackupPrivateKey({ + encryptedPrivateKey: ciphertext, + iv, + tag, + salt: result.salt, + verifier: result.verifier, + clientProof + }); + + if (res?.status == 400) { + setBackupKeyError(true); + } else if (res?.status == 200) { + generateBackupPDF({ + personalName, + personalEmail: email, + generatedKey + }); + setBackupKeyIssued(true); + } + } + ); + } + ); + } + ); + } catch (error) { + setBackupKeyError(true); + console.log('Failed to issue a backup key'); + } + return true; +}; + +export default issueBackupKey; diff --git a/frontend/components/utilities/file.js b/frontend/components/utilities/file.ts similarity index 74% rename from frontend/components/utilities/file.js rename to frontend/components/utilities/file.ts index 96d1820eb..3784405f9 100644 --- a/frontend/components/utilities/file.js +++ b/frontend/components/utilities/file.ts @@ -6,21 +6,21 @@ const LINE = * @param {Buffer} src - source buffer * @returns {String} text - text of buffer */ -function parse(src) { - const obj = {}; +function parse(src: Buffer) { + const obj: Record = {}; // Convert buffer to string let lines = src.toString(); // Convert line breaks to same format - lines = lines.replace(/\r\n?/gm, "\n"); + lines = lines.replace(/\r\n?/gm, '\n'); let match; while ((match = LINE.exec(lines)) != null) { const key = match[1]; // Default undefined or null to empty string - let value = match[2] || ""; + let value = match[2] || ''; // Remove whitespace value = value.trim(); @@ -29,12 +29,12 @@ function parse(src) { const maybeQuote = value[0]; // Remove surrounding quotes - value = value.replace(/^(['"`])([\s\S]*)\1$/gm, "$2"); + value = value.replace(/^(['"`])([\s\S]*)\1$/gm, '$2'); // Expand newlines if double quoted if (maybeQuote === '"') { - value = value.replace(/\\n/g, "\n"); - value = value.replace(/\\r/g, "\r"); + value = value.replace(/\\n/g, '\n'); + value = value.replace(/\\r/g, '\r'); } // Add to object diff --git a/frontend/components/utilities/generateBackupPDF.js b/frontend/components/utilities/generateBackupPDF.ts similarity index 94% rename from frontend/components/utilities/generateBackupPDF.js rename to frontend/components/utilities/generateBackupPDF.ts index 58e0b6667..d47bdfe73 100644 --- a/frontend/components/utilities/generateBackupPDF.js +++ b/frontend/components/utilities/generateBackupPDF.ts @@ -1,86 +1,96 @@ -import { jsPDF } from "jspdf"; +import { jsPDF } from 'jspdf'; + +interface PDFProps { + personalName: string; + personalEmail: string; + generatedKey: string; +} /** * This function generate a pdf with a secret key for a user. */ -function generateBackupPDF(personalName, personalEmail, generatedKey) { +function generateBackupPDF({ + personalName, + personalEmail, + generatedKey +}: PDFProps) { const imgData = - "data:image/png;base64,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"; + 'data:image/png;base64,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'; // new jsPDF('p', 'mm', [297, 210]); - var today = new Date(); - var dd = String(today.getDate()).padStart(2, "0"); - var mm = String(today.getMonth() + 1).padStart(2, "0"); //January is 0! - var yyyy = today.getFullYear(); + const today = new Date(); + const dd = String(today.getDate()).padStart(2, '0'); + const mm = String(today.getMonth() + 1).padStart(2, '0'); //January is 0! + const yyyy = today.getFullYear(); - today = mm + "/" + dd + "/" + yyyy; + const todayFormatted = mm + '/' + dd + '/' + yyyy; - var doc = new jsPDF("p", "pt"); - doc.setFillColor(13, 17, 23); - doc.rect(0, 0, 600, 900, "F"); - doc.setTextColor(227, 227, 227); - doc.addImage(imgData, "png", 30, 35, 535, 72); - doc.addFont("helvetica", "normal"); + const doc = new jsPDF('p', 'pt'); + doc.setFillColor(255, 255, 255); + doc.rect(0, 0, 600, 900, 'F'); + doc.setTextColor(23, 23, 23); + doc.addImage(imgData, 'png', 30, 35, 535, 72); + doc.addFont('helvetica', 'normal', ''); doc.setFontSize(12); doc.text( + 'Created for ' + personalName + ' on ' + todayFormatted + '.', 290, 130, - "Created for " + personalName + " on " + today + ".", - "center" + { align: 'center' } ); doc.setFontSize(14); doc.text( + 'In case you get locked out of you Infisical account, you`ll need these account details', 32, - 180, - "In case you get locked out of you Infisical account, you`ll need these account details" + 180 ); - doc.text(32, 200, "to sign in โ€”"); - doc.setFont(undefined, "bold"); + doc.text('to sign in โ€”', 32, 200); + doc.setFont('helvetica', 'bold'); doc.text( + 'including your Secret Key, which we absolutely cannot access or', 110, - 200, - "including your Secret Key, which we absolutely cannot access or" + 200 ); - doc.text(32, 220, "recover for you. "); - doc.setFont(undefined, "normal"); - doc.text(32, 250, "Recommendations:"); + doc.text('recover for you. ', 32, 220); + doc.setFont('helvetica', 'normal'); + doc.text('Recommendations:', 32, 250); doc.text( + '1. We recommend to get your Emergency Kit off your computer and print a copy.', 32, - 280, - "1. We recommend to get your Emergency Kit off your computer and print a copy." + 280 ); doc.text( + '2. Store it somewhere safe (such as with your birth certificate, your will, or on your', 32, - 310, - "2. Store it somewhere safe (such as with your birth certificate, your will, or on your" + 310 ); - doc.text(32, 330, "personal cloud storage)."); - doc.setFillColor(206, 217, 111); - doc.roundedRect(32, 350, 530, 190, 5, 5, "F"); + doc.text('personal cloud storage).', 32, 330); + doc.setFillColor(251, 255, 158); + doc.roundedRect(32, 350, 530, 190, 5, 5, 'F'); doc.setDrawColor(228, 255, 0); doc.setLineWidth(1); - doc.roundedRect(32, 350, 530, 190, 5, 5, "S"); + doc.roundedRect(32, 350, 530, 190, 5, 5, 'S'); doc.setTextColor(43, 43, 43); - doc.setFont(undefined, "bold"); + doc.setFont('helvetica', 'bold'); doc.setFontSize(15); - doc.text(290, 375, "Infisical Account Details", "center"); - doc.setFont(undefined, "normal"); + doc.text('Infisical Account Details', 290, 375, { align: 'center' }); doc.setFontSize(12); - doc.text(50, 420, "SIGN-IN URL"); - doc.text(50, 465, "EMAIL ADDRESS"); - doc.text(50, 510, "SECRET KEY"); - doc.setFillColor(23, 27, 33); - doc.roundedRect(170, 398, 375, 35, 5, 5, "F"); - doc.roundedRect(170, 443, 375, 35, 5, 5, "F"); - doc.roundedRect(170, 488, 375, 35, 5, 5, "F"); - doc.setTextColor(227, 227, 227); + doc.text('SIGN-IN URL', 50, 420); + doc.text('EMAIL ADDRESS', 50, 465); + doc.text('SECRET KEY', 50, 510); + doc.setFont('helvetica', 'normal'); + doc.setFillColor(254, 255, 235); + doc.roundedRect(170, 398, 375, 35, 5, 5, 'F'); + doc.roundedRect(170, 443, 375, 35, 5, 5, 'F'); + doc.roundedRect(170, 488, 375, 35, 5, 5, 'F'); + doc.setTextColor(23, 23, 23); doc.setFontSize(14); - doc.text(180, 420, "https://app.infisical.com/login"); - doc.text(180, 465, personalEmail); - doc.text(180, 510, generatedKey); - doc.text(32, 575, "Need help? Contact us at support@infisical.com"); + doc.text('https://app.infisical.com/login', 180, 420); + doc.text(personalEmail, 180, 465); + doc.text(generatedKey, 180, 510); + doc.text('Need help? Contact us at support@infisical.com', 32, 575); - doc.save("Infisical Emergency Kit.pdf"); + doc.save('Infisical Emergency Kit.pdf'); } export default generateBackupPDF; diff --git a/frontend/components/utilities/randomId.js b/frontend/components/utilities/randomId.ts similarity index 82% rename from frontend/components/utilities/randomId.js rename to frontend/components/utilities/randomId.ts index 7d1c0859b..8e1c7f972 100644 --- a/frontend/components/utilities/randomId.js +++ b/frontend/components/utilities/randomId.ts @@ -3,19 +3,19 @@ * @returns */ const guidGenerator = () => { - var S4 = function () { + const S4 = function () { return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1); }; return ( S4() + S4() + - "-" + + '-' + S4() + - "-" + + '-' + S4() + - "-" + + '-' + S4() + - "-" + + '-' + S4() + S4() + S4() diff --git a/frontend/components/utilities/saveTokenToLocalStorage.ts b/frontend/components/utilities/saveTokenToLocalStorage.ts index 13e50b2c4..35b336181 100644 --- a/frontend/components/utilities/saveTokenToLocalStorage.ts +++ b/frontend/components/utilities/saveTokenToLocalStorage.ts @@ -3,7 +3,7 @@ interface Props { encryptedPrivateKey: string; iv: string; tag: string; - privateTag: string; + privateKey: string; } export const saveTokenToLocalStorage = ({ @@ -11,14 +11,14 @@ export const saveTokenToLocalStorage = ({ encryptedPrivateKey, iv, tag, - privateTag, + privateKey, }: Props) => { try { localStorage.setItem("publicKey", publicKey); localStorage.setItem("encryptedPrivateKey", encryptedPrivateKey); localStorage.setItem("iv", iv); localStorage.setItem("tag", tag); - localStorage.setItem("PRIVATE_KEY", privateTag); + localStorage.setItem("PRIVATE_KEY", privateKey); } catch (err) { if (err instanceof Error) { throw new Error( diff --git a/frontend/components/utilities/secrets/getSecretsForProject.js b/frontend/components/utilities/secrets/getSecretsForProject.js deleted file mode 100644 index 7bdac922f..000000000 --- a/frontend/components/utilities/secrets/getSecretsForProject.js +++ /dev/null @@ -1,97 +0,0 @@ -import getSecrets from "~/pages/api/files/GetSecrets"; - -import { envMapping } from "../../../public/data/frequentConstants"; -import guidGenerator from "../randomId"; - -const { - decryptAssymmetric, - decryptSymmetric, -} = require("../cryptography/crypto"); -const nacl = require("tweetnacl"); -nacl.util = require("tweetnacl-util"); - -const getSecretsForProject = async ({ - env, - setFileState, - setIsKeyAvailable, - setData, - workspaceId, -}) => { - try { - let file; - try { - file = await getSecrets(workspaceId, envMapping[env]); - - setFileState(file); - } catch (error) { - console.log("ERROR: Not able to access the latest file"); - } - // This is called isKeyAvilable but what it really means is if a person is able to create new key pairs - setIsKeyAvailable( - !file.key ? (file.secrets.length == 0 ? true : false) : true - ); - - const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY"); - - let tempFileState = []; - if (file.key) { - // assymmetrically decrypt symmetric key with local private key - const key = decryptAssymmetric({ - ciphertext: file.key.encryptedKey, - nonce: file.key.nonce, - publicKey: file.key.sender.publicKey, - privateKey: PRIVATE_KEY, - }); - - file.secrets.map((secretPair) => { - // decrypt .env file with symmetric key - const plainTextKey = decryptSymmetric({ - ciphertext: secretPair.secretKey.ciphertext, - iv: secretPair.secretKey.iv, - tag: secretPair.secretKey.tag, - key, - }); - - const plainTextValue = decryptSymmetric({ - ciphertext: secretPair.secretValue.ciphertext, - iv: secretPair.secretValue.iv, - tag: secretPair.secretValue.tag, - key, - }); - tempFileState.push({ - key: plainTextKey, - value: plainTextValue, - type: secretPair.type, - }); - }); - } - setFileState(tempFileState); - - setData( - tempFileState.map((line, index) => [ - guidGenerator(), - index, - line["key"], - line["value"], - line["type"], - ]) - // .sort((a, b) => - // sortMethod == "alphabetical" - // ? a[2].localeCompare(b[2]) - // : b[2].localeCompare(a[2]) - // ) - ); - return tempFileState.map((line, index) => [ - guidGenerator(), - index, - line["key"], - line["value"], - line["type"], - ]); - } catch (error) { - console.log("Something went wrong during accessing or decripting secrets."); - } - return true; -}; - -export default getSecretsForProject; diff --git a/frontend/components/utilities/secrets/getSecretsForProject.ts b/frontend/components/utilities/secrets/getSecretsForProject.ts new file mode 100644 index 000000000..3b63f9177 --- /dev/null +++ b/frontend/components/utilities/secrets/getSecretsForProject.ts @@ -0,0 +1,103 @@ +import getSecrets from '~/pages/api/files/GetSecrets'; + +import { envMapping } from '../../../public/data/frequentConstants'; +import guidGenerator from '../randomId'; + +const { + decryptAssymmetric, + decryptSymmetric +} = require('../cryptography/crypto'); +const nacl = require('tweetnacl'); +nacl.util = require('tweetnacl-util'); + +interface Props { + env: keyof typeof envMapping; + setFileState: any; + setIsKeyAvailable: any; + setData: any; + workspaceId: string; +} + +const getSecretsForProject = async ({ + env, + setFileState, + setIsKeyAvailable, + setData, + workspaceId +}: Props) => { + try { + let file; + try { + file = await getSecrets(workspaceId, envMapping[env]); + + setFileState(file); + } catch (error) { + console.log('ERROR: Not able to access the latest file'); + } + // This is called isKeyAvilable but what it really means is if a person is able to create new key pairs + setIsKeyAvailable(!file.key ? file.secrets.length == 0 : true); + + const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY'); + + const tempFileState: { key: string; value: string; type: string }[] = []; + if (file.key) { + // assymmetrically decrypt symmetric key with local private key + const key = decryptAssymmetric({ + ciphertext: file.key.encryptedKey, + nonce: file.key.nonce, + publicKey: file.key.sender.publicKey, + privateKey: PRIVATE_KEY + }); + + file.secrets.map((secretPair: any) => { + // decrypt .env file with symmetric key + const plainTextKey = decryptSymmetric({ + ciphertext: secretPair.secretKey.ciphertext, + iv: secretPair.secretKey.iv, + tag: secretPair.secretKey.tag, + key + }); + + const plainTextValue = decryptSymmetric({ + ciphertext: secretPair.secretValue.ciphertext, + iv: secretPair.secretValue.iv, + tag: secretPair.secretValue.tag, + key + }); + tempFileState.push({ + key: plainTextKey, + value: plainTextValue, + type: secretPair.type + }); + }); + } + setFileState(tempFileState); + + setData( + tempFileState.map((line, index) => { + return { + id: guidGenerator(), + pos: index, + key: line['key'], + value: line['value'], + type: line['type'] + }; + }) + ); + + return tempFileState.map((line, index) => { + return { + id: guidGenerator(), + pos: index, + key: line['key'], + value: line['value'], + type: line['type'] + }; + }); + } catch (error) { + console.log('Something went wrong during accessing or decripting secrets.'); + } + return true; +}; + +export default getSecretsForProject; diff --git a/frontend/components/utilities/secrets/pushKeysIntegration.js b/frontend/components/utilities/secrets/pushKeysIntegration.js deleted file mode 100644 index 5b08748e7..000000000 --- a/frontend/components/utilities/secrets/pushKeysIntegration.js +++ /dev/null @@ -1,74 +0,0 @@ -import publicKeyInfical from "~/pages/api/auth/publicKeyInfisical"; -import changeHerokuConfigVars from "~/pages/api/integrations/ChangeHerokuConfigVars"; - -const crypto = require("crypto"); -const { - encryptSymmetric, - encryptAssymmetric, -} = require("../cryptography/crypto"); -const nacl = require("tweetnacl"); -nacl.util = require("tweetnacl-util"); - -const pushKeysIntegration = async ({ obj, integrationId }) => { - const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY"); - - let randomBytes = crypto.randomBytes(16).toString("hex"); - - const secrets = Object.keys(obj).map((key) => { - // encrypt key - const { - ciphertext: ciphertextKey, - iv: ivKey, - tag: tagKey, - } = encryptSymmetric({ - plaintext: key, - key: randomBytes, - }); - - // encrypt value - const { - ciphertext: ciphertextValue, - iv: ivValue, - tag: tagValue, - } = encryptSymmetric({ - plaintext: obj[key], - key: randomBytes, - }); - - const visibility = "shared"; - - return { - ciphertextKey, - ivKey, - tagKey, - hashKey: crypto.createHash("sha256").update(key).digest("hex"), - ciphertextValue, - ivValue, - tagValue, - hashValue: crypto.createHash("sha256").update(obj[key]).digest("hex"), - type: visibility, - }; - }); - - // obtain public keys of all receivers (i.e. members in workspace) - let publicKeyInfisical = await publicKeyInfical(); - - publicKeyInfisical = (await publicKeyInfisical.json()).publicKey; - - // assymmetrically encrypt key with each receiver public keys - - const { ciphertext, nonce } = encryptAssymmetric({ - plaintext: randomBytes, - publicKey: publicKeyInfisical, - privateKey: PRIVATE_KEY, - }); - - const key = { - encryptedKey: ciphertext, - nonce, - }; - - changeHerokuConfigVars({ integrationId, key, secrets }); -}; - -export default pushKeysIntegration; diff --git a/frontend/components/utilities/secrets/pushKeysIntegration.ts b/frontend/components/utilities/secrets/pushKeysIntegration.ts new file mode 100644 index 000000000..949778927 --- /dev/null +++ b/frontend/components/utilities/secrets/pushKeysIntegration.ts @@ -0,0 +1,79 @@ +import publicKeyInfical from '~/pages/api/auth/publicKeyInfisical'; +import changeHerokuConfigVars from '~/pages/api/integrations/ChangeHerokuConfigVars'; + +const crypto = require('crypto'); +const { + encryptSymmetric, + encryptAssymmetric +} = require('../cryptography/crypto'); +const nacl = require('tweetnacl'); +nacl.util = require('tweetnacl-util'); + +interface Props { + obj: Record; + integrationId: string; +} + +const pushKeysIntegration = async ({ obj, integrationId }: Props) => { + const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY'); + + const randomBytes = crypto.randomBytes(16).toString('hex'); + + const secrets = Object.keys(obj).map((key) => { + // encrypt key + const { + ciphertext: ciphertextKey, + iv: ivKey, + tag: tagKey + } = encryptSymmetric({ + plaintext: key, + key: randomBytes + }); + + // encrypt value + const { + ciphertext: ciphertextValue, + iv: ivValue, + tag: tagValue + } = encryptSymmetric({ + plaintext: obj[key], + key: randomBytes + }); + + const visibility = 'shared'; + + return { + ciphertextKey, + ivKey, + tagKey, + hashKey: crypto.createHash('sha256').update(key).digest('hex'), + ciphertextValue, + ivValue, + tagValue, + hashValue: crypto.createHash('sha256').update(obj[key]).digest('hex'), + type: visibility + }; + }); + + // obtain public keys of all receivers (i.e. members in workspace) + const publicKeyInfisical = await publicKeyInfical(); + + const publicKey = (await publicKeyInfisical.json()).publicKey; + + // assymmetrically encrypt key with each receiver public keys + + const { ciphertext, nonce } = encryptAssymmetric({ + plaintext: randomBytes, + publicKey, + privateKey: PRIVATE_KEY + }); + + const key = { + encryptedKey: ciphertext, + nonce + }; + + changeHerokuConfigVars({ integrationId, key, secrets }); +}; + +export default pushKeysIntegration; diff --git a/frontend/components/utilities/telemetry/Telemetry.js b/frontend/components/utilities/telemetry/Telemetry.js new file mode 100644 index 000000000..92bdc078e --- /dev/null +++ b/frontend/components/utilities/telemetry/Telemetry.js @@ -0,0 +1,44 @@ +/* eslint-disable */ +import { initPostHog } from "~/components/analytics/posthog"; +import { ENV } from "~/components/utilities/config"; + +class Capturer { + constructor() { + this.api = initPostHog(); + } + + capture(item) { + if (ENV == "production" && TELEMETRY_CAPTURING_ENABLED) { + try { + api.capture(item); + } catch (error) { + console.error("PostHog", error); + } + } + } + + identify(id) { + if (ENV == "production" && TELEMETRY_CAPTURING_ENABLED) { + try { + api.identify(id); + } catch (error) { + console.error("PostHog", error); + } + } + } + +} + +class Telemetry { + constructor() { + if (!Telemetry.instance) { + Telemetry.instance = new Capturer(); + } + } + + getInstance() { + return Telemetry.instance; + } +} + +module.exports = Telemetry; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 0334604c5..639fa7052 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -24,24 +24,27 @@ "fs": "^0.0.1-security", "gray-matter": "^4.0.3", "http-proxy": "^1.18.1", + "i18next": "^22.4.6", "jspdf": "^2.5.1", "jsrp": "^0.2.4", "markdown-it": "^13.0.1", "next": "^12.2.5", - "next-translate": "^1.6.0", + "next-i18next": "^13.0.2", "posthog-js": "^1.34.0", - "query-string": "^7.1.1", + "query-string": "^7.1.3", "react": "^17.0.2", "react-beautiful-dnd": "^13.1.1", "react-code-input": "^3.10.1", "react-dom": "^17.0.2", "react-github-btn": "^1.4.0", "react-grid-layout": "^1.3.4", + "react-i18next": "^12.1.1", "react-mailchimp-subscribe": "^2.1.3", "react-markdown": "^8.0.3", "react-redux": "^8.0.2", "react-table": "^7.8.0", "set-cookie-parser": "^2.5.1", + "sharp": "^0.31.2", "styled-components": "^5.3.5", "tweetnacl": "^1.0.3", "tweetnacl-util": "^0.15.1", @@ -52,22 +55,43 @@ "@tailwindcss/typography": "^0.5.4", "@types/node": "18.11.9", "@types/react": "^18.0.26", - "@typescript-eslint/eslint-plugin": "^5.45.0", "@typescript-eslint/parser": "^5.45.0", "autoprefixer": "^10.4.7", "eslint": "^8.29.0", "eslint-config-next": "^13.0.5", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-simple-import-sort": "^8.0.0", - "i18next": "^22.1.4", - "next-i18next": "^13.0.0", "postcss": "^8.4.14", - "prettier": "2.7.1", - "react-i18next": "^12.1.1", "tailwindcss": "^3.1.4", "typescript": "^4.9.3" } }, + "node_modules/@ampproject/remapping": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", + "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "peer": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@ampproject/remapping/node_modules/@jridgewell/gen-mapping": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", + "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "peer": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", @@ -79,12 +103,83 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/generator": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.19.0.tgz", - "integrity": "sha512-S1ahxf1gZ2dpoiFgA+ohK9DIpz50bJ0CWs7Zlzb54Z4sG8qmdIrGrVqmy1sAtTVRb+9CU6U8VqT9L0Zj7hxHVg==", + "node_modules/@babel/compat-data": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.5.tgz", + "integrity": "sha512-KZXo2t10+/jxmkhNXc7pZTqRvSOIvVv/+lJwHS+B2rErwOyjuVRh60yVpb7liQ1U5t7lLJ1bz+t8tSypUZdm0g==", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.5.tgz", + "integrity": "sha512-UdOWmk4pNWTm/4DlPUl/Pt4Gz4rcEMb7CY0Y3eJl5Yz1vI8ZJGmHWaVE55LoxRjdpx0z259GE9U5STA9atUinQ==", + "peer": true, "dependencies": { - "@babel/types": "^7.19.0", + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.20.5", + "@babel/helper-compilation-targets": "^7.20.0", + "@babel/helper-module-transforms": "^7.20.2", + "@babel/helpers": "^7.20.5", + "@babel/parser": "^7.20.5", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.20.5", + "@babel/types": "^7.20.5", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "peer": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/core/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "peer": true + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.5.tgz", + "integrity": "sha512-jl7JY2Ykn9S0yj4DQP82sYvPU+T3g0HFcWTqDLqiuA9tGRNIj9VfbtXGAYTTkyNEnQk1jkMGOdYka8aG/lulCA==", + "dependencies": { + "@babel/types": "^7.20.5", "@jridgewell/gen-mapping": "^0.3.2", "jsesc": "^2.5.1" }, @@ -103,6 +198,33 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.0.tgz", + "integrity": "sha512-0jp//vDGp9e8hZzBc6N/KwA5ZK3Wsm/pfm4CrY7vzegkVxc65SgSn6wYOnwHe9Js9HRQ1YTCKLGPzDtaS3RoLQ==", + "peer": true, + "dependencies": { + "@babel/compat-data": "^7.20.0", + "@babel/helper-validator-option": "^7.18.6", + "browserslist": "^4.21.3", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/helper-environment-visitor": { "version": "7.18.9", "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", @@ -145,6 +267,25 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.20.2.tgz", + "integrity": "sha512-zvBKyJXRbmK07XhMuujYoJ48B5yvvmM6+wcpv6Ivj4Yg6qO7NOZOSnvZN9CRl1zz1Z4cKf8YejmCMh8clOoOeA==", + "peer": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-simple-access": "^7.20.2", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/helper-validator-identifier": "^7.19.1", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.20.1", + "@babel/types": "^7.20.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-plugin-utils": { "version": "7.19.0", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.19.0.tgz", @@ -153,6 +294,18 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-simple-access": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz", + "integrity": "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==", + "peer": true, + "dependencies": { + "@babel/types": "^7.20.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-split-export-declaration": { "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", @@ -165,17 +318,40 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.18.10.tgz", - "integrity": "sha512-XtIfWmeNY3i4t7t4D2t02q50HvqHybPqW2ki1kosnvWCwuCMeo81Jf0gwr85jy/neUdg5XDdeFE/80DXiO+njw==", + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", + "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz", - "integrity": "sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz", + "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.6.tgz", + "integrity": "sha512-Pf/OjgfgFRW5bApskEz5pvidpim7tEDPlFtKcNRXWmfHGn9IEI2W2flqRQXTFb7gIPTyK++N6rVHuwKut4XK6w==", + "peer": true, + "dependencies": { + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.20.5", + "@babel/types": "^7.20.5" + }, "engines": { "node": ">=6.9.0" } @@ -194,9 +370,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.19.1.tgz", - "integrity": "sha512-h7RCSorm1DdTVGJf3P2Mhj3kdnkmF/EiysUkzS2TdgAYqyjFdMQJbVuXOBej2SBJaXan/lIVtT6KkGbyyq753A==", + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.5.tgz", + "integrity": "sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA==", "bin": { "parser": "bin/babel-parser.js" }, @@ -219,9 +395,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.20.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.6.tgz", - "integrity": "sha512-Q+8MqP7TiHMWzSfwiJwXCjyf4GYA4Dgw3emg/7xmwsdLJOZUp+nMqcOwOzzYheuM1rhDu8FSj2l0aoMygEuXuA==", + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.7.tgz", + "integrity": "sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==", "dependencies": { "regenerator-runtime": "^0.13.11" }, @@ -256,18 +432,18 @@ } }, "node_modules/@babel/traverse": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.19.1.tgz", - "integrity": "sha512-0j/ZfZMxKukDaag2PtOPDbwuELqIar6lLskVPPJDjXMXjfLb1Obo/1yjxIGqqAJrmfaTIY3z2wFLAQ7qSkLsuA==", + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.5.tgz", + "integrity": "sha512-WM5ZNN3JITQIq9tFZaw1ojLU3WgWdtkxnhM1AegMS+PvHjkM5IXjmYEGY7yukz5XS4sJyEf2VzWjI8uAavhxBQ==", "dependencies": { "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.19.0", + "@babel/generator": "^7.20.5", "@babel/helper-environment-visitor": "^7.18.9", "@babel/helper-function-name": "^7.19.0", "@babel/helper-hoist-variables": "^7.18.6", "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.19.1", - "@babel/types": "^7.19.0", + "@babel/parser": "^7.20.5", + "@babel/types": "^7.20.5", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -297,12 +473,12 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/@babel/types": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.19.0.tgz", - "integrity": "sha512-YuGopBq3ke25BVSiS6fgF49Ul9gH1x70Bcr6bqRLjWCkcX8Hre1/5+z+IiWOIerRMSSEfGZVB9z9kyq7wVs9YA==", + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.5.tgz", + "integrity": "sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==", "dependencies": { - "@babel/helper-string-parser": "^7.18.10", - "@babel/helper-validator-identifier": "^7.18.6", + "@babel/helper-string-parser": "^7.19.4", + "@babel/helper-validator-identifier": "^7.19.1", "to-fast-properties": "^2.0.0" }, "engines": { @@ -1094,12 +1270,6 @@ "hoist-non-react-statics": "^3.3.0" } }, - "node_modules/@types/json-schema": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", - "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", - "dev": true - }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -1172,12 +1342,6 @@ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" }, - "node_modules/@types/semver": { - "version": "7.3.13", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", - "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==", - "dev": true - }, "node_modules/@types/unist": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz", @@ -1193,62 +1357,6 @@ "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==" }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.45.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.45.0.tgz", - "integrity": "sha512-CXXHNlf0oL+Yg021cxgOdMHNTXD17rHkq7iW6RFHoybdFgQBjU3yIXhhcPpGwr1CjZlo6ET8C6tzX5juQoXeGA==", - "dev": true, - "dependencies": { - "@typescript-eslint/scope-manager": "5.45.0", - "@typescript-eslint/type-utils": "5.45.0", - "@typescript-eslint/utils": "5.45.0", - "debug": "^4.3.4", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "regexpp": "^3.2.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, "node_modules/@typescript-eslint/parser": { "version": "5.45.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.45.0.tgz", @@ -1316,56 +1424,6 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/type-utils": { - "version": "5.45.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.45.0.tgz", - "integrity": "sha512-DY7BXVFSIGRGFZ574hTEyLPRiQIvI/9oGcN8t1A7f6zIs6ftbrU0nhyV26ZW//6f85avkwrLag424n+fkuoJ1Q==", - "dev": true, - "dependencies": { - "@typescript-eslint/typescript-estree": "5.45.0", - "@typescript-eslint/utils": "5.45.0", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, "node_modules/@typescript-eslint/types": { "version": "5.45.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.45.0.tgz", @@ -1429,54 +1487,6 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "node_modules/@typescript-eslint/utils": { - "version": "5.45.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.45.0.tgz", - "integrity": "sha512-OUg2JvsVI1oIee/SwiejTot2OxwU8a7UfTFMOdlhD2y+Hl6memUSL4s98bpUTo8EpVEr0lmwlU7JSu/p2QpSvA==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.45.0", - "@typescript-eslint/types": "5.45.0", - "@typescript-eslint/typescript-estree": "5.45.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^3.0.0", - "semver": "^7.3.7" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, "node_modules/@typescript-eslint/visitor-keys": { "version": "5.45.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.45.0.tgz", @@ -1851,6 +1861,25 @@ "node": ">= 0.6.0" } }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -1860,6 +1889,16 @@ "node": ">=8" } }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1886,7 +1925,6 @@ "version": "4.21.3", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.3.tgz", "integrity": "sha512-898rgRXLAyRkM1GryrrBHGkqA5hlpkV5MhtZwg9QXeiyLUYs2k00Un05aX5l2/yJIOObYKOpS2JNo8nJDE7fWQ==", - "dev": true, "funding": [ { "type": "opencollective", @@ -1921,6 +1959,29 @@ "node": ">= 0.4.0" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/buffer-from": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-0.1.2.tgz", @@ -2056,6 +2117,11 @@ "node": ">= 6" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, "node_modules/cipher-base": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", @@ -2078,11 +2144,22 @@ "node": ">=6" } }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -2093,8 +2170,16 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } }, "node_modules/combined-stream": { "version": "1.0.8", @@ -2151,7 +2236,6 @@ "version": "3.26.0", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.26.0.tgz", "integrity": "sha512-+DkDrhoR4Y0PxDz6rurahuB+I45OsEUv8E1maPTB6OuHRohMMcznBq9TMpdpDMm/hUPob/mJJS3PqgbHpMTQgw==", - "devOptional": true, "hasInstallScript": true, "funding": { "type": "opencollective", @@ -2294,13 +2378,35 @@ } }, "node_modules/decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", "engines": { "node": ">=0.10" } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2371,6 +2477,14 @@ "node": ">=6" } }, + "node_modules/detect-libc": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", + "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", + "engines": { + "node": ">=8" + } + }, "node_modules/detective": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz", @@ -2476,8 +2590,7 @@ "node_modules/electron-to-chromium": { "version": "1.4.206", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.206.tgz", - "integrity": "sha512-h+Fadt1gIaQ06JaIiyqPsBjJ08fV5Q7md+V8bUvQW/9OvXfL2LRICTz2EcnnCP7QzrFTS6/27MRV6Bl9Yn97zA==", - "dev": true + "integrity": "sha512-h+Fadt1gIaQ06JaIiyqPsBjJ08fV5Q7md+V8bUvQW/9OvXfL2LRICTz2EcnnCP7QzrFTS6/27MRV6Bl9Yn97zA==" }, "node_modules/emoji-regex": { "version": "9.2.2", @@ -2485,6 +2598,14 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.12.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz", @@ -2599,7 +2720,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true, "engines": { "node": ">=6" } @@ -3250,6 +3370,14 @@ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "engines": { + "node": ">=6" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -3448,6 +3576,11 @@ "resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz", "integrity": "sha512-3XY9e1pP0CVEUCdj5BmfIZxRBTSDycnbqhIOGec9QYtmVH2fbLpj86CFWkrNOkt/Fvty4KZG5lTglL9j/gJ87w==" }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3500,6 +3633,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/get-intrinsic": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", @@ -3544,6 +3686,11 @@ "resolved": "https://registry.npmjs.org/github-buttons/-/github-buttons-2.22.0.tgz", "integrity": "sha512-N5bk01s1WgK1FVtoeSUVkRkJpkaSu8yHMPcjye+PTa0jsRjMRNrYqVLgpUf2RA5Kvec05DfHYAT6/68fwkdqPw==" }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" + }, "node_modules/glob": { "version": "7.1.7", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", @@ -3743,7 +3890,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", - "dev": true, "dependencies": { "void-elements": "3.1.0" } @@ -3811,10 +3957,9 @@ } }, "node_modules/i18next": { - "version": "22.1.4", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-22.1.4.tgz", - "integrity": "sha512-MCDtNRyovLY22rgLoZdCzg2QIza1V1A/3Hxb99akJzTDjcqCRWEsglTpFUt0vUjOxSxz+WmxmFETLHORRS+n6Q==", - "dev": true, + "version": "22.4.6", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-22.4.6.tgz", + "integrity": "sha512-9Tm1ezxWyzV+306CIDMBbYBitC1jedQyYuuLtIv7oxjp2ohh8eyxP9xytIf+2bbQfhH784IQKPSYp+Zq9+YSbw==", "funding": [ { "type": "individual", @@ -3834,10 +3979,28 @@ } }, "node_modules/i18next-fs-backend": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/i18next-fs-backend/-/i18next-fs-backend-2.0.1.tgz", - "integrity": "sha512-fzeiFOXqsMiFAFUnNyC4buERI11vTAuf7JIDWqaiPgBK3R+XJQMSY1LyoXaWspBEFaAkXH/0uMbOv7nttBFztg==", - "dev": true + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/i18next-fs-backend/-/i18next-fs-backend-2.1.1.tgz", + "integrity": "sha512-FTnj+UmNgT3YRml5ruRv0jMZDG7odOL/OP5PF5mOqvXud2vHrPOOs68Zdk6iqzL47cnnM0ZVkK2BAvpFeDJToA==" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] }, "node_modules/ignore": { "version": "5.2.0", @@ -3896,6 +4059,11 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, "node_modules/inline-style-parser": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", @@ -3915,6 +4083,11 @@ "node": ">= 0.4" } }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + }, "node_modules/is-bigint": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", @@ -4268,6 +4441,18 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/json5": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", + "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "peer": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/jsonp": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/jsonp/-/jsonp-0.2.1.tgz", @@ -4451,7 +4636,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -5051,6 +5235,17 @@ "node": ">= 0.6" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -5068,6 +5263,11 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", @@ -5101,18 +5301,17 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, - "node_modules/natural-compare-lite": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", - "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", - "dev": true - }, "node_modules/next": { "version": "12.3.1", "resolved": "https://registry.npmjs.org/next/-/next-12.3.1.tgz", @@ -5166,19 +5365,10 @@ } }, "node_modules/next-i18next": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/next-i18next/-/next-i18next-13.0.0.tgz", - "integrity": "sha512-XiODAmMdueAIETQKIRPvYEZ5ghLOlzHb6PI4/WzwYkKdC/5q6UROzwIRw7aj3VWRB3xwnuuzEVI9NAjMfXyrkQ==", - "dev": true, + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/next-i18next/-/next-i18next-13.0.2.tgz", + "integrity": "sha512-aUHyKT2kztMgEP44zDB5KoW8XZUQawIdOYWXcrMH6lxAcS0kBsKX0uKMzGS5XlgLW88gvOVc3D7NdfCznLgyyg==", "funding": [ - { - "type": "individual", - "url": "https://github.com/belgattitude" - }, - { - "type": "individual", - "url": "https://locize.com" - }, { "type": "individual", "url": "https://locize.com/i18next.html" @@ -5186,14 +5376,22 @@ { "type": "individual", "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + }, + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://github.com/belgattitude" } ], "dependencies": { - "@babel/runtime": "^7.18.9", + "@babel/runtime": "^7.20.6", "@types/hoist-non-react-statics": "^3.3.1", "core-js": "^3", "hoist-non-react-statics": "^3.3.2", - "i18next-fs-backend": "^2.0.0" + "i18next-fs-backend": "^2.1.0" }, "engines": { "node": ">=14" @@ -5202,23 +5400,29 @@ "i18next": "^22.0.6", "next": ">= 12.0.0", "react": ">= 17.0.2", - "react-i18next": "^12.0.0" + "react-i18next": "^12.1.1" } }, - "node_modules/next-translate": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/next-translate/-/next-translate-1.6.0.tgz", - "integrity": "sha512-rmRBYOwPHvokN+5uH3O/SZqfRUVORLkgt/097mSaq3+CWcEtXEjkVrfBZpYwZ8W3og4QVd+uphYX//qegnIbRg==", - "peerDependencies": { - "next": ">= 10.0.0", - "react": ">= 16.8.0" + "node_modules/node-abi": { + "version": "3.30.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.30.0.tgz", + "integrity": "sha512-qWO5l3SCqbwQavymOmtTVuCWZE23++S+rxyoHjXqUmPyzRcaoI4lA2gO55/drddGnedAyjA7sk76SfQ5lfUMnw==", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" } }, + "node_modules/node-addon-api": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.0.0.tgz", + "integrity": "sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA==" + }, "node_modules/node-releases": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", - "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==", - "dev": true + "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==" }, "node_modules/normalize-path": { "version": "3.0.0", @@ -5361,7 +5565,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "dependencies": { "wrappy": "1" } @@ -5664,6 +5867,31 @@ "rrweb-snapshot": "^1.1.14" } }, + "node_modules/prebuild-install": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", + "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -5673,21 +5901,6 @@ "node": ">= 0.8.0" } }, - "node_modules/prettier": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz", - "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==", - "dev": true, - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -5712,6 +5925,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", @@ -5722,11 +5944,11 @@ } }, "node_modules/query-string": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.1.tgz", - "integrity": "sha512-MplouLRDHBZSG9z7fpuAAcI7aAYjDLhtsiVZsevsfaHWDS2IDdORKbSd1kWUA+V4zyva/HZoSfpwnYMMQDhb0w==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", + "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", "dependencies": { - "decode-uri-component": "^0.2.0", + "decode-uri-component": "^0.2.2", "filter-obj": "^1.1.0", "split-on-first": "^1.0.0", "strict-uri-encode": "^2.0.0" @@ -5792,6 +6014,28 @@ "safe-buffer": "^5.1.0" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", @@ -5958,7 +6202,6 @@ "version": "12.1.1", "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-12.1.1.tgz", "integrity": "sha512-mFdieOI0LDy84q3JuZU6Aou1DoWW2fhapcTGeBS8+vWSJuViuoCLQAMYSb0QoHhXS8B0WKUOPpx4cffAP7r/aA==", - "dev": true, "dependencies": { "@babel/runtime": "^7.14.5", "html-parse-stringify": "^3.0.1" @@ -6383,7 +6626,6 @@ "version": "7.3.8", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", - "dev": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -6416,6 +6658,28 @@ "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" }, + "node_modules/sharp": { + "version": "0.31.2", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.31.2.tgz", + "integrity": "sha512-DUdNVEXgS5A97cTagSLIIp8dUZ/lZtk78iNVZgHdHbx1qnQR7JAHY0BnXnwwH39Iw+VKhO08CTYhIg0p98vQ5Q==", + "hasInstallScript": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.1", + "node-addon-api": "^5.0.0", + "prebuild-install": "^7.1.1", + "semver": "^7.3.8", + "simple-get": "^4.0.1", + "tar-fs": "^2.1.1", + "tunnel-agent": "^0.6.0" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -6451,6 +6715,57 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -6769,6 +7084,32 @@ "node": ">=6" } }, + "node_modules/tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/text-segmentation": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", @@ -6946,6 +7287,17 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/tweetnacl": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", @@ -7116,7 +7468,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.5.tgz", "integrity": "sha512-dteFFpCyvuDdr9S/ff1ISkKt/9YZxKjI9WlRR99c180GaztJtRa/fn18FdxGVKVsnPY7/a/FDN68mcvUmP4U7Q==", - "dev": true, "funding": [ { "type": "opencollective", @@ -7243,7 +7594,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -7291,8 +7641,7 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/xtend": { "version": "4.0.2", @@ -7306,8 +7655,7 @@ "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yaml": { "version": "1.10.2", @@ -7331,6 +7679,28 @@ } }, "dependencies": { + "@ampproject/remapping": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", + "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "peer": true, + "requires": { + "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "dependencies": { + "@jridgewell/gen-mapping": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", + "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "peer": true, + "requires": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + } + } + }, "@babel/code-frame": { "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", @@ -7339,12 +7709,64 @@ "@babel/highlight": "^7.18.6" } }, - "@babel/generator": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.19.0.tgz", - "integrity": "sha512-S1ahxf1gZ2dpoiFgA+ohK9DIpz50bJ0CWs7Zlzb54Z4sG8qmdIrGrVqmy1sAtTVRb+9CU6U8VqT9L0Zj7hxHVg==", + "@babel/compat-data": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.5.tgz", + "integrity": "sha512-KZXo2t10+/jxmkhNXc7pZTqRvSOIvVv/+lJwHS+B2rErwOyjuVRh60yVpb7liQ1U5t7lLJ1bz+t8tSypUZdm0g==", + "peer": true + }, + "@babel/core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.5.tgz", + "integrity": "sha512-UdOWmk4pNWTm/4DlPUl/Pt4Gz4rcEMb7CY0Y3eJl5Yz1vI8ZJGmHWaVE55LoxRjdpx0z259GE9U5STA9atUinQ==", + "peer": true, "requires": { - "@babel/types": "^7.19.0", + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.20.5", + "@babel/helper-compilation-targets": "^7.20.0", + "@babel/helper-module-transforms": "^7.20.2", + "@babel/helpers": "^7.20.5", + "@babel/parser": "^7.20.5", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.20.5", + "@babel/types": "^7.20.5", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.1", + "semver": "^6.3.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "peer": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "peer": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "peer": true + } + } + }, + "@babel/generator": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.5.tgz", + "integrity": "sha512-jl7JY2Ykn9S0yj4DQP82sYvPU+T3g0HFcWTqDLqiuA9tGRNIj9VfbtXGAYTTkyNEnQk1jkMGOdYka8aG/lulCA==", + "requires": { + "@babel/types": "^7.20.5", "@jridgewell/gen-mapping": "^0.3.2", "jsesc": "^2.5.1" } @@ -7357,6 +7779,26 @@ "@babel/types": "^7.18.6" } }, + "@babel/helper-compilation-targets": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.0.tgz", + "integrity": "sha512-0jp//vDGp9e8hZzBc6N/KwA5ZK3Wsm/pfm4CrY7vzegkVxc65SgSn6wYOnwHe9Js9HRQ1YTCKLGPzDtaS3RoLQ==", + "peer": true, + "requires": { + "@babel/compat-data": "^7.20.0", + "@babel/helper-validator-option": "^7.18.6", + "browserslist": "^4.21.3", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "peer": true + } + } + }, "@babel/helper-environment-visitor": { "version": "7.18.9", "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", @@ -7387,11 +7829,36 @@ "@babel/types": "^7.18.6" } }, + "@babel/helper-module-transforms": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.20.2.tgz", + "integrity": "sha512-zvBKyJXRbmK07XhMuujYoJ48B5yvvmM6+wcpv6Ivj4Yg6qO7NOZOSnvZN9CRl1zz1Z4cKf8YejmCMh8clOoOeA==", + "peer": true, + "requires": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-simple-access": "^7.20.2", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/helper-validator-identifier": "^7.19.1", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.20.1", + "@babel/types": "^7.20.2" + } + }, "@babel/helper-plugin-utils": { "version": "7.19.0", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.19.0.tgz", "integrity": "sha512-40Ryx7I8mT+0gaNxm8JGTZFUITNqdLAgdg0hXzeVZxVD6nFsdhQvip6v8dqkRHzsz1VFpFAaOCHNn0vKBL7Czw==" }, + "@babel/helper-simple-access": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz", + "integrity": "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==", + "peer": true, + "requires": { + "@babel/types": "^7.20.2" + } + }, "@babel/helper-split-export-declaration": { "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", @@ -7401,14 +7868,31 @@ } }, "@babel/helper-string-parser": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.18.10.tgz", - "integrity": "sha512-XtIfWmeNY3i4t7t4D2t02q50HvqHybPqW2ki1kosnvWCwuCMeo81Jf0gwr85jy/neUdg5XDdeFE/80DXiO+njw==" + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", + "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==" }, "@babel/helper-validator-identifier": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==" + }, + "@babel/helper-validator-option": { "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz", - "integrity": "sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==" + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz", + "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==", + "peer": true + }, + "@babel/helpers": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.6.tgz", + "integrity": "sha512-Pf/OjgfgFRW5bApskEz5pvidpim7tEDPlFtKcNRXWmfHGn9IEI2W2flqRQXTFb7gIPTyK++N6rVHuwKut4XK6w==", + "peer": true, + "requires": { + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.20.5", + "@babel/types": "^7.20.5" + } }, "@babel/highlight": { "version": "7.18.6", @@ -7421,9 +7905,9 @@ } }, "@babel/parser": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.19.1.tgz", - "integrity": "sha512-h7RCSorm1DdTVGJf3P2Mhj3kdnkmF/EiysUkzS2TdgAYqyjFdMQJbVuXOBej2SBJaXan/lIVtT6KkGbyyq753A==" + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.5.tgz", + "integrity": "sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA==" }, "@babel/plugin-syntax-jsx": { "version": "7.18.6", @@ -7434,9 +7918,9 @@ } }, "@babel/runtime": { - "version": "7.20.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.6.tgz", - "integrity": "sha512-Q+8MqP7TiHMWzSfwiJwXCjyf4GYA4Dgw3emg/7xmwsdLJOZUp+nMqcOwOzzYheuM1rhDu8FSj2l0aoMygEuXuA==", + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.7.tgz", + "integrity": "sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==", "requires": { "regenerator-runtime": "^0.13.11" } @@ -7462,18 +7946,18 @@ } }, "@babel/traverse": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.19.1.tgz", - "integrity": "sha512-0j/ZfZMxKukDaag2PtOPDbwuELqIar6lLskVPPJDjXMXjfLb1Obo/1yjxIGqqAJrmfaTIY3z2wFLAQ7qSkLsuA==", + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.5.tgz", + "integrity": "sha512-WM5ZNN3JITQIq9tFZaw1ojLU3WgWdtkxnhM1AegMS+PvHjkM5IXjmYEGY7yukz5XS4sJyEf2VzWjI8uAavhxBQ==", "requires": { "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.19.0", + "@babel/generator": "^7.20.5", "@babel/helper-environment-visitor": "^7.18.9", "@babel/helper-function-name": "^7.19.0", "@babel/helper-hoist-variables": "^7.18.6", "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.19.1", - "@babel/types": "^7.19.0", + "@babel/parser": "^7.20.5", + "@babel/types": "^7.20.5", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -7494,12 +7978,12 @@ } }, "@babel/types": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.19.0.tgz", - "integrity": "sha512-YuGopBq3ke25BVSiS6fgF49Ul9gH1x70Bcr6bqRLjWCkcX8Hre1/5+z+IiWOIerRMSSEfGZVB9z9kyq7wVs9YA==", + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.5.tgz", + "integrity": "sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==", "requires": { - "@babel/helper-string-parser": "^7.18.10", - "@babel/helper-validator-identifier": "^7.18.6", + "@babel/helper-string-parser": "^7.19.4", + "@babel/helper-validator-identifier": "^7.19.1", "to-fast-properties": "^2.0.0" } }, @@ -8038,12 +8522,6 @@ "hoist-non-react-statics": "^3.3.0" } }, - "@types/json-schema": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", - "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", - "dev": true - }, "@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -8116,12 +8594,6 @@ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" }, - "@types/semver": { - "version": "7.3.13", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", - "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==", - "dev": true - }, "@types/unist": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz", @@ -8137,40 +8609,6 @@ "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==" }, - "@typescript-eslint/eslint-plugin": { - "version": "5.45.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.45.0.tgz", - "integrity": "sha512-CXXHNlf0oL+Yg021cxgOdMHNTXD17rHkq7iW6RFHoybdFgQBjU3yIXhhcPpGwr1CjZlo6ET8C6tzX5juQoXeGA==", - "dev": true, - "requires": { - "@typescript-eslint/scope-manager": "5.45.0", - "@typescript-eslint/type-utils": "5.45.0", - "@typescript-eslint/utils": "5.45.0", - "debug": "^4.3.4", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "regexpp": "^3.2.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "dependencies": { - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, "@typescript-eslint/parser": { "version": "5.45.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.45.0.tgz", @@ -8210,35 +8648,6 @@ "@typescript-eslint/visitor-keys": "5.45.0" } }, - "@typescript-eslint/type-utils": { - "version": "5.45.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.45.0.tgz", - "integrity": "sha512-DY7BXVFSIGRGFZ574hTEyLPRiQIvI/9oGcN8t1A7f6zIs6ftbrU0nhyV26ZW//6f85avkwrLag424n+fkuoJ1Q==", - "dev": true, - "requires": { - "@typescript-eslint/typescript-estree": "5.45.0", - "@typescript-eslint/utils": "5.45.0", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - }, - "dependencies": { - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, "@typescript-eslint/types": { "version": "5.45.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.45.0.tgz", @@ -8277,40 +8686,6 @@ } } }, - "@typescript-eslint/utils": { - "version": "5.45.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.45.0.tgz", - "integrity": "sha512-OUg2JvsVI1oIee/SwiejTot2OxwU8a7UfTFMOdlhD2y+Hl6memUSL4s98bpUTo8EpVEr0lmwlU7JSu/p2QpSvA==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.45.0", - "@typescript-eslint/types": "5.45.0", - "@typescript-eslint/typescript-estree": "5.45.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^3.0.0", - "semver": "^7.3.7" - }, - "dependencies": { - "eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - } - }, - "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true - } - } - }, "@typescript-eslint/visitor-keys": { "version": "5.45.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.45.0.tgz", @@ -8588,12 +8963,27 @@ "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", "optional": true }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "dev": true }, + "bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -8617,7 +9007,6 @@ "version": "4.21.3", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.3.tgz", "integrity": "sha512-898rgRXLAyRkM1GryrrBHGkqA5hlpkV5MhtZwg9QXeiyLUYs2k00Un05aX5l2/yJIOObYKOpS2JNo8nJDE7fWQ==", - "dev": true, "requires": { "caniuse-lite": "^1.0.30001370", "electron-to-chromium": "^1.4.202", @@ -8630,6 +9019,15 @@ "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==" }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "buffer-from": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-0.1.2.tgz", @@ -8724,6 +9122,11 @@ } } }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, "cipher-base": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", @@ -8743,11 +9146,19 @@ "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==" }, + "color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "requires": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + } + }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "requires": { "color-name": "~1.1.4" } @@ -8755,8 +9166,16 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "requires": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } }, "combined-stream": { "version": "1.0.8", @@ -8804,8 +9223,7 @@ "core-js": { "version": "3.26.0", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.26.0.tgz", - "integrity": "sha512-+DkDrhoR4Y0PxDz6rurahuB+I45OsEUv8E1maPTB6OuHRohMMcznBq9TMpdpDMm/hUPob/mJJS3PqgbHpMTQgw==", - "devOptional": true + "integrity": "sha512-+DkDrhoR4Y0PxDz6rurahuB+I45OsEUv8E1maPTB6OuHRohMMcznBq9TMpdpDMm/hUPob/mJJS3PqgbHpMTQgw==" }, "core-js-pure": { "version": "3.26.1", @@ -8919,9 +9337,22 @@ } }, "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==" + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==" + }, + "decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "requires": { + "mimic-response": "^3.1.0" + } + }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" }, "deep-is": { "version": "0.1.4", @@ -8974,6 +9405,11 @@ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==" }, + "detect-libc": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", + "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==" + }, "detective": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz", @@ -9066,8 +9502,7 @@ "electron-to-chromium": { "version": "1.4.206", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.206.tgz", - "integrity": "sha512-h+Fadt1gIaQ06JaIiyqPsBjJ08fV5Q7md+V8bUvQW/9OvXfL2LRICTz2EcnnCP7QzrFTS6/27MRV6Bl9Yn97zA==", - "dev": true + "integrity": "sha512-h+Fadt1gIaQ06JaIiyqPsBjJ08fV5Q7md+V8bUvQW/9OvXfL2LRICTz2EcnnCP7QzrFTS6/27MRV6Bl9Yn97zA==" }, "emoji-regex": { "version": "9.2.2", @@ -9075,6 +9510,14 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "requires": { + "once": "^1.4.0" + } + }, "enhanced-resolve": { "version": "5.12.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz", @@ -9168,8 +9611,7 @@ "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" }, "escape-string-regexp": { "version": "1.0.5", @@ -9643,6 +10085,11 @@ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" }, + "expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==" + }, "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -9792,6 +10239,11 @@ "resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz", "integrity": "sha512-3XY9e1pP0CVEUCdj5BmfIZxRBTSDycnbqhIOGec9QYtmVH2fbLpj86CFWkrNOkt/Fvty4KZG5lTglL9j/gJ87w==" }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -9828,6 +10280,12 @@ "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "dev": true }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "peer": true + }, "get-intrinsic": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", @@ -9860,6 +10318,11 @@ "resolved": "https://registry.npmjs.org/github-buttons/-/github-buttons-2.22.0.tgz", "integrity": "sha512-N5bk01s1WgK1FVtoeSUVkRkJpkaSu8yHMPcjye+PTa0jsRjMRNrYqVLgpUf2RA5Kvec05DfHYAT6/68fwkdqPw==" }, + "github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" + }, "glob": { "version": "7.1.7", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", @@ -10007,7 +10470,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", - "dev": true, "requires": { "void-elements": "3.1.0" } @@ -10068,19 +10530,22 @@ } }, "i18next": { - "version": "22.1.4", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-22.1.4.tgz", - "integrity": "sha512-MCDtNRyovLY22rgLoZdCzg2QIza1V1A/3Hxb99akJzTDjcqCRWEsglTpFUt0vUjOxSxz+WmxmFETLHORRS+n6Q==", - "dev": true, + "version": "22.4.6", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-22.4.6.tgz", + "integrity": "sha512-9Tm1ezxWyzV+306CIDMBbYBitC1jedQyYuuLtIv7oxjp2ohh8eyxP9xytIf+2bbQfhH784IQKPSYp+Zq9+YSbw==", "requires": { "@babel/runtime": "^7.20.6" } }, "i18next-fs-backend": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/i18next-fs-backend/-/i18next-fs-backend-2.0.1.tgz", - "integrity": "sha512-fzeiFOXqsMiFAFUnNyC4buERI11vTAuf7JIDWqaiPgBK3R+XJQMSY1LyoXaWspBEFaAkXH/0uMbOv7nttBFztg==", - "dev": true + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/i18next-fs-backend/-/i18next-fs-backend-2.1.1.tgz", + "integrity": "sha512-FTnj+UmNgT3YRml5ruRv0jMZDG7odOL/OP5PF5mOqvXud2vHrPOOs68Zdk6iqzL47cnnM0ZVkK2BAvpFeDJToA==" + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" }, "ignore": { "version": "5.2.0", @@ -10123,6 +10588,11 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, "inline-style-parser": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", @@ -10139,6 +10609,11 @@ "side-channel": "^1.0.4" } }, + "is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + }, "is-bigint": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", @@ -10366,6 +10841,12 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "json5": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", + "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "peer": true + }, "jsonp": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/jsonp/-/jsonp-0.2.1.tgz", @@ -10520,7 +11001,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "requires": { "yallist": "^4.0.0" } @@ -10872,6 +11352,11 @@ "mime-db": "1.52.0" } }, + "mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==" + }, "minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -10886,6 +11371,11 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" }, + "mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, "mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", @@ -10910,18 +11400,17 @@ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==" }, + "napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" + }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, - "natural-compare-lite": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", - "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", - "dev": true - }, "next": { "version": "12.3.1", "resolved": "https://registry.npmjs.org/next/-/next-12.3.1.tgz", @@ -10949,29 +11438,34 @@ } }, "next-i18next": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/next-i18next/-/next-i18next-13.0.0.tgz", - "integrity": "sha512-XiODAmMdueAIETQKIRPvYEZ5ghLOlzHb6PI4/WzwYkKdC/5q6UROzwIRw7aj3VWRB3xwnuuzEVI9NAjMfXyrkQ==", - "dev": true, + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/next-i18next/-/next-i18next-13.0.2.tgz", + "integrity": "sha512-aUHyKT2kztMgEP44zDB5KoW8XZUQawIdOYWXcrMH6lxAcS0kBsKX0uKMzGS5XlgLW88gvOVc3D7NdfCznLgyyg==", "requires": { - "@babel/runtime": "^7.18.9", + "@babel/runtime": "^7.20.6", "@types/hoist-non-react-statics": "^3.3.1", "core-js": "^3", "hoist-non-react-statics": "^3.3.2", - "i18next-fs-backend": "^2.0.0" + "i18next-fs-backend": "^2.1.0" } }, - "next-translate": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/next-translate/-/next-translate-1.6.0.tgz", - "integrity": "sha512-rmRBYOwPHvokN+5uH3O/SZqfRUVORLkgt/097mSaq3+CWcEtXEjkVrfBZpYwZ8W3og4QVd+uphYX//qegnIbRg==", - "requires": {} + "node-abi": { + "version": "3.30.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.30.0.tgz", + "integrity": "sha512-qWO5l3SCqbwQavymOmtTVuCWZE23++S+rxyoHjXqUmPyzRcaoI4lA2gO55/drddGnedAyjA7sk76SfQ5lfUMnw==", + "requires": { + "semver": "^7.3.5" + } + }, + "node-addon-api": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.0.0.tgz", + "integrity": "sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA==" }, "node-releases": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", - "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==", - "dev": true + "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==" }, "normalize-path": { "version": "3.0.0", @@ -11074,7 +11568,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "requires": { "wrappy": "1" } @@ -11265,18 +11758,31 @@ "rrweb-snapshot": "^1.1.14" } }, + "prebuild-install": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", + "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", + "requires": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + } + }, "prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true }, - "prettier": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz", - "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==", - "dev": true - }, "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -11297,6 +11803,15 @@ "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.1.1.tgz", "integrity": "sha512-hrzC564QIl0r0vy4l6MvRLhafmUowhO/O3KgVSoXIbbA2Sz4j8HGpJc6T2cubRVwMwpdiG/vKGfhT4IixmKN9w==" }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", @@ -11304,11 +11819,11 @@ "dev": true }, "query-string": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.1.tgz", - "integrity": "sha512-MplouLRDHBZSG9z7fpuAAcI7aAYjDLhtsiVZsevsfaHWDS2IDdORKbSd1kWUA+V4zyva/HZoSfpwnYMMQDhb0w==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", + "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", "requires": { - "decode-uri-component": "^0.2.0", + "decode-uri-component": "^0.2.2", "filter-obj": "^1.1.0", "split-on-first": "^1.0.0", "strict-uri-encode": "^2.0.0" @@ -11348,6 +11863,24 @@ "safe-buffer": "^5.1.0" } }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==" + } + } + }, "react": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", @@ -11476,7 +12009,6 @@ "version": "12.1.1", "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-12.1.1.tgz", "integrity": "sha512-mFdieOI0LDy84q3JuZU6Aou1DoWW2fhapcTGeBS8+vWSJuViuoCLQAMYSb0QoHhXS8B0WKUOPpx4cffAP7r/aA==", - "dev": true, "requires": { "@babel/runtime": "^7.14.5", "html-parse-stringify": "^3.0.1" @@ -11761,7 +12293,6 @@ "version": "7.3.8", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", - "dev": true, "requires": { "lru-cache": "^6.0.0" } @@ -11785,6 +12316,21 @@ "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" }, + "sharp": { + "version": "0.31.2", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.31.2.tgz", + "integrity": "sha512-DUdNVEXgS5A97cTagSLIIp8dUZ/lZtk78iNVZgHdHbx1qnQR7JAHY0BnXnwwH39Iw+VKhO08CTYhIg0p98vQ5Q==", + "requires": { + "color": "^4.2.3", + "detect-libc": "^2.0.1", + "node-addon-api": "^5.0.0", + "prebuild-install": "^7.1.1", + "semver": "^7.3.8", + "simple-get": "^4.0.1", + "tar-fs": "^2.1.1", + "tunnel-agent": "^0.6.0" + } + }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -11811,6 +12357,29 @@ "object-inspect": "^1.9.0" } }, + "simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==" + }, + "simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "requires": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "requires": { + "is-arrayish": "^0.3.1" + } + }, "slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -12026,6 +12595,29 @@ "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", "dev": true }, + "tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "requires": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "requires": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + } + }, "text-segmentation": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", @@ -12180,6 +12772,14 @@ } } }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, "tweetnacl": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", @@ -12299,7 +12899,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.5.tgz", "integrity": "sha512-dteFFpCyvuDdr9S/ff1ISkKt/9YZxKjI9WlRR99c180GaztJtRa/fn18FdxGVKVsnPY7/a/FDN68mcvUmP4U7Q==", - "dev": true, "requires": { "escalade": "^3.1.1", "picocolors": "^1.0.0" @@ -12388,8 +12987,7 @@ "void-elements": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", - "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", - "dev": true + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==" }, "which": { "version": "2.0.2", @@ -12422,8 +13020,7 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "xtend": { "version": "4.0.2", @@ -12434,8 +13031,7 @@ "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "yaml": { "version": "1.10.2", diff --git a/frontend/package.json b/frontend/package.json index 890e057f9..570a6e5ec 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "prepare": "cd .. && npm install", "dev": "next dev", "build": "next build", "start": "next start", @@ -27,24 +28,27 @@ "fs": "^0.0.1-security", "gray-matter": "^4.0.3", "http-proxy": "^1.18.1", + "i18next": "^22.4.6", "jspdf": "^2.5.1", "jsrp": "^0.2.4", "markdown-it": "^13.0.1", "next": "^12.2.5", - "next-translate": "^1.6.0", + "next-i18next": "^13.0.2", "posthog-js": "^1.34.0", - "query-string": "^7.1.1", + "query-string": "^7.1.3", "react": "^17.0.2", "react-beautiful-dnd": "^13.1.1", "react-code-input": "^3.10.1", "react-dom": "^17.0.2", "react-github-btn": "^1.4.0", "react-grid-layout": "^1.3.4", + "react-i18next": "^12.1.1", "react-mailchimp-subscribe": "^2.1.3", "react-markdown": "^8.0.3", "react-redux": "^8.0.2", "react-table": "^7.8.0", "set-cookie-parser": "^2.5.1", + "sharp": "^0.31.2", "styled-components": "^5.3.5", "tweetnacl": "^1.0.3", "tweetnacl-util": "^0.15.1", @@ -55,18 +59,13 @@ "@tailwindcss/typography": "^0.5.4", "@types/node": "18.11.9", "@types/react": "^18.0.26", - "@typescript-eslint/eslint-plugin": "^5.45.0", "@typescript-eslint/parser": "^5.45.0", "autoprefixer": "^10.4.7", "eslint": "^8.29.0", "eslint-config-next": "^13.0.5", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-simple-import-sort": "^8.0.0", - "i18next": "^22.1.4", - "next-i18next": "^13.0.0", "postcss": "^8.4.14", - "prettier": "2.7.1", - "react-i18next": "^12.1.1", "tailwindcss": "^3.1.4", "typescript": "^4.9.3" } diff --git a/frontend/pages/404.tsx b/frontend/pages/404.tsx new file mode 100644 index 000000000..6192f8773 --- /dev/null +++ b/frontend/pages/404.tsx @@ -0,0 +1,28 @@ +import React from "react"; +import Head from "next/head"; +import Image from "next/image"; +import Link from "next/link"; + +export default function Custom404() { + return ( +
+ + Infisical | Page Not Found + + +
+

Oops, something went wrong

+

Think this is a mistake? Email team@infisical.com and we`ll fix it!

+ + Go to Dashboard + + google logo +
+
+ ); +} diff --git a/frontend/pages/_app.js b/frontend/pages/_app.js index 56d030da1..0d1eeee32 100644 --- a/frontend/pages/_app.js +++ b/frontend/pages/_app.js @@ -3,12 +3,11 @@ import { useRouter } from "next/router"; import { appWithTranslation } from "next-i18next"; import { config } from "@fortawesome/fontawesome-svg-core"; -import { initPostHog } from "~/components/analytics/posthog"; -import Layout from "~/components/basic/layout"; +import Layout from "~/components/basic/Layout"; import NotificationProvider from "~/components/context/Notifications/NotificationProvider"; import RouteGuard from "~/components/RouteGuard"; import { publicPaths } from "~/const"; -import { ENV } from "~/utilities/config"; +import Telemetry from "~/utilities/telemetry/Telemetry"; import "@fortawesome/fontawesome-svg-core/styles.css"; import "../styles/globals.css"; @@ -17,7 +16,6 @@ config.autoAddCss = false; const App = ({ Component, pageProps, ...appProps }) => { const router = useRouter(); - const posthog = initPostHog(); // useEffect(() => { // const storedLang = localStorage.getItem("lang"); @@ -32,13 +30,11 @@ const App = ({ Component, pageProps, ...appProps }) => { useEffect(() => { // Init for auto capturing - const posthog = initPostHog(); + const telemetry = new Telemetry().getInstance(); const handleRouteChange = () => { if (typeof window !== "undefined") { - if (ENV == "production") { - posthog.capture("$pageview"); - } + telemetry.capture("$pageview"); } }; diff --git a/frontend/pages/api/auth/ChangePassword2.js b/frontend/pages/api/auth/ChangePassword2.ts similarity index 50% rename from frontend/pages/api/auth/ChangePassword2.js rename to frontend/pages/api/auth/ChangePassword2.ts index b764e6067..132eeddf4 100644 --- a/frontend/pages/api/auth/ChangePassword2.js +++ b/frontend/pages/api/auth/ChangePassword2.ts @@ -1,4 +1,13 @@ -import SecurityClient from "~/utilities/SecurityClient"; +import SecurityClient from '~/utilities/SecurityClient'; + +interface Props { + encryptedPrivateKey: string; + iv: string; + tag: string; + salt: string; + verifier: string; + clientProof: string; +} /** * This is the second step of the change password process (pake) @@ -11,12 +20,12 @@ const changePassword2 = ({ tag, salt, verifier, - clientProof, -}) => { - return SecurityClient.fetchCall("/api/v1/password/change-password", { - method: "POST", + clientProof +}: Props) => { + return SecurityClient.fetchCall('/api/v1/password/change-password', { + method: 'POST', headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json' }, body: JSON.stringify({ clientProof: clientProof, @@ -24,13 +33,13 @@ const changePassword2 = ({ iv: iv, tag: tag, salt: salt, - verifier: verifier, - }), + verifier: verifier + }) }).then(async (res) => { - if (res.status == 200) { + if (res && res.status == 200) { return res; } else { - console.log("Failed to change the password"); + console.log('Failed to change the password'); } }); }; diff --git a/frontend/pages/api/auth/CheckAuth.js b/frontend/pages/api/auth/CheckAuth.js deleted file mode 100644 index edd373614..000000000 --- a/frontend/pages/api/auth/CheckAuth.js +++ /dev/null @@ -1,25 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient.js"; - -/** - * This function is used to check if the user is authenticated. - * To do that, we get their tokens from cookies, and verify if they are good. - * @param {*} req - * @param {*} res - * @returns - */ -const checkAuth = async (req, res) => { - return SecurityClient.fetchCall("/api/v1/auth/checkAuth", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - }).then((res) => { - if (res.status == 200) { - return res; - } else { - console.log("Not authorized"); - } - }); -}; - -export default checkAuth; diff --git a/frontend/pages/api/auth/CheckAuth.ts b/frontend/pages/api/auth/CheckAuth.ts new file mode 100644 index 000000000..2578d7ce9 --- /dev/null +++ b/frontend/pages/api/auth/CheckAuth.ts @@ -0,0 +1,22 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +/** + * This function is used to check if the user is authenticated. + * To do that, we get their tokens from cookies, and verify if they are good. + */ +const checkAuth = async () => { + return SecurityClient.fetchCall('/api/v1/auth/checkAuth', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }).then((res) => { + if (res && res.status == 200) { + return res; + } else { + console.log('Not authorized'); + } + }); +}; + +export default checkAuth; diff --git a/frontend/pages/api/auth/CheckEmailVerificationCode.js b/frontend/pages/api/auth/CheckEmailVerificationCode.js deleted file mode 100644 index 83a64602a..000000000 --- a/frontend/pages/api/auth/CheckEmailVerificationCode.js +++ /dev/null @@ -1,20 +0,0 @@ -/** - * This route check the verification code from the email that user just recieved - * @param {*} email - * @param {*} code - * @returns - */ -const checkEmailVerificationCode = (email, code) => { - return fetch("/api/v1/signup/email/verify", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - email: email, - code: code, - }), - }); -}; - -export default checkEmailVerificationCode; diff --git a/frontend/pages/api/auth/CheckEmailVerificationCode.ts b/frontend/pages/api/auth/CheckEmailVerificationCode.ts new file mode 100644 index 000000000..0709592a3 --- /dev/null +++ b/frontend/pages/api/auth/CheckEmailVerificationCode.ts @@ -0,0 +1,26 @@ +interface Props { + email: string; + code: string; +} + +/** + * This route check the verification code from the email that user just recieved + * @param {object} obj + * @param {string} obj.email + * @param {string} obj.code + * @returns + */ +const checkEmailVerificationCode = ({ email, code }: Props) => { + return fetch('/api/v1/signup/email/verify', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + email: email, + code: code + }) + }); +}; + +export default checkEmailVerificationCode; diff --git a/frontend/pages/api/auth/CompleteAccountInformationSignup.js b/frontend/pages/api/auth/CompleteAccountInformationSignup.js deleted file mode 100644 index 21406e381..000000000 --- a/frontend/pages/api/auth/CompleteAccountInformationSignup.js +++ /dev/null @@ -1,50 +0,0 @@ -/** - * This function is called in the end of the signup process. - * It sends all the necessary nformation to the server. - * @param {*} email - * @param {*} firstName - * @param {*} lastName - * @param {*} workspace - * @param {*} publicKey - * @param {*} ciphertext - * @param {*} iv - * @param {*} tag - * @param {*} salt - * @param {*} verifier - * @returns - */ -const completeAccountInformationSignup = ({ - email, - firstName, - lastName, - organizationName, - publicKey, - ciphertext, - iv, - tag, - salt, - verifier, - token, -}) => { - return fetch("/api/v1/signup/complete-account/signup", { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer " + token, - }, - body: JSON.stringify({ - email, - firstName, - lastName, - publicKey, - encryptedPrivateKey: ciphertext, - organizationName, - iv, - tag, - salt, - verifier, - }), - }); -}; - -export default completeAccountInformationSignup; diff --git a/frontend/pages/api/auth/CompleteAccountInformationSignup.ts b/frontend/pages/api/auth/CompleteAccountInformationSignup.ts new file mode 100644 index 000000000..8057e776a --- /dev/null +++ b/frontend/pages/api/auth/CompleteAccountInformationSignup.ts @@ -0,0 +1,66 @@ +interface Props { + email: string; + firstName: string; + lastName: string; + publicKey: string; + ciphertext: string; + organizationName: string; + iv: string; + tag: string; + salt: string; + verifier: string; + token: string; +} + +/** + * This function is called in the end of the signup process. + * It sends all the necessary nformation to the server. + * @param {object} obj + * @param {string} obj.email - email of the user completing signup + * @param {string} obj.firstName - first name of the user completing signup + * @param {string} obj.lastName - last name of the user completing sign up + * @param {string} obj.organizationName - organization name for this user (usually, [FIRST_NAME]'s organization) + * @param {string} obj.publicKey - public key of the user completing signup + * @param {string} obj.ciphertext + * @param {string} obj.iv + * @param {string} obj.tag + * @param {string} obj.salt + * @param {string} obj.verifier + * @param {string} obj.token - token that confirms a user's identity + * @returns + */ +const completeAccountInformationSignup = ({ + email, + firstName, + lastName, + organizationName, + publicKey, + ciphertext, + iv, + tag, + salt, + verifier, + token +}: Props) => { + return fetch('/api/v1/signup/complete-account/signup', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer ' + token + }, + body: JSON.stringify({ + email, + firstName, + lastName, + publicKey, + encryptedPrivateKey: ciphertext, + organizationName, + iv, + tag, + salt, + verifier + }) + }); +}; + +export default completeAccountInformationSignup; diff --git a/frontend/pages/api/auth/CompleteAccountInformationSignupInvite.js b/frontend/pages/api/auth/CompleteAccountInformationSignupInvite.js deleted file mode 100644 index a205e6f59..000000000 --- a/frontend/pages/api/auth/CompleteAccountInformationSignupInvite.js +++ /dev/null @@ -1,47 +0,0 @@ -/** - * This function is called in the end of the signup process. - * It sends all the necessary nformation to the server. - * @param {*} email - * @param {*} firstName - * @param {*} lastName - * @param {*} publicKey - * @param {*} ciphertext - * @param {*} iv - * @param {*} tag - * @param {*} salt - * @param {*} verifier - * @returns - */ -const completeAccountInformationSignupInvite = ({ - email, - firstName, - lastName, - publicKey, - ciphertext, - iv, - tag, - salt, - verifier, - token, -}) => { - return fetch("/api/v1/signup/complete-account/invite", { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer " + token, - }, - body: JSON.stringify({ - email: email, - firstName: firstName, - lastName: lastName, - publicKey: publicKey, - encryptedPrivateKey: ciphertext, - iv: iv, - tag: tag, - salt: salt, - verifier: verifier, - }), - }); -}; - -export default completeAccountInformationSignupInvite; diff --git a/frontend/pages/api/auth/CompleteAccountInformationSignupInvite.ts b/frontend/pages/api/auth/CompleteAccountInformationSignupInvite.ts new file mode 100644 index 000000000..03c11cb67 --- /dev/null +++ b/frontend/pages/api/auth/CompleteAccountInformationSignupInvite.ts @@ -0,0 +1,62 @@ +interface Props { + email: string; + firstName: string; + lastName: string; + publicKey: string; + ciphertext: string; + iv: string; + tag: string; + salt: string; + verifier: string; + token: string; +} + +/** + * This function is called in the end of the signup process. + * It sends all the necessary nformation to the server. + * @param {object} obj + * @param {string} obj.email - email of the user completing signupinvite flow + * @param {string} obj.firstName - first name of the user completing signupinvite flow + * @param {string} obj.lastName - last name of the user completing signupinvite flow + * @param {string} obj.publicKey - public key of the user completing signupinvite flow + * @param {string} obj.ciphertext + * @param {string} obj.iv + * @param {string} obj.tag + * @param {string} obj.salt + * @param {string} obj.verifier + * @param {string} obj.token - token that confirms a user's identity + * @returns + */ +const completeAccountInformationSignupInvite = ({ + email, + firstName, + lastName, + publicKey, + ciphertext, + iv, + tag, + salt, + verifier, + token +}: Props) => { + return fetch('/api/v1/signup/complete-account/invite', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer ' + token + }, + body: JSON.stringify({ + email: email, + firstName: firstName, + lastName: lastName, + publicKey: publicKey, + encryptedPrivateKey: ciphertext, + iv: iv, + tag: tag, + salt: salt, + verifier: verifier + }) + }); +}; + +export default completeAccountInformationSignupInvite; diff --git a/frontend/pages/api/auth/EmailVerifyOnPasswordReset.ts b/frontend/pages/api/auth/EmailVerifyOnPasswordReset.ts new file mode 100644 index 000000000..b3d5e3d55 --- /dev/null +++ b/frontend/pages/api/auth/EmailVerifyOnPasswordReset.ts @@ -0,0 +1,34 @@ +interface Props { + email: string; + code: string; +} + +/** + * This is the second part of the account recovery step (a user needs to verify their email). + * A user need to click on a button in a magic link page + * @param {object} obj + * @param {object} obj.email - email of a user that is trying to recover access to their account + * @param {object} obj.code - token that a use received via the magic link + * @returns + */ +const EmailVerifyOnPasswordReset = async ({ email, code }: Props) => { + const response = await fetch('/api/v1/password/email/password-reset-verify', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + email: email, + code: code + }) + }); + if (response?.status === 200) { + return response; + } + + throw new Error( + 'Something went wrong during email verification on password reset.' + ); +}; + +export default EmailVerifyOnPasswordReset; diff --git a/frontend/pages/api/auth/IssueBackupPrivateKey.js b/frontend/pages/api/auth/IssueBackupPrivateKey.js deleted file mode 100644 index 9a31f7b00..000000000 --- a/frontend/pages/api/auth/IssueBackupPrivateKey.js +++ /dev/null @@ -1,40 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This is the route that issues a backup private key that will afterwards be added into a pdf - */ -const issueBackupPrivateKey = ({ - encryptedPrivateKey, - iv, - tag, - salt, - verifier, - clientProof, -}) => { - return SecurityClient.fetchCall( - "/api/v1/password/backup-private-key", - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - clientProof: clientProof, - encryptedPrivateKey: encryptedPrivateKey, - iv: iv, - tag: tag, - salt: salt, - verifier: verifier, - }), - } - ).then((res) => { - if (res.status == 200) { - return res; - } else { - console.log("Failed to issue the backup key"); - return res; - } - }); -}; - -export default issueBackupPrivateKey; diff --git a/frontend/pages/api/auth/IssueBackupPrivateKey.ts b/frontend/pages/api/auth/IssueBackupPrivateKey.ts new file mode 100644 index 000000000..e24ac510e --- /dev/null +++ b/frontend/pages/api/auth/IssueBackupPrivateKey.ts @@ -0,0 +1,52 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +interface Props { + encryptedPrivateKey: string; + iv: string; + tag: string; + salt: string; + verifier: string; + clientProof: string; +} + +/** + * This is the route that issues a backup private key that will afterwards be added into a pdf + * @param {object} obj + * @param {string} obj.encryptedPrivateKey + * @param {string} obj.iv + * @param {string} obj.tag + * @param {string} obj.salt + * @param {string} obj.verifier + * @param {string} obj.clientProof + * @returns + */ +const issueBackupPrivateKey = ({ + encryptedPrivateKey, + iv, + tag, + salt, + verifier, + clientProof +}: Props) => { + return SecurityClient.fetchCall('/api/v1/password/backup-private-key', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + clientProof: clientProof, + encryptedPrivateKey: encryptedPrivateKey, + iv: iv, + tag: tag, + salt: salt, + verifier: verifier + }) + }).then((res) => { + if (res?.status !== 200) { + console.log('Failed to issue the backup key'); + } + return res; + }); +}; + +export default issueBackupPrivateKey; diff --git a/frontend/pages/api/auth/Logout.ts b/frontend/pages/api/auth/Logout.ts index 4dbcb7bca..cd4ff9a61 100644 --- a/frontend/pages/api/auth/Logout.ts +++ b/frontend/pages/api/auth/Logout.ts @@ -1,29 +1,29 @@ -import SecurityClient from "~/utilities/SecurityClient"; +import SecurityClient from '~/utilities/SecurityClient'; /** * This route logs the user out. Note: the user should authorized to do this. * We first try to log out - if the authorization fails (response.status = 401), we refetch the new token, and then retry */ const logout = async () => { - return SecurityClient.fetchCall("/api/v1/auth/logout", { - method: "POST", + return SecurityClient.fetchCall('/api/v1/auth/logout', { + method: 'POST', headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json' }, - credentials: "include", + credentials: 'include' }).then((res) => { if (res?.status == 200) { - SecurityClient.setToken(""); + SecurityClient.setToken(''); // Delete the cookie by not setting a value; Alternatively clear the local storage - localStorage.setItem("publicKey", ""); - localStorage.setItem("encryptedPrivateKey", ""); - localStorage.setItem("iv", ""); - localStorage.setItem("tag", ""); - localStorage.setItem("PRIVATE_KEY", ""); - console.log("User logged out", res); + localStorage.setItem('publicKey', ''); + localStorage.setItem('encryptedPrivateKey', ''); + localStorage.setItem('iv', ''); + localStorage.setItem('tag', ''); + localStorage.setItem('PRIVATE_KEY', ''); + console.log('User logged out', res); return res; } else { - console.log("Failed to log out"); + console.log('Failed to log out'); } }); }; diff --git a/frontend/pages/api/auth/SRP1.js b/frontend/pages/api/auth/SRP1.js deleted file mode 100644 index b0fefb857..000000000 --- a/frontend/pages/api/auth/SRP1.js +++ /dev/null @@ -1,26 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This is the first step of the change password process (pake) - * @param {*} clientPublicKey - * @returns - */ -const SRP1 = ({ clientPublicKey }) => { - return SecurityClient.fetchCall("/api/v1/password/srp1", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - clientPublicKey, - }), - }).then(async (res) => { - if (res.status == 200) { - return await res.json(); - } else { - console.log("Failed to do the first step of SRP"); - } - }); -}; - -export default SRP1; diff --git a/frontend/pages/api/auth/SRP1.ts b/frontend/pages/api/auth/SRP1.ts new file mode 100644 index 000000000..cb6386ff2 --- /dev/null +++ b/frontend/pages/api/auth/SRP1.ts @@ -0,0 +1,30 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +interface Props { + clientPublicKey: string; +} + +/** + * This is the first step of the change password process (pake) + * @param {string} clientPublicKey + * @returns + */ +const SRP1 = ({ clientPublicKey }: Props) => { + return SecurityClient.fetchCall('/api/v1/password/srp1', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + clientPublicKey + }) + }).then(async (res) => { + if (res && res.status == 200) { + return await res.json(); + } else { + console.log('Failed to do the first step of SRP'); + } + }); +}; + +export default SRP1; diff --git a/frontend/pages/api/auth/SendEmailOnPasswordReset.ts b/frontend/pages/api/auth/SendEmailOnPasswordReset.ts new file mode 100644 index 000000000..abe610f07 --- /dev/null +++ b/frontend/pages/api/auth/SendEmailOnPasswordReset.ts @@ -0,0 +1,33 @@ +interface Props { + email: string; +} + +/** + * This is the first of the account recovery step (a user needs to verify their email). + * It will send an email containing a magic link to start the account recovery flow. + * @param {object} obj + * @param {object} obj.email - email of a user that is trying to recover access to their account + * @returns + */ +const SendEmailOnPasswordReset = async ({ email }: Props) => { + const response = await fetch('/api/v1/password/email/password-reset', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + email: email + }) + }); + // need precise error handling about the status code + if (response?.status === 200) { + const data = await response.json(); + return data; + } + + throw new Error( + 'Something went wrong while sending the email verification for password reset.' + ); +}; + +export default SendEmailOnPasswordReset; diff --git a/frontend/pages/api/auth/SendVerificationEmail.js b/frontend/pages/api/auth/SendVerificationEmail.ts similarity index 55% rename from frontend/pages/api/auth/SendVerificationEmail.js rename to frontend/pages/api/auth/SendVerificationEmail.ts index ae952852d..4f3b063c6 100644 --- a/frontend/pages/api/auth/SendVerificationEmail.js +++ b/frontend/pages/api/auth/SendVerificationEmail.ts @@ -2,15 +2,15 @@ * This route send the verification email to the user's email (contains a 6-digit verification code) * @param {*} email */ -const sendVerificationEmail = (email) => { - fetch("/api/v1/signup/email/signup", { - method: "POST", +const sendVerificationEmail = (email: string) => { + fetch('/api/v1/signup/email/signup', { + method: 'POST', headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json' }, body: JSON.stringify({ - email: email, - }), + email: email + }) }); }; diff --git a/frontend/pages/api/auth/Token.js b/frontend/pages/api/auth/Token.js deleted file mode 100644 index c3e5fd958..000000000 --- a/frontend/pages/api/auth/Token.js +++ /dev/null @@ -1,17 +0,0 @@ -const token = async (req, res) => { - return fetch("/api/v1/auth/token", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - credentials: "include", - }).then(async (res) => { - if (res.status == 200) { - return (await res.json()).token; - } else { - console.log("Getting a new token failed"); - } - }); -}; - -export default token; diff --git a/frontend/pages/api/auth/Token.ts b/frontend/pages/api/auth/Token.ts new file mode 100644 index 000000000..ed347ba4b --- /dev/null +++ b/frontend/pages/api/auth/Token.ts @@ -0,0 +1,17 @@ +const token = async () => { + return fetch('/api/v1/auth/token', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + credentials: 'include' + }).then(async (res) => { + if (res.status == 200) { + return (await res.json()).token; + } else { + console.log('Getting a new token failed'); + } + }); +}; + +export default token; diff --git a/frontend/pages/api/auth/VerifySignupInvite.js b/frontend/pages/api/auth/VerifySignupInvite.js deleted file mode 100644 index 2a9ba4dcd..000000000 --- a/frontend/pages/api/auth/VerifySignupInvite.js +++ /dev/null @@ -1,20 +0,0 @@ -/** - * This route verifies the signup invite link - * @param {*} email - * @param {*} code - * @returns - */ -const verifySignupInvite = ({ email, code }) => { - return fetch("/api/v1/invite-org/verify", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - email, - code, - }), - }); -}; - -export default verifySignupInvite; diff --git a/frontend/pages/api/auth/VerifySignupInvite.ts b/frontend/pages/api/auth/VerifySignupInvite.ts new file mode 100644 index 000000000..0b6cc7577 --- /dev/null +++ b/frontend/pages/api/auth/VerifySignupInvite.ts @@ -0,0 +1,26 @@ +interface Props { + email: string; + code: string; +} + +/** + * This route verifies the signup invite link + * @param {object} obj + * @param {string} obj.email - email that a user is trying to verify + * @param {string} obj.code - code that a user received to the abovementioned email + * @returns + */ +const verifySignupInvite = ({ email, code }: Props) => { + return fetch('/api/v1/invite-org/verify', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + email, + code + }) + }); +}; + +export default verifySignupInvite; diff --git a/frontend/pages/api/auth/getBackupEncryptedPrivateKey.ts b/frontend/pages/api/auth/getBackupEncryptedPrivateKey.ts new file mode 100644 index 000000000..826085263 --- /dev/null +++ b/frontend/pages/api/auth/getBackupEncryptedPrivateKey.ts @@ -0,0 +1,26 @@ +/** + * This is the route that get an encrypted private key (will be decrypted with a backup key) + * @param {object} obj + * @param {object} obj.verificationToken - this is the token that confirms that a user is the right one + * @returns + */ +const getBackupEncryptedPrivateKey = ({ + verificationToken +}: { + verificationToken: string; +}) => { + return fetch('/api/v1/password/backup-private-key', { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer ' + verificationToken + } + }).then(async (res) => { + if (res?.status !== 200) { + console.log('Failed to get the backup key'); + } + return (await res?.json())?.backupPrivateKey; + }); +}; + +export default getBackupEncryptedPrivateKey; diff --git a/frontend/pages/api/auth/publicKeyInfisical.js b/frontend/pages/api/auth/publicKeyInfisical.js deleted file mode 100644 index 76cad1726..000000000 --- a/frontend/pages/api/auth/publicKeyInfisical.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * This route lets us get the public key of infisical. Th euser doesn't have to be authenticated since this is just the public key. - * @param {*} req - * @param {*} res - * @returns - */ -const publicKeyInfisical = (req, res) => { - return fetch("/api/v1/key/publicKey/infisical", { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }); -}; - -export default publicKeyInfisical; diff --git a/frontend/pages/api/auth/publicKeyInfisical.ts b/frontend/pages/api/auth/publicKeyInfisical.ts new file mode 100644 index 000000000..d3e0f646c --- /dev/null +++ b/frontend/pages/api/auth/publicKeyInfisical.ts @@ -0,0 +1,10 @@ +const publicKeyInfisical = () => { + return fetch('/api/v1/key/publicKey/infisical', { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }); +}; + +export default publicKeyInfisical; diff --git a/frontend/pages/api/auth/resetPasswordOnAccountRecovery.ts b/frontend/pages/api/auth/resetPasswordOnAccountRecovery.ts new file mode 100644 index 000000000..f2b5ad98e --- /dev/null +++ b/frontend/pages/api/auth/resetPasswordOnAccountRecovery.ts @@ -0,0 +1,50 @@ +interface Props { + verificationToken: string; + encryptedPrivateKey: string; + iv: string; + tag: string; + salt: string; + verifier: string; +} + +/** + * This is the route that resets the account password if all the previus steps were passed + * @param {object} obj + * @param {object} obj.verificationToken - this is the token that confirms that a user is the right one + * @param {object} obj.encryptedPrivateKey - the new encrypted private key (encrypted using the new password) + * @param {object} obj.iv + * @param {object} obj.tag + * @param {object} obj.salt + * @param {object} obj.verifier + * @returns + */ +const resetPasswordOnAccountRecovery = ({ + verificationToken, + encryptedPrivateKey, + iv, + tag, + salt, + verifier +}: Props) => { + return fetch('/api/v1/password/password-reset', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer ' + verificationToken + }, + body: JSON.stringify({ + encryptedPrivateKey: encryptedPrivateKey, + iv: iv, + tag: tag, + salt: salt, + verifier: verifier + }) + }).then(async (res) => { + if (res?.status !== 200) { + console.log('Failed to get the backup key'); + } + return res; + }); +}; + +export default resetPasswordOnAccountRecovery; diff --git a/frontend/pages/api/bot/getBot.ts b/frontend/pages/api/bot/getBot.ts new file mode 100644 index 000000000..145b50891 --- /dev/null +++ b/frontend/pages/api/bot/getBot.ts @@ -0,0 +1,31 @@ +import SecurityClient from "~/utilities/SecurityClient"; + +interface Props { + workspaceId: string; +} + +/** + * This function fetches the bot for a project + * @param {Object} obj + * @param {String} obj.workspaceId + * @returns + */ +const getBot = async ({ workspaceId }: Props) => { + return SecurityClient.fetchCall( + "/api/v1/bot/" + workspaceId, + { + method: "GET", + headers: { + "Content-Type": "application/json", + } + } + ).then(async (res) => { + if (res && res.status == 200) { + return (await res.json()).bot; + } else { + console.log("Failed to get bot for project"); + } + }); +}; + +export default getBot; \ No newline at end of file diff --git a/frontend/pages/api/bot/setBotActiveStatus.ts b/frontend/pages/api/bot/setBotActiveStatus.ts new file mode 100644 index 000000000..0a974a588 --- /dev/null +++ b/frontend/pages/api/bot/setBotActiveStatus.ts @@ -0,0 +1,46 @@ +import SecurityClient from "~/utilities/SecurityClient"; + +interface BotKey { + encryptedKey: string; + nonce: string; +} + +interface Props { + botId: string; + isActive: boolean; + botKey: BotKey; +} + +/** + * This function sets the active status of a bot and shares a copy of + * the project key (encrypted under the bot's public key) with the + * project's bot + * @param {Object} obj + * @param {String} obj.botId + * @param {String} obj.isActive + * @param {Object} obj.botKey + * @returns + */ +const setBotActiveStatus = async ({ botId, isActive, botKey }: Props) => { + return SecurityClient.fetchCall( + "/api/v1/bot/" + botId + "/active", + { + method: "PATCH", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + isActive, + botKey + }) + } + ).then(async (res) => { + if (res && res.status == 200) { + return await res.json(); + } else { + console.log("Failed to get bot for project"); + } + }); +}; + +export default setBotActiveStatus; \ No newline at end of file diff --git a/frontend/pages/api/files/GetSecrets.js b/frontend/pages/api/files/GetSecrets.js deleted file mode 100644 index fa91d0962..000000000 --- a/frontend/pages/api/files/GetSecrets.js +++ /dev/null @@ -1,33 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient.js"; - -/** - * This function fetches the encrypted secrets from the .env file - * @param {*} workspaceId - * @param {*} env - * @returns - */ -const getSecrets = async (workspaceId, env) => { - return SecurityClient.fetchCall( - "/api/v1/secret/" + - workspaceId + - "?" + - new URLSearchParams({ - environment: env, - channel: "web", - }), - { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - } - ).then(async (res) => { - if (res.status == 200) { - return await res.json(); - } else { - console.log("Failed to get project secrets"); - } - }); -}; - -export default getSecrets; diff --git a/frontend/pages/api/files/GetSecrets.ts b/frontend/pages/api/files/GetSecrets.ts new file mode 100644 index 000000000..8ff22b57c --- /dev/null +++ b/frontend/pages/api/files/GetSecrets.ts @@ -0,0 +1,33 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +/** + * This function fetches the encrypted secrets from the .env file + * @param {string} workspaceId - project is for which a user is trying to get secrets + * @param {string} env - environment of a project for which a user is trying ot get secrets + * @returns + */ +const getSecrets = async (workspaceId: string, env: string) => { + return SecurityClient.fetchCall( + '/api/v1/secret/' + + workspaceId + + '?' + + new URLSearchParams({ + environment: env, + channel: 'web' + }), + { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + } + ).then(async (res) => { + if (res && res.status == 200) { + return await res.json(); + } else { + console.log('Failed to get project secrets'); + } + }); +}; + +export default getSecrets; diff --git a/frontend/pages/api/files/UploadSecrets.js b/frontend/pages/api/files/UploadSecrets.js deleted file mode 100644 index c2f94e64d..000000000 --- a/frontend/pages/api/files/UploadSecrets.js +++ /dev/null @@ -1,30 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This function uploads the encrypted .env file - * @param {*} req - * @param {*} res - * @returns - */ -const uploadSecrets = async ({ workspaceId, secrets, keys, environment }) => { - return SecurityClient.fetchCall("/api/v1/secret/" + workspaceId, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - secrets, - keys, - environment, - channel: "web", - }), - }).then(async (res) => { - if (res.status == 200) { - return res; - } else { - console.log("Failed to push secrets"); - } - }); -}; - -export default uploadSecrets; diff --git a/frontend/pages/api/files/UploadSecrets.ts b/frontend/pages/api/files/UploadSecrets.ts new file mode 100644 index 000000000..04fcb78b5 --- /dev/null +++ b/frontend/pages/api/files/UploadSecrets.ts @@ -0,0 +1,45 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +interface Props { + workspaceId: string; + secrets: any; + keys: string; + environment: string; +} + +/** + * This function uploads the encrypted .env file + * @param {object} obj + * @param {string} obj.workspaceId + * @param {} obj.secrets + * @param {} obj.keys + * @param {string} obj.environment + * @returns + */ +const uploadSecrets = async ({ + workspaceId, + secrets, + keys, + environment +}: Props) => { + return SecurityClient.fetchCall('/api/v1/secret/' + workspaceId, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + secrets, + keys, + environment, + channel: 'web' + }) + }).then(async (res) => { + if (res && res.status == 200) { + return res; + } else { + console.log('Failed to push secrets'); + } + }); +}; + +export default uploadSecrets; diff --git a/frontend/pages/api/integrations/ChangeHerokuConfigVars.js b/frontend/pages/api/integrations/ChangeHerokuConfigVars.js deleted file mode 100644 index 118848ae6..000000000 --- a/frontend/pages/api/integrations/ChangeHerokuConfigVars.js +++ /dev/null @@ -1,25 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -const changeHerokuConfigVars = ({ integrationId, key, secrets }) => { - return SecurityClient.fetchCall( - "/api/v1/integration/" + integrationId + "/sync", - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - key, - secrets, - }), - } - ).then(async (res) => { - if (res.status == 200) { - return res; - } else { - console.log("Failed to sync secrets to Heroku"); - } - }); -}; - -export default changeHerokuConfigVars; diff --git a/frontend/pages/api/integrations/ChangeHerokuConfigVars.ts b/frontend/pages/api/integrations/ChangeHerokuConfigVars.ts new file mode 100644 index 000000000..e011bda7b --- /dev/null +++ b/frontend/pages/api/integrations/ChangeHerokuConfigVars.ts @@ -0,0 +1,41 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +interface Props { + integrationId: string; + key: { encryptedKey: any; nonce: any }; + secrets: { + ciphertextKey: any; + ivKey: any; + tagKey: any; + hashKey: any; + ciphertextValue: any; + ivValue: any; + tagValue: any; + hashValue: any; + type: string; + }[]; +} + +const changeHerokuConfigVars = ({ integrationId, key, secrets }: Props) => { + return SecurityClient.fetchCall( + '/api/v1/integration/' + integrationId + '/sync', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + key, + secrets + }) + } + ).then(async (res) => { + if (res && res.status == 200) { + return res; + } else { + console.log('Failed to sync secrets to Heroku'); + } + }); +}; + +export default changeHerokuConfigVars; diff --git a/frontend/pages/api/integrations/DeleteIntegration.js b/frontend/pages/api/integrations/DeleteIntegration.js deleted file mode 100644 index 7698d7eae..000000000 --- a/frontend/pages/api/integrations/DeleteIntegration.js +++ /dev/null @@ -1,26 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This route deletes an integration from a certain project - * @param {*} integrationId - * @returns - */ -const deleteIntegration = ({ integrationId }) => { - return SecurityClient.fetchCall( - "/api/v1/integration/" + integrationId, - { - method: "DELETE", - headers: { - "Content-Type": "application/json", - }, - } - ).then(async (res) => { - if (res.status == 200) { - return (await res.json()).workspace; - } else { - console.log("Failed to delete an integration"); - } - }); -}; - -export default deleteIntegration; diff --git a/frontend/pages/api/integrations/DeleteIntegration.ts b/frontend/pages/api/integrations/DeleteIntegration.ts new file mode 100644 index 000000000..89aa0ab0c --- /dev/null +++ b/frontend/pages/api/integrations/DeleteIntegration.ts @@ -0,0 +1,27 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +interface Props { + integrationId: string; +} + +/** + * This route deletes an integration from a certain project + * @param {*} integrationId + * @returns + */ +const deleteIntegration = ({ integrationId }: Props) => { + return SecurityClient.fetchCall('/api/v1/integration/' + integrationId, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json' + } + }).then(async (res) => { + if (res && res.status == 200) { + return (await res.json()).workspace; + } else { + console.log('Failed to delete an integration'); + } + }); +}; + +export default deleteIntegration; diff --git a/frontend/pages/api/integrations/DeleteIntegrationAuth.js b/frontend/pages/api/integrations/DeleteIntegrationAuth.js deleted file mode 100644 index eb6106e8e..000000000 --- a/frontend/pages/api/integrations/DeleteIntegrationAuth.js +++ /dev/null @@ -1,26 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This route deletes an integration authorization from a certain project - * @param {*} integrationAuthId - * @returns - */ -const deleteIntegrationAuth = ({ integrationAuthId }) => { - return SecurityClient.fetchCall( - "/api/v1/integration-auth/" + integrationAuthId, - { - method: "DELETE", - headers: { - "Content-Type": "application/json", - }, - } - ).then(async (res) => { - if (res.status == 200) { - return res; - } else { - console.log("Failed to delete an integration authorization"); - } - }); -}; - -export default deleteIntegrationAuth; diff --git a/frontend/pages/api/integrations/DeleteIntegrationAuth.ts b/frontend/pages/api/integrations/DeleteIntegrationAuth.ts new file mode 100644 index 000000000..3a2da2cbf --- /dev/null +++ b/frontend/pages/api/integrations/DeleteIntegrationAuth.ts @@ -0,0 +1,30 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +interface Props { + integrationAuthId: string; +} + +/** + * This route deletes an integration authorization from a certain project + * @param {*} integrationAuthId + * @returns + */ +const deleteIntegrationAuth = ({ integrationAuthId }: Props) => { + return SecurityClient.fetchCall( + '/api/v1/integration-auth/' + integrationAuthId, + { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json' + } + } + ).then(async (res) => { + if (res && res.status == 200) { + return res; + } else { + console.log('Failed to delete an integration authorization'); + } + }); +}; + +export default deleteIntegrationAuth; diff --git a/frontend/pages/api/integrations/GetIntegrationApps.js b/frontend/pages/api/integrations/GetIntegrationApps.js deleted file mode 100644 index dc0bfb4b6..000000000 --- a/frontend/pages/api/integrations/GetIntegrationApps.js +++ /dev/null @@ -1,21 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -const getIntegrationApps = ({ integrationAuthId }) => { - return SecurityClient.fetchCall( - "/api/v1/integration-auth/" + integrationAuthId + "/apps", - { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - } - ).then(async (res) => { - if (res.status == 200) { - return (await res.json()).apps; - } else { - console.log("Failed to get available apps for an integration"); - } - }); -}; - -export default getIntegrationApps; diff --git a/frontend/pages/api/integrations/GetIntegrationApps.ts b/frontend/pages/api/integrations/GetIntegrationApps.ts new file mode 100644 index 000000000..5597c24b1 --- /dev/null +++ b/frontend/pages/api/integrations/GetIntegrationApps.ts @@ -0,0 +1,25 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +interface Props { + integrationAuthId: string; +} + +const getIntegrationApps = ({ integrationAuthId }: Props) => { + return SecurityClient.fetchCall( + '/api/v1/integration-auth/' + integrationAuthId + '/apps', + { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + } + ).then(async (res) => { + if (res && res.status == 200) { + return (await res.json()).apps; + } else { + console.log('Failed to get available apps for an integration'); + } + }); +}; + +export default getIntegrationApps; diff --git a/frontend/pages/api/integrations/GetIntegrationOptions.ts b/frontend/pages/api/integrations/GetIntegrationOptions.ts new file mode 100644 index 000000000..caf0c8626 --- /dev/null +++ b/frontend/pages/api/integrations/GetIntegrationOptions.ts @@ -0,0 +1,21 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +const getIntegrationOptions = () => { + return SecurityClient.fetchCall( + '/api/v1/integration-auth/integration-options', + { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + } + ).then(async (res) => { + if (res && res.status == 200) { + return (await res.json()).integrationOptions; + } else { + console.log('Failed to get (cloud) integration options'); + } + }); +}; + +export default getIntegrationOptions; diff --git a/frontend/pages/api/integrations/GetIntegrations.js b/frontend/pages/api/integrations/GetIntegrations.js deleted file mode 100644 index 401c80d3b..000000000 --- a/frontend/pages/api/integrations/GetIntegrations.js +++ /dev/null @@ -1,18 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -const getIntegrations = () => { - return SecurityClient.fetchCall("/api/v1/integration/integrations", { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }).then(async (res) => { - if (res.status == 200) { - return (await res.json()).integrations; - } else { - console.log("Failed to get project integrations"); - } - }); -}; - -export default getIntegrations; diff --git a/frontend/pages/api/integrations/StartIntegration.js b/frontend/pages/api/integrations/StartIntegration.js deleted file mode 100644 index a4e8b0b02..000000000 --- a/frontend/pages/api/integrations/StartIntegration.js +++ /dev/null @@ -1,33 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This route starts the integration after teh default one if gonna set up. - * @param {*} integrationId - * @returns - */ -const startIntegration = ({ integrationId, appName, environment }) => { - return SecurityClient.fetchCall( - "/api/v1/integration/" + integrationId, - { - method: "PATCH", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - update: { - app: appName, - environment, - isActive: true, - }, - }), - } - ).then(async (res) => { - if (res.status == 200) { - return res; - } else { - console.log("Failed to start an integration"); - } - }); -}; - -export default startIntegration; diff --git a/frontend/pages/api/integrations/StartIntegration.ts b/frontend/pages/api/integrations/StartIntegration.ts new file mode 100644 index 000000000..bef82e7e1 --- /dev/null +++ b/frontend/pages/api/integrations/StartIntegration.ts @@ -0,0 +1,36 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +interface Props { + integrationId: string; + appName: string; + environment: string; +} + +/** + * This route starts the integration after teh default one if gonna set up. + * @param {*} integrationId + * @returns + */ +const startIntegration = ({ integrationId, appName, environment }: Props) => { + return SecurityClient.fetchCall('/api/v1/integration/' + integrationId, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + update: { + app: appName, + environment, + isActive: true + } + }) + }).then(async (res) => { + if (res && res.status == 200) { + return res; + } else { + console.log('Failed to start an integration'); + } + }); +}; + +export default startIntegration; diff --git a/frontend/pages/api/integrations/authorizeIntegration.js b/frontend/pages/api/integrations/authorizeIntegration.js deleted file mode 100644 index b9a1d3995..000000000 --- a/frontend/pages/api/integrations/authorizeIntegration.js +++ /dev/null @@ -1,31 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This is the first step of the change password process (pake) - * @param {*} clientPublicKey - * @returns - */ -const AuthorizeIntegration = ({ workspaceId, code, integration }) => { - return SecurityClient.fetchCall( - "/api/v1/integration-auth/oauth-token", - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - workspaceId, - code, - integration, - }), - } - ).then(async (res) => { - if (res.status == 200) { - return res; - } else { - console.log("Failed to authorize the integration"); - } - }); -}; - -export default AuthorizeIntegration; diff --git a/frontend/pages/api/integrations/authorizeIntegration.ts b/frontend/pages/api/integrations/authorizeIntegration.ts new file mode 100644 index 000000000..1454bde4d --- /dev/null +++ b/frontend/pages/api/integrations/authorizeIntegration.ts @@ -0,0 +1,36 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +interface Props { + workspaceId: string; + code: string; + integration: string; +} +/** + * This is the first step of the change password process (pake) + * @param {object} obj + * @param {object} obj.workspaceId - project id for which we want to authorize the integration + * @param {object} obj.code + * @param {object} obj.integration - integration which a user is trying to turn on + * @returns + */ +const AuthorizeIntegration = ({ workspaceId, code, integration }: Props) => { + return SecurityClient.fetchCall('/api/v1/integration-auth/oauth-token', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + workspaceId, + code, + integration + }) + }).then(async (res) => { + if (res && res.status == 200) { + return res; + } else { + console.log('Failed to authorize the integration'); + } + }); +}; + +export default AuthorizeIntegration; diff --git a/frontend/pages/api/integrations/getWorkspaceAuthorizations.js b/frontend/pages/api/integrations/getWorkspaceAuthorizations.js deleted file mode 100644 index ad8c4732b..000000000 --- a/frontend/pages/api/integrations/getWorkspaceAuthorizations.js +++ /dev/null @@ -1,26 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This route gets authorizations of a certain project (Heroku, etc.) - * @param {*} workspaceId - * @returns - */ -const getWorkspaceAuthorizations = ({ workspaceId }) => { - return SecurityClient.fetchCall( - "/api/v1/workspace/" + workspaceId + "/authorizations", - { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - } - ).then(async (res) => { - if (res.status == 200) { - return (await res.json()).authorizations; - } else { - console.log("Failed to get project authorizations"); - } - }); -}; - -export default getWorkspaceAuthorizations; diff --git a/frontend/pages/api/integrations/getWorkspaceAuthorizations.ts b/frontend/pages/api/integrations/getWorkspaceAuthorizations.ts new file mode 100644 index 000000000..f1b406555 --- /dev/null +++ b/frontend/pages/api/integrations/getWorkspaceAuthorizations.ts @@ -0,0 +1,30 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +interface Props { + workspaceId: string; +} + +/** + * This route gets authorizations of a certain project (Heroku, etc.) + * @param {*} workspaceId + * @returns + */ +const getWorkspaceAuthorizations = ({ workspaceId }: Props) => { + return SecurityClient.fetchCall( + '/api/v1/workspace/' + workspaceId + '/authorizations', + { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + } + ).then(async (res) => { + if (res && res.status == 200) { + return (await res.json()).authorizations; + } else { + console.log('Failed to get project authorizations'); + } + }); +}; + +export default getWorkspaceAuthorizations; diff --git a/frontend/pages/api/integrations/getWorkspaceIntegrations.js b/frontend/pages/api/integrations/getWorkspaceIntegrations.js deleted file mode 100644 index 22470e4be..000000000 --- a/frontend/pages/api/integrations/getWorkspaceIntegrations.js +++ /dev/null @@ -1,26 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This route gets integrations of a certain project (Heroku, etc.) - * @param {*} workspaceId - * @returns - */ -const getWorkspaceIntegrations = ({ workspaceId }) => { - return SecurityClient.fetchCall( - "/api/v1/workspace/" + workspaceId + "/integrations", - { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - } - ).then(async (res) => { - if (res.status == 200) { - return (await res.json()).integrations; - } else { - console.log("Failed to get the project integrations"); - } - }); -}; - -export default getWorkspaceIntegrations; diff --git a/frontend/pages/api/integrations/getWorkspaceIntegrations.ts b/frontend/pages/api/integrations/getWorkspaceIntegrations.ts new file mode 100644 index 000000000..a33256749 --- /dev/null +++ b/frontend/pages/api/integrations/getWorkspaceIntegrations.ts @@ -0,0 +1,30 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +interface Props { + workspaceId: string; +} + +/** + * This route gets integrations of a certain project (Heroku, etc.) + * @param {*} workspaceId + * @returns + */ +const getWorkspaceIntegrations = ({ workspaceId }: Props) => { + return SecurityClient.fetchCall( + '/api/v1/workspace/' + workspaceId + '/integrations', + { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + } + ).then(async (res) => { + if (res && res.status == 200) { + return (await res.json()).integrations; + } else { + console.log('Failed to get the project integrations'); + } + }); +}; + +export default getWorkspaceIntegrations; diff --git a/frontend/pages/api/integrations/updateIntegration.ts b/frontend/pages/api/integrations/updateIntegration.ts new file mode 100644 index 000000000..8afcfc0a9 --- /dev/null +++ b/frontend/pages/api/integrations/updateIntegration.ts @@ -0,0 +1,60 @@ +import SecurityClient from "~/utilities/SecurityClient"; + +/** + * This route starts the integration after teh default one if gonna set up. + * Update integration with id [integrationId] to sync envars from the project's + * [environment] to the integration [app] with active state [isActive] + * @param {Object} obj + * @param {String} obj.integrationId - id of integration + * @param {String} obj.app - name of app + * @param {String} obj.environment - project environment to push secrets from + * @param {Boolean} obj.isActive - active state + * @param {String} obj.target - (optional) target (environment) for Vercel integration + * @param {String} obj.context - (optional) context (environment) for Netlify integration + * @param {String} obj.siteId - (optional) app (site_id) for Netlify integration + * @returns + */ +const updateIntegration = ({ + integrationId, + app, + environment, + isActive, + target, + context, + siteId +}: { + integrationId: string, + app: string, + environment: string, + isActive: boolean, + target: string | null, + context: string | null, + siteId: string | null + +}) => { + return SecurityClient.fetchCall( + "/api/v1/integration/" + integrationId, + { + method: "PATCH", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + app, + environment, + isActive, + target, + context, + siteId + }), + } + ).then(async (res) => { + if (res && res.status == 200) { + return res; + } else { + console.log("Failed to start an integration"); + } + }); +}; + +export default updateIntegration; diff --git a/frontend/pages/api/organization/GetOrg.ts b/frontend/pages/api/organization/GetOrg.ts index ecb07bd2d..5e56e5c72 100644 --- a/frontend/pages/api/organization/GetOrg.ts +++ b/frontend/pages/api/organization/GetOrg.ts @@ -1,21 +1,21 @@ -import SecurityClient from "~/utilities/SecurityClient"; +import SecurityClient from '~/utilities/SecurityClient'; /** * This route lets us get info about a certain org * @param {string} orgId - the organization ID * @returns */ -const getOrganization = ({ orgId }: { orgId: string; }) => { - return SecurityClient.fetchCall("/api/v1/organization/" + orgId, { - method: "GET", +const getOrganization = ({ orgId }: { orgId: string }) => { + return SecurityClient.fetchCall('/api/v1/organization/' + orgId, { + method: 'GET', headers: { - "Content-Type": "application/json", - }, + 'Content-Type': 'application/json' + } }).then(async (res) => { if (res?.status == 200) { return (await res.json()).organization; } else { - console.log("Failed to get org info"); + console.log('Failed to get org info'); } }); }; diff --git a/frontend/pages/api/organization/GetOrgProjects.js b/frontend/pages/api/organization/GetOrgProjects.js deleted file mode 100644 index 656fea18f..000000000 --- a/frontend/pages/api/organization/GetOrgProjects.js +++ /dev/null @@ -1,27 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This route lets us get all the users in an org. - * @param {*} req - * @param {*} res - * @returns - */ -const getOrganizationProjects = (req, res) => { - return SecurityClient.fetchCall( - "/api/organization/" + req.orgId + "/workspaces", - { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - } - ).then(async (res) => { - if (res.status == 200) { - return (await res.json()).workspaces; - } else { - console.log("Failed to get projects for an org"); - } - }); -}; - -export default getOrganizationProjects; diff --git a/frontend/pages/api/organization/GetOrgProjects.ts b/frontend/pages/api/organization/GetOrgProjects.ts new file mode 100644 index 000000000..954694486 --- /dev/null +++ b/frontend/pages/api/organization/GetOrgProjects.ts @@ -0,0 +1,29 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +/** + * This route lets us get all the users in an org. + * @param {*} req + * @param {*} res + * @returns + */ + +// TODO: this file is not used anywhere +const getOrganizationProjects = (req: { orgId: string }) => { + return SecurityClient.fetchCall( + '/api/organization/' + req.orgId + '/workspaces', + { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + } + ).then(async (res) => { + if (res && res.status == 200) { + return (await res.json()).workspaces; + } else { + console.log('Failed to get projects for an org'); + } + }); +}; + +export default getOrganizationProjects; diff --git a/frontend/pages/api/organization/GetOrgSubscription.js b/frontend/pages/api/organization/GetOrgSubscription.js deleted file mode 100644 index 97c6f4e5c..000000000 --- a/frontend/pages/api/organization/GetOrgSubscription.js +++ /dev/null @@ -1,27 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This route lets us get the current subscription of an org. - * @param {*} req - * @param {*} res - * @returns - */ -const getOrganizationSubscriptions = (req, res) => { - return SecurityClient.fetchCall( - "/api/v1/organization/" + req.orgId + "/subscriptions", - { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - } - ).then(async (res) => { - if (res.status == 200) { - return (await res.json()).subscriptions; - } else { - console.log("Failed to get org subscriptions"); - } - }); -}; - -export default getOrganizationSubscriptions; diff --git a/frontend/pages/api/organization/GetOrgSubscription.ts b/frontend/pages/api/organization/GetOrgSubscription.ts new file mode 100644 index 000000000..c93679cdc --- /dev/null +++ b/frontend/pages/api/organization/GetOrgSubscription.ts @@ -0,0 +1,27 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +/** + * This route lets us get the current subscription of an org. + * @param {*} req + * @param {*} res + * @returns + */ +const getOrganizationSubscriptions = (req: { orgId: string }) => { + return SecurityClient.fetchCall( + '/api/v1/organization/' + req.orgId + '/subscriptions', + { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + } + ).then(async (res) => { + if (res && res.status == 200) { + return (await res.json()).subscriptions; + } else { + console.log('Failed to get org subscriptions'); + } + }); +}; + +export default getOrganizationSubscriptions; diff --git a/frontend/pages/api/organization/GetOrgUserProjects.js b/frontend/pages/api/organization/GetOrgUserProjects.js deleted file mode 100644 index c2d751f64..000000000 --- a/frontend/pages/api/organization/GetOrgUserProjects.js +++ /dev/null @@ -1,27 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This route lets us get all the projects of a certain user in an org. - * @param {*} req - * @param {*} res - * @returns - */ -const getOrganizationUserProjects = (req) => { - return SecurityClient.fetchCall( - "/api/v1/organization/" + req.orgId + "/my-workspaces", - { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - } - ).then(async (res) => { - if (res.status == 200) { - return (await res.json()).workspaces; - } else { - console.log("Failed to get projects of a user in an org"); - } - }); -}; - -export default getOrganizationUserProjects; diff --git a/frontend/pages/api/organization/GetOrgUserProjects.ts b/frontend/pages/api/organization/GetOrgUserProjects.ts new file mode 100644 index 000000000..872b8d781 --- /dev/null +++ b/frontend/pages/api/organization/GetOrgUserProjects.ts @@ -0,0 +1,27 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +/** + * This route lets us get all the projects of a certain user in an org. + * @param {*} req + * @param {*} res + * @returns + */ +const getOrganizationUserProjects = (req: { orgId: string }) => { + return SecurityClient.fetchCall( + '/api/v1/organization/' + req.orgId + '/my-workspaces', + { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + } + ).then(async (res) => { + if (res && res.status == 200) { + return (await res.json()).workspaces; + } else { + console.log('Failed to get projects of a user in an org'); + } + }); +}; + +export default getOrganizationUserProjects; diff --git a/frontend/pages/api/organization/GetOrgUsers.ts b/frontend/pages/api/organization/GetOrgUsers.ts index 53b9518cf..f1aaa7209 100644 --- a/frontend/pages/api/organization/GetOrgUsers.ts +++ b/frontend/pages/api/organization/GetOrgUsers.ts @@ -1,4 +1,4 @@ -import SecurityClient from "~/utilities/SecurityClient"; +import SecurityClient from '~/utilities/SecurityClient'; /** * This route lets us get all the users in an org. @@ -6,20 +6,17 @@ import SecurityClient from "~/utilities/SecurityClient"; * @param {string} obj.orgId - organization Id * @returns */ -const getOrganizationUsers = ({ orgId }: { orgId: string; }) => { - return SecurityClient.fetchCall( - "/api/v1/organization/" + orgId + "/users", - { - method: "GET", - headers: { - "Content-Type": "application/json", - }, +const getOrganizationUsers = ({ orgId }: { orgId: string }) => { + return SecurityClient.fetchCall('/api/v1/organization/' + orgId + '/users', { + method: 'GET', + headers: { + 'Content-Type': 'application/json' } - ).then(async (res) => { + }).then(async (res) => { if (res?.status == 200) { return (await res.json()).users; } else { - console.log("Failed to get org users"); + console.log('Failed to get org users'); } }); }; diff --git a/frontend/pages/api/organization/StripeRedirect.js b/frontend/pages/api/organization/StripeRedirect.js deleted file mode 100644 index e8911cf53..000000000 --- a/frontend/pages/api/organization/StripeRedirect.js +++ /dev/null @@ -1,27 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This route redirects the user to the right stripe billing page. - * @param {*} req - * @param {*} res - * @returns - */ -const StripeRedirect = ({ orgId }) => { - return SecurityClient.fetchCall( - "/api/v1/organization/" + orgId + "/customer-portal-session", - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - } - ).then(async (res) => { - if (res.status == 200) { - return (window.location.href = (await res.json()).url); - } else { - console.log("Failed to redirect to Stripe"); - } - }); -}; - -export default StripeRedirect; diff --git a/frontend/pages/api/organization/StripeRedirect.ts b/frontend/pages/api/organization/StripeRedirect.ts new file mode 100644 index 000000000..a9fe1f066 --- /dev/null +++ b/frontend/pages/api/organization/StripeRedirect.ts @@ -0,0 +1,27 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +/** + * This route redirects the user to the right stripe billing page. + * @param {*} req + * @param {*} res + * @returns + */ +const StripeRedirect = ({ orgId }: { orgId: string }) => { + return SecurityClient.fetchCall( + '/api/v1/organization/' + orgId + '/customer-portal-session', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + } + ).then(async (res) => { + if (res && res.status == 200) { + return (window.location.href = (await res.json()).url); + } else { + console.log('Failed to redirect to Stripe'); + } + }); +}; + +export default StripeRedirect; diff --git a/frontend/pages/api/organization/addIncidentContact.js b/frontend/pages/api/organization/addIncidentContact.js deleted file mode 100644 index 0e6068a1a..000000000 --- a/frontend/pages/api/organization/addIncidentContact.js +++ /dev/null @@ -1,29 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This route add an incident contact email to a certain organization - * @param {*} param0 - * @returns - */ -const addIncidentContact = (organizationId, email) => { - return SecurityClient.fetchCall( - "/api/v1/organization/" + organizationId + "/incidentContactOrg", - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - email: email, - }), - } - ).then(async (res) => { - if (res.status == 200) { - return res; - } else { - console.log("Failed to add an incident contact"); - } - }); -}; - -export default addIncidentContact; diff --git a/frontend/pages/api/organization/addIncidentContact.ts b/frontend/pages/api/organization/addIncidentContact.ts new file mode 100644 index 000000000..d3676e641 --- /dev/null +++ b/frontend/pages/api/organization/addIncidentContact.ts @@ -0,0 +1,29 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +/** + * This route add an incident contact email to a certain organization + * @param {*} param0 + * @returns + */ +const addIncidentContact = (organizationId: string, email: string) => { + return SecurityClient.fetchCall( + '/api/v1/organization/' + organizationId + '/incidentContactOrg', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + email: email + }) + } + ).then(async (res) => { + if (res && res.status == 200) { + return res; + } else { + console.log('Failed to add an incident contact'); + } + }); +}; + +export default addIncidentContact; diff --git a/frontend/pages/api/organization/addUserToOrg.js b/frontend/pages/api/organization/addUserToOrg.js deleted file mode 100644 index 15f22cff9..000000000 --- a/frontend/pages/api/organization/addUserToOrg.js +++ /dev/null @@ -1,28 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This function sends an email invite to a user to join an org - * @param {*} email - * @param {*} orgId - * @returns - */ -const addUserToOrg = (email, orgId) => { - return SecurityClient.fetchCall("/api/v1/invite-org/signup", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - inviteeEmail: email, - organizationId: orgId, - }), - }).then(async (res) => { - if (res.status == 200) { - return res; - } else { - console.log("Failed to add a user to an org"); - } - }); -}; - -export default addUserToOrg; diff --git a/frontend/pages/api/organization/addUserToOrg.ts b/frontend/pages/api/organization/addUserToOrg.ts new file mode 100644 index 000000000..70dbad56c --- /dev/null +++ b/frontend/pages/api/organization/addUserToOrg.ts @@ -0,0 +1,28 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +/** + * This function sends an email invite to a user to join an org + * @param {*} email + * @param {*} orgId + * @returns + */ +const addUserToOrg = (email: string, orgId: string) => { + return SecurityClient.fetchCall('/api/v1/invite-org/signup', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + inviteeEmail: email, + organizationId: orgId + }) + }).then(async (res) => { + if (res && res.status == 200) { + return res; + } else { + console.log('Failed to add a user to an org'); + } + }); +}; + +export default addUserToOrg; diff --git a/frontend/pages/api/organization/deleteIncidentContact.js b/frontend/pages/api/organization/deleteIncidentContact.js deleted file mode 100644 index f6e57c590..000000000 --- a/frontend/pages/api/organization/deleteIncidentContact.js +++ /dev/null @@ -1,29 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This route deletes an incident Contact from a certain organization - * @param {*} param0 - * @returns - */ -const deleteIncidentContact = (organizaionId, email) => { - return SecurityClient.fetchCall( - "/api/v1/organization/" + organizaionId + "/incidentContactOrg", - { - method: "DELETE", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - email: email, - }), - } - ).then(async (res) => { - if (res.status == 200) { - return res; - } else { - console.log("Failed to delete an incident contact"); - } - }); -}; - -export default deleteIncidentContact; diff --git a/frontend/pages/api/organization/deleteIncidentContact.ts b/frontend/pages/api/organization/deleteIncidentContact.ts new file mode 100644 index 000000000..fd6374e9b --- /dev/null +++ b/frontend/pages/api/organization/deleteIncidentContact.ts @@ -0,0 +1,29 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +/** + * This route deletes an incident Contact from a certain organization + * @param {*} param0 + * @returns + */ +const deleteIncidentContact = (organizationId: string, email: string) => { + return SecurityClient.fetchCall( + '/api/v1/organization/' + organizationId + '/incidentContactOrg', + { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + email: email + }) + } + ).then(async (res) => { + if (res && res.status == 200) { + return res; + } else { + console.log('Failed to delete an incident contact'); + } + }); +}; + +export default deleteIncidentContact; diff --git a/frontend/pages/api/organization/deleteUserFromOrganization.js b/frontend/pages/api/organization/deleteUserFromOrganization.js deleted file mode 100644 index 66ea84793..000000000 --- a/frontend/pages/api/organization/deleteUserFromOrganization.js +++ /dev/null @@ -1,26 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This function removes a certain member from a certain organization - * @param {*} membershipId - * @returns - */ -const deleteUserFromOrganization = (membershipId) => { - return SecurityClient.fetchCall( - "/api/v1/membership-org/" + membershipId, - { - method: "DELETE", - headers: { - "Content-Type": "application/json", - }, - } - ).then(async (res) => { - if (res.status == 200) { - return res; - } else { - console.log("Failed to delete a user from an org"); - } - }); -}; - -export default deleteUserFromOrganization; diff --git a/frontend/pages/api/organization/deleteUserFromOrganization.ts b/frontend/pages/api/organization/deleteUserFromOrganization.ts new file mode 100644 index 000000000..988a09485 --- /dev/null +++ b/frontend/pages/api/organization/deleteUserFromOrganization.ts @@ -0,0 +1,23 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +/** + * This function removes a certain member from a certain organization + * @param {*} membershipId + * @returns + */ +const deleteUserFromOrganization = (membershipId: string) => { + return SecurityClient.fetchCall('/api/v1/membership-org/' + membershipId, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json' + } + }).then(async (res) => { + if (res && res.status == 200) { + return res; + } else { + console.log('Failed to delete a user from an org'); + } + }); +}; + +export default deleteUserFromOrganization; diff --git a/frontend/pages/api/organization/getIncidentContacts.js b/frontend/pages/api/organization/getIncidentContacts.js deleted file mode 100644 index 4f3f61a48..000000000 --- a/frontend/pages/api/organization/getIncidentContacts.js +++ /dev/null @@ -1,26 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This routes gets all the incident contacts of a certain organization - * @param {*} workspaceId - * @returns - */ -const getIncidentContacts = (organizationId) => { - return SecurityClient.fetchCall( - "/api/v1/organization/" + organizationId + "/incidentContactOrg", - { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - } - ).then(async (res) => { - if (res.status == 200) { - return (await res.json()).incidentContactsOrg; - } else { - console.log("Failed to get incident contacts"); - } - }); -}; - -export default getIncidentContacts; diff --git a/frontend/pages/api/organization/getIncidentContacts.ts b/frontend/pages/api/organization/getIncidentContacts.ts new file mode 100644 index 000000000..bb9c3613a --- /dev/null +++ b/frontend/pages/api/organization/getIncidentContacts.ts @@ -0,0 +1,26 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +/** + * This routes gets all the incident contacts of a certain organization + * @param {*} workspaceId + * @returns + */ +const getIncidentContacts = (organizationId: string) => { + return SecurityClient.fetchCall( + '/api/v1/organization/' + organizationId + '/incidentContactOrg', + { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + } + ).then(async (res) => { + if (res && res.status == 200) { + return (await res.json()).incidentContactsOrg; + } else { + console.log('Failed to get incident contacts'); + } + }); +}; + +export default getIncidentContacts; diff --git a/frontend/pages/api/organization/getOrgs.ts b/frontend/pages/api/organization/getOrgs.ts index cc655642f..5f01bc4d1 100644 --- a/frontend/pages/api/organization/getOrgs.ts +++ b/frontend/pages/api/organization/getOrgs.ts @@ -1,20 +1,20 @@ -import SecurityClient from "~/utilities/SecurityClient"; +import SecurityClient from '~/utilities/SecurityClient'; /** * This route lets us get the all the orgs of a certain user. * @returns */ const getOrganizations = () => { - return SecurityClient.fetchCall("/api/v1/organization", { - method: "GET", + return SecurityClient.fetchCall('/api/v1/organization', { + method: 'GET', headers: { - "Content-Type": "application/json", - }, + 'Content-Type': 'application/json' + } }).then(async (res) => { if (res?.status == 200) { return (await res.json()).organizations; } else { - console.log("Failed to get orgs of a user"); + console.log('Failed to get orgs of a user'); } }); }; diff --git a/frontend/pages/api/organization/renameOrg.js b/frontend/pages/api/organization/renameOrg.js deleted file mode 100644 index a9d80a3a5..000000000 --- a/frontend/pages/api/organization/renameOrg.js +++ /dev/null @@ -1,30 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This route lets us rename a certain org. - * @param {*} req - * @param {*} res - * @returns - */ -const renameOrg = (orgId, newOrgName) => { - return SecurityClient.fetchCall( - "/api/v1/organization/" + orgId + "/name", - { - method: "PATCH", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - name: newOrgName, - }), - } - ).then(async (res) => { - if (res.status == 200) { - return res; - } else { - console.log("Failed to rename an organization"); - } - }); -}; - -export default renameOrg; diff --git a/frontend/pages/api/organization/renameOrg.ts b/frontend/pages/api/organization/renameOrg.ts new file mode 100644 index 000000000..c5c0b4d65 --- /dev/null +++ b/frontend/pages/api/organization/renameOrg.ts @@ -0,0 +1,27 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +/** + * This route lets us rename a certain org. + * @param {*} req + * @param {*} res + * @returns + */ +const renameOrg = (orgId: string, newOrgName: string) => { + return SecurityClient.fetchCall('/api/v1/organization/' + orgId + '/name', { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + name: newOrgName + }) + }).then(async (res) => { + if (res && res.status == 200) { + return res; + } else { + console.log('Failed to rename an organization'); + } + }); +}; + +export default renameOrg; diff --git a/frontend/pages/api/serviceToken/addServiceToken.js b/frontend/pages/api/serviceToken/addServiceToken.ts similarity index 51% rename from frontend/pages/api/serviceToken/addServiceToken.js rename to frontend/pages/api/serviceToken/addServiceToken.ts index 4c6031db8..5dad69f69 100644 --- a/frontend/pages/api/serviceToken/addServiceToken.js +++ b/frontend/pages/api/serviceToken/addServiceToken.ts @@ -1,4 +1,14 @@ -import SecurityClient from "~/utilities/SecurityClient"; +import SecurityClient from '~/utilities/SecurityClient'; + +interface Props { + name: string; + workspaceId: string; + environment: string; + expiresIn: number; + publicKey: string; + encryptedKey: string; + nonce: string; +} /** * This route gets service tokens for a specific user in a project @@ -12,12 +22,12 @@ const addServiceToken = ({ expiresIn, publicKey, encryptedKey, - nonce, -}) => { - return SecurityClient.fetchCall("/api/v1/service-token/", { - method: "POST", + nonce +}: Props) => { + return SecurityClient.fetchCall('/api/v1/service-token/', { + method: 'POST', headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json' }, body: JSON.stringify({ name, @@ -26,13 +36,13 @@ const addServiceToken = ({ expiresIn, publicKey, encryptedKey, - nonce, - }), + nonce + }) }).then(async (res) => { - if (res.status == 200) { + if (res && res.status == 200) { return (await res.json()).token; } else { - console.log("Failed to add service tokens"); + console.log('Failed to add service tokens'); } }); }; diff --git a/frontend/pages/api/serviceToken/getServiceTokens.js b/frontend/pages/api/serviceToken/getServiceTokens.js deleted file mode 100644 index 79c6a7fc0..000000000 --- a/frontend/pages/api/serviceToken/getServiceTokens.js +++ /dev/null @@ -1,26 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This route gets service tokens for a specific user in a project - * @param {*} param0 - * @returns - */ -const getServiceTokens = ({ workspaceId }) => { - return SecurityClient.fetchCall( - "/api/v1/workspace/" + workspaceId + "/service-tokens", - { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - } - ).then(async (res) => { - if (res.status == 200) { - return (await res.json()).serviceTokens; - } else { - console.log("Failed to get service tokens"); - } - }); -}; - -export default getServiceTokens; diff --git a/frontend/pages/api/serviceToken/getServiceTokens.ts b/frontend/pages/api/serviceToken/getServiceTokens.ts new file mode 100644 index 000000000..d2577cc83 --- /dev/null +++ b/frontend/pages/api/serviceToken/getServiceTokens.ts @@ -0,0 +1,26 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +/** + * This route gets service tokens for a specific user in a project + * @param {*} param0 + * @returns + */ +const getServiceTokens = ({ workspaceId }: { workspaceId: string }) => { + return SecurityClient.fetchCall( + '/api/v1/workspace/' + workspaceId + '/service-tokens', + { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + } + ).then(async (res) => { + if (res && res.status == 200) { + return (await res.json()).serviceTokens; + } else { + console.log('Failed to get service tokens'); + } + }); +}; + +export default getServiceTokens; diff --git a/frontend/pages/api/user/getUser.ts b/frontend/pages/api/user/getUser.ts index 4bfebc5fe..7562b0f3b 100644 --- a/frontend/pages/api/user/getUser.ts +++ b/frontend/pages/api/user/getUser.ts @@ -1,19 +1,19 @@ -import SecurityClient from "~/utilities/SecurityClient"; +import SecurityClient from '~/utilities/SecurityClient'; /** * This route gets the information about a specific user. */ const getUser = () => { - return SecurityClient.fetchCall("/api/v1/user", { - method: "GET", + return SecurityClient.fetchCall('/api/v1/user', { + method: 'GET', headers: { - "Content-Type": "application/json", - }, + 'Content-Type': 'application/json' + } }).then(async (res) => { if (res?.status == 200) { return (await res.json()).user; } else { - console.log("Failed to get user info"); + console.log('Failed to get user info'); } }); }; diff --git a/frontend/pages/api/userActions/checkUserAction.js b/frontend/pages/api/userActions/checkUserAction.ts similarity index 51% rename from frontend/pages/api/userActions/checkUserAction.js rename to frontend/pages/api/userActions/checkUserAction.ts index dfe217856..9941ae85a 100644 --- a/frontend/pages/api/userActions/checkUserAction.js +++ b/frontend/pages/api/userActions/checkUserAction.ts @@ -1,4 +1,4 @@ -import SecurityClient from "~/utilities/SecurityClient"; +import SecurityClient from '~/utilities/SecurityClient'; /** * This route registers a certain action for a user @@ -6,24 +6,24 @@ import SecurityClient from "~/utilities/SecurityClient"; * @param {*} workspaceId * @returns */ -const checkUserAction = ({ action }) => { +const checkUserAction = ({ action }: { action: string }) => { return SecurityClient.fetchCall( - "/api/v1/user-action" + - "?" + + '/api/v1/user-action' + + '?' + new URLSearchParams({ - action, + action }), { - method: "GET", + method: 'GET', headers: { - "Content-Type": "application/json", - }, + 'Content-Type': 'application/json' + } } ).then(async (res) => { - if (res.status == 200) { + if (res && res.status == 200) { return (await res.json()).userAction; } else { - console.log("Failed to check a user action"); + console.log('Failed to check a user action'); } }); }; diff --git a/frontend/pages/api/userActions/registerUserAction.js b/frontend/pages/api/userActions/registerUserAction.js deleted file mode 100644 index 619886d89..000000000 --- a/frontend/pages/api/userActions/registerUserAction.js +++ /dev/null @@ -1,26 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This route registers a certain action for a user - * @param {*} action - * @returns - */ -const registerUserAction = ({ action }) => { - return SecurityClient.fetchCall("/api/v1/user-action", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - action, - }), - }).then(async (res) => { - if (res.status == 200) { - return res; - } else { - console.log("Failed to register a user action"); - } - }); -}; - -export default registerUserAction; diff --git a/frontend/pages/api/userActions/registerUserAction.ts b/frontend/pages/api/userActions/registerUserAction.ts new file mode 100644 index 000000000..d8f4d41e6 --- /dev/null +++ b/frontend/pages/api/userActions/registerUserAction.ts @@ -0,0 +1,26 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +/** + * This route registers a certain action for a user + * @param {*} action + * @returns + */ +const registerUserAction = ({ action }: { action: string }) => { + return SecurityClient.fetchCall('/api/v1/user-action', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + action + }) + }).then(async (res) => { + if (res && res.status == 200) { + return res; + } else { + console.log('Failed to register a user action'); + } + }); +}; + +export default registerUserAction; diff --git a/frontend/pages/api/workspace/addUserToWorkspace.js b/frontend/pages/api/workspace/addUserToWorkspace.js deleted file mode 100644 index 86e991389..000000000 --- a/frontend/pages/api/workspace/addUserToWorkspace.js +++ /dev/null @@ -1,30 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This function adds a user to a project - * @param {*} email - * @param {*} workspaceId - * @returns - */ -const addUserToWorkspace = (email, workspaceId) => { - return SecurityClient.fetchCall( - "/api/v1/workspace/" + workspaceId + "/invite-signup", - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - email: email, - }), - } - ).then(async (res) => { - if (res.status == 200) { - return await res.json(); - } else { - console.log("Failed to add a user to project"); - } - }); -}; - -export default addUserToWorkspace; diff --git a/frontend/pages/api/workspace/addUserToWorkspace.ts b/frontend/pages/api/workspace/addUserToWorkspace.ts new file mode 100644 index 000000000..e1efa51a1 --- /dev/null +++ b/frontend/pages/api/workspace/addUserToWorkspace.ts @@ -0,0 +1,30 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +/** + * This function adds a user to a project + * @param {*} email + * @param {*} workspaceId + * @returns + */ +const addUserToWorkspace = (email: string, workspaceId: string) => { + return SecurityClient.fetchCall( + '/api/v1/workspace/' + workspaceId + '/invite-signup', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + email: email + }) + } + ).then(async (res) => { + if (res && res.status == 200) { + return await res.json(); + } else { + console.log('Failed to add a user to project'); + } + }); +}; + +export default addUserToWorkspace; diff --git a/frontend/pages/api/workspace/changeUserRoleInWorkspace.js b/frontend/pages/api/workspace/changeUserRoleInWorkspace.js deleted file mode 100644 index a93e22181..000000000 --- a/frontend/pages/api/workspace/changeUserRoleInWorkspace.js +++ /dev/null @@ -1,30 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This function change the access of a user in a certain workspace - * @param {*} membershipId - * @param {*} role - * @returns - */ -const changeUserRoleInWorkspace = (membershipId, role) => { - return SecurityClient.fetchCall( - "/api/v1/membership/" + membershipId + "/change-role", - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - role: role, - }), - } - ).then(async (res) => { - if (res.status == 200) { - return res; - } else { - console.log("Failed to change the user role in a project"); - } - }); -}; - -export default changeUserRoleInWorkspace; diff --git a/frontend/pages/api/workspace/changeUserRoleInWorkspace.ts b/frontend/pages/api/workspace/changeUserRoleInWorkspace.ts new file mode 100644 index 000000000..9ac49440a --- /dev/null +++ b/frontend/pages/api/workspace/changeUserRoleInWorkspace.ts @@ -0,0 +1,30 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +/** + * This function change the access of a user in a certain workspace + * @param {*} membershipId + * @param {*} role + * @returns + */ +const changeUserRoleInWorkspace = (membershipId: string, role: string) => { + return SecurityClient.fetchCall( + '/api/v1/membership/' + membershipId + '/change-role', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + role: role + }) + } + ).then(async (res) => { + if (res && res.status == 200) { + return res; + } else { + console.log('Failed to change the user role in a project'); + } + }); +}; + +export default changeUserRoleInWorkspace; diff --git a/frontend/pages/api/workspace/createWorkspace.ts b/frontend/pages/api/workspace/createWorkspace.ts index 0cedb0a57..877ef7567 100644 --- a/frontend/pages/api/workspace/createWorkspace.ts +++ b/frontend/pages/api/workspace/createWorkspace.ts @@ -1,4 +1,4 @@ -import SecurityClient from "~/utilities/SecurityClient"; +import SecurityClient from '~/utilities/SecurityClient'; /** * This route creates a new workspace for a user within a certain organization. @@ -6,21 +6,27 @@ import SecurityClient from "~/utilities/SecurityClient"; * @param {string} organizationId - org ID * @returns */ -const createWorkspace = ( { workspaceName, organizationId }: { workspaceName: string; organizationId: string; }) => { - return SecurityClient.fetchCall("/api/v1/workspace", { - method: "POST", +const createWorkspace = ({ + workspaceName, + organizationId +}: { + workspaceName: string; + organizationId: string; +}) => { + return SecurityClient.fetchCall('/api/v1/workspace', { + method: 'POST', headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json' }, body: JSON.stringify({ workspaceName: workspaceName, - organizationId: organizationId, - }), + organizationId: organizationId + }) }).then(async (res) => { if (res?.status == 200) { return (await res.json()).workspace; } else { - console.log("Failed to create a project"); + console.log('Failed to create a project'); } }); }; diff --git a/frontend/pages/api/workspace/deleteUserFromWorkspace.js b/frontend/pages/api/workspace/deleteUserFromWorkspace.js deleted file mode 100644 index bad8beced..000000000 --- a/frontend/pages/api/workspace/deleteUserFromWorkspace.js +++ /dev/null @@ -1,23 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This function removes a certain member from a certain workspace - * @param {*} membershipId - * @returns - */ -const deleteUserFromWorkspace = (membershipId) => { - return SecurityClient.fetchCall("/api/v1/membership/" + membershipId, { - method: "DELETE", - headers: { - "Content-Type": "application/json", - }, - }).then(async (res) => { - if (res.status == 200) { - return res; - } else { - console.log("Failed to delete a user from a project"); - } - }); -}; - -export default deleteUserFromWorkspace; diff --git a/frontend/pages/api/workspace/deleteUserFromWorkspace.ts b/frontend/pages/api/workspace/deleteUserFromWorkspace.ts new file mode 100644 index 000000000..ea02f96d4 --- /dev/null +++ b/frontend/pages/api/workspace/deleteUserFromWorkspace.ts @@ -0,0 +1,23 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +/** + * This function removes a certain member from a certain workspace + * @param {*} membershipId + * @returns + */ +const deleteUserFromWorkspace = (membershipId: string) => { + return SecurityClient.fetchCall('/api/v1/membership/' + membershipId, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json' + } + }).then(async (res) => { + if (res && res.status == 200) { + return res; + } else { + console.log('Failed to delete a user from a project'); + } + }); +}; + +export default deleteUserFromWorkspace; diff --git a/frontend/pages/api/workspace/deleteWorkspace.js b/frontend/pages/api/workspace/deleteWorkspace.js deleted file mode 100644 index 33d34844d..000000000 --- a/frontend/pages/api/workspace/deleteWorkspace.js +++ /dev/null @@ -1,23 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This route deletes a specified workspace. - * @param {*} workspaceId - * @returns - */ -const deleteWorkspace = (workspaceId) => { - return SecurityClient.fetchCall("/api/v1/workspace/" + workspaceId, { - method: "DELETE", - headers: { - "Content-Type": "application/json", - }, - }).then(async (res) => { - if (res.status == 200) { - return res; - } else { - console.log("Failed to delete a project"); - } - }); -}; - -export default deleteWorkspace; diff --git a/frontend/pages/api/workspace/deleteWorkspace.ts b/frontend/pages/api/workspace/deleteWorkspace.ts new file mode 100644 index 000000000..2bdb64cd3 --- /dev/null +++ b/frontend/pages/api/workspace/deleteWorkspace.ts @@ -0,0 +1,23 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +/** + * This route deletes a specified workspace. + * @param {*} workspaceId + * @returns + */ +const deleteWorkspace = (workspaceId: string) => { + return SecurityClient.fetchCall('/api/v1/workspace/' + workspaceId, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json' + } + }).then(async (res) => { + if (res && res.status == 200) { + return res; + } else { + console.log('Failed to delete a project'); + } + }); +}; + +export default deleteWorkspace; diff --git a/frontend/pages/api/workspace/getLatestFileKey.ts b/frontend/pages/api/workspace/getLatestFileKey.ts index 86ecb7456..1ecd2035d 100644 --- a/frontend/pages/api/workspace/getLatestFileKey.ts +++ b/frontend/pages/api/workspace/getLatestFileKey.ts @@ -1,24 +1,21 @@ -import SecurityClient from "~/utilities/SecurityClient"; +import SecurityClient from '~/utilities/SecurityClient'; /** * Get the latest key pairs from a certain workspace * @param {string} workspaceId * @returns */ -const getLatestFileKey = ({ workspaceId } : { workspaceId: string; }) => { - return SecurityClient.fetchCall( - "/api/v1/key/" + workspaceId + "/latest", - { - method: "GET", - headers: { - "Content-Type": "application/json", - }, +const getLatestFileKey = ({ workspaceId }: { workspaceId: string }) => { + return SecurityClient.fetchCall('/api/v1/key/' + workspaceId + '/latest', { + method: 'GET', + headers: { + 'Content-Type': 'application/json' } - ).then(async (res) => { + }).then(async (res) => { if (res?.status == 200) { return await res.json(); } else { - console.log("Failed to get the latest key pairs for a certain project"); + console.log('Failed to get the latest key pairs for a certain project'); } }); }; diff --git a/frontend/pages/api/workspace/getProjectInfo.ts b/frontend/pages/api/workspace/getProjectInfo.ts index c6eef9dce..a1ab0438c 100644 --- a/frontend/pages/api/workspace/getProjectInfo.ts +++ b/frontend/pages/api/workspace/getProjectInfo.ts @@ -1,24 +1,21 @@ -import SecurityClient from "~/utilities/SecurityClient"; +import SecurityClient from '~/utilities/SecurityClient'; /** * This route lets us get the information of a certain project. * @param {*} projectId - project ID (we renamed workspaces to projects in the app) * @returns */ -const getProjectInfo = ({ projectId }: { projectId: string; }) => { - return SecurityClient.fetchCall( - "/api/v1/workspace/" + projectId, - { - method: "GET", - headers: { - "Content-Type": "application/json", - }, +const getProjectInfo = ({ projectId }: { projectId: string }) => { + return SecurityClient.fetchCall('/api/v1/workspace/' + projectId, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' } - ).then(async (res) => { + }).then(async (res) => { if (res?.status == 200) { return (await res.json()).workspace; } else { - console.log("Failed to get project info"); + console.log('Failed to get project info'); } }); }; diff --git a/frontend/pages/api/workspace/getWorkspaceUsers.ts b/frontend/pages/api/workspace/getWorkspaceUsers.ts index 9805c2695..c4f00eb0d 100644 --- a/frontend/pages/api/workspace/getWorkspaceUsers.ts +++ b/frontend/pages/api/workspace/getWorkspaceUsers.ts @@ -1,24 +1,24 @@ -import SecurityClient from "~/utilities/SecurityClient"; +import SecurityClient from '~/utilities/SecurityClient'; /** * This route lets us get all the users in the workspace. * @param {string} workspaceId - workspace ID * @returns */ -const getWorkspaceUsers = ({ workspaceId }: { workspaceId: string; }) => { +const getWorkspaceUsers = ({ workspaceId }: { workspaceId: string }) => { return SecurityClient.fetchCall( - "/api/v1/workspace/" + workspaceId + "/users", + '/api/v1/workspace/' + workspaceId + '/users', { - method: "GET", + method: 'GET', headers: { - "Content-Type": "application/json", - }, + 'Content-Type': 'application/json' + } } ).then(async (res) => { if (res?.status == 200) { return (await res.json()).users; } else { - console.log("Failed to get Project Users"); + console.log('Failed to get Project Users'); } }); }; diff --git a/frontend/pages/api/workspace/getWorkspaces.ts b/frontend/pages/api/workspace/getWorkspaces.ts index 33292b6ee..1bbb42c7a 100644 --- a/frontend/pages/api/workspace/getWorkspaces.ts +++ b/frontend/pages/api/workspace/getWorkspaces.ts @@ -1,30 +1,29 @@ -import SecurityClient from "~/utilities/SecurityClient"; +import SecurityClient from '~/utilities/SecurityClient'; -interface Workspaces { +interface Workspace { __v: number; _id: string; name: string; organization: string; } -[]; /** * This route lets us get the workspaces of a certain user * @returns */ const getWorkspaces = () => { - return SecurityClient.fetchCall("/api/v1/workspace", { - method: "GET", + return SecurityClient.fetchCall('/api/v1/workspace', { + method: 'GET', headers: { - "Content-Type": "application/json", - }, + 'Content-Type': 'application/json' + } }).then(async (res) => { if (res?.status == 200) { - const data = (await res.json()) as unknown as { workspaces: Workspaces }; + const data = (await res.json()) as unknown as { workspaces: Workspace[] }; return data.workspaces; } - throw new Error("Failed to get projects"); + throw new Error('Failed to get projects'); }); }; diff --git a/frontend/pages/api/workspace/renameWorkspace.js b/frontend/pages/api/workspace/renameWorkspace.js deleted file mode 100644 index 6dd297f82..000000000 --- a/frontend/pages/api/workspace/renameWorkspace.js +++ /dev/null @@ -1,30 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This route lets us rename a certain workspace. - * @param {*} req - * @param {*} res - * @returns - */ -const renameWorkspace = (workspaceId, newWorkspaceName) => { - return SecurityClient.fetchCall( - "/api/v1/workspace/" + workspaceId + "/name", - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - name: newWorkspaceName, - }), - } - ).then(async (res) => { - if (res.status == 200) { - return res; - } else { - console.log("Failed to rename a project"); - } - }); -}; - -export default renameWorkspace; diff --git a/frontend/pages/api/workspace/renameWorkspace.ts b/frontend/pages/api/workspace/renameWorkspace.ts new file mode 100644 index 000000000..a25d2068c --- /dev/null +++ b/frontend/pages/api/workspace/renameWorkspace.ts @@ -0,0 +1,30 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +/** + * This route lets us rename a certain workspace. + * @param {*} req + * @param {*} res + * @returns + */ +const renameWorkspace = (workspaceId: string, newWorkspaceName: string) => { + return SecurityClient.fetchCall( + '/api/v1/workspace/' + workspaceId + '/name', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + name: newWorkspaceName + }) + } + ).then(async (res) => { + if (res && res.status == 200) { + return res; + } else { + console.log('Failed to rename a project'); + } + }); +}; + +export default renameWorkspace; diff --git a/frontend/pages/api/workspace/uploadKeys.js b/frontend/pages/api/workspace/uploadKeys.js deleted file mode 100644 index 37b384f30..000000000 --- a/frontend/pages/api/workspace/uploadKeys.js +++ /dev/null @@ -1,33 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This route uplods the keys in an encrypted format. - * @param {*} workspaceId - * @param {*} userId - * @param {*} encryptedKey - * @param {*} nonce - * @returns - */ -const uploadKeys = (workspaceId, userId, encryptedKey, nonce) => { - return SecurityClient.fetchCall("/api/v1/key/" + workspaceId, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - key: { - userId: userId, - encryptedKey: encryptedKey, - nonce: nonce, - }, - }), - }).then(async (res) => { - if (res.status == 200) { - return res; - } else { - console.log("Failed to upload keys for a new user"); - } - }); -}; - -export default uploadKeys; diff --git a/frontend/pages/api/workspace/uploadKeys.ts b/frontend/pages/api/workspace/uploadKeys.ts new file mode 100644 index 000000000..2f791735d --- /dev/null +++ b/frontend/pages/api/workspace/uploadKeys.ts @@ -0,0 +1,38 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +/** + * This route uplods the keys in an encrypted format. + * @param {*} workspaceId + * @param {*} userId + * @param {*} encryptedKey + * @param {*} nonce + * @returns + */ +const uploadKeys = ( + workspaceId: string, + userId: string, + encryptedKey: string, + nonce: string +) => { + return SecurityClient.fetchCall('/api/v1/key/' + workspaceId, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + key: { + userId: userId, + encryptedKey: encryptedKey, + nonce: nonce + } + }) + }).then(async (res) => { + if (res && res.status == 200) { + return res; + } else { + console.log('Failed to upload keys for a new user'); + } + }); +}; + +export default uploadKeys; diff --git a/frontend/pages/dashboard/[id].js b/frontend/pages/dashboard/[id].js index 0696c2ac5..a4147ef86 100644 --- a/frontend/pages/dashboard/[id].js +++ b/frontend/pages/dashboard/[id].js @@ -19,35 +19,35 @@ import { faPerson, faPlus, faShuffle, - faX, -} from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Menu, Transition } from "@headlessui/react"; + faX +} from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Menu, Transition } from '@headlessui/react'; -import Button from "~/components/basic/buttons/Button"; -import ListBox from "~/components/basic/Listbox"; -import BottonRightPopup from "~/components/basic/popups/BottomRightPopup"; -import { useNotificationContext } from "~/components/context/Notifications/NotificationProvider"; -import DashboardInputField from "~/components/dashboard/DashboardInputField"; -import DropZone from "~/components/dashboard/DropZone"; -import NavHeader from "~/components/navigation/NavHeader"; -import getSecretsForProject from "~/components/utilities/secrets/getSecretsForProject"; -import pushKeys from "~/components/utilities/secrets/pushKeys"; -import pushKeysIntegration from "~/components/utilities/secrets/pushKeysIntegration"; -import guidGenerator from "~/utilities/randomId"; +import Button from '~/components/basic/buttons/Button'; +import ListBox from '~/components/basic/Listbox'; +import BottonRightPopup from '~/components/basic/popups/BottomRightPopup'; +import { useNotificationContext } from '~/components/context/Notifications/NotificationProvider'; +import DashboardInputField from '~/components/dashboard/DashboardInputField'; +import DropZone from '~/components/dashboard/DropZone'; +import NavHeader from '~/components/navigation/NavHeader'; +import getSecretsForProject from '~/components/utilities/secrets/getSecretsForProject'; +import pushKeys from '~/components/utilities/secrets/pushKeys'; +import pushKeysIntegration from '~/components/utilities/secrets/pushKeysIntegration'; +import guidGenerator from '~/utilities/randomId'; import { getTranslatedServerSideProps } from "~/utilities/withTranslateProps"; -import { envMapping } from "../../public/data/frequentConstants"; -import getWorkspaceIntegrations from "../api/integrations/getWorkspaceIntegrations"; -import getUser from "../api/user/getUser"; -import checkUserAction from "../api/userActions/checkUserAction"; -import registerUserAction from "../api/userActions/registerUserAction"; -import getWorkspaces from "../api/workspace/getWorkspaces"; + +import { envMapping } from '../../public/data/frequentConstants'; +import getUser from '../api/user/getUser'; +import checkUserAction from '../api/userActions/checkUserAction'; +import registerUserAction from '../api/userActions/registerUserAction'; +import getWorkspaces from '../api/workspace/getWorkspaces'; /** * This component represent a single row for an environemnt variable on the dashboard * @param {object} obj - * @param {String[]} obj.keyPair - data related to the environment variable (index, key, value, public/private) + * @param {String[]} obj.keyPair - data related to the environment variable (id, pos, key, value, public/private) * @param {function} obj.deleteRow - a function to delete a certain keyPair * @param {function} obj.modifyKey - modify the key of a certain environment variable * @param {function} obj.modifyValue - modify the value of a certain environment variable @@ -63,7 +63,7 @@ const KeyPair = ({ modifyValue, modifyVisibility, isBlurred, - duplicates, + duplicates }) => { const [randomStringLength, setRandomStringLength] = useState(32); const { t } = useTranslation(); @@ -76,8 +76,8 @@ const KeyPair = ({
@@ -87,8 +87,8 @@ const KeyPair = ({
@@ -117,15 +117,15 @@ const KeyPair = ({
modifyVisibility( - keyPair[4] == "personal" ? "shared" : "personal", - keyPair[1] + keyPair.type == 'personal' ? 'shared' : 'personal', + keyPair.pos ) } className="relative flex justify-start items-center cursor-pointer select-none py-2 px-2 rounded-md text-gray-400 hover:bg-white/10 duration-200 hover:text-gray-200 w-full" >
{keyPair[4] == "personal" @@ -143,8 +143,8 @@ const KeyPair = ({ modifyValue( [...Array(randomStringLength)] .map(() => Math.floor(Math.random() * 16).toString(16)) - .join(""), - keyPair[1] + .join(''), + keyPair.pos ); } }} @@ -152,7 +152,7 @@ const KeyPair = ({ >

Generate Random Hex

@@ -195,7 +195,7 @@ const KeyPair = ({
8 ? "h-3/4" : "h-min" + data?.length > 8 ? 'h-3/4' : 'h-min' }`} >
@@ -708,19 +711,14 @@ export default function Dashboard() { {data .filter( (keyPair) => - keyPair[2] + keyPair.key .toLowerCase() .includes(searchKeys.toLowerCase()) && - keyPair[4] == "shared" + keyPair.type == 'shared' ) - .sort((a, b) => - sortMethod == "alphabetical" - ? a[2].localeCompare(b[2]) - : b[2].localeCompare(a[2]) - ) - ?.map((keyPair, index) => ( + ?.map((keyPair) => ( item[2]) + ?.map((item) => item.key) .filter( (item, index) => index !== - data?.map((item) => item[2]).indexOf(item) + data?.map((item) => item.key).indexOf(item) )} /> ))} @@ -772,10 +770,10 @@ export default function Dashboard() { /> )} {fileState.message == - "Failed membership validation for workspace" && ( + 'Failed membership validation for workspace' && (

You are not authorized to view this project.

)} - {fileState.message == "Access needed to pull the latest file" || + {fileState.message == 'Access needed to pull the latest file' || (!isKeyAvailable && ( <> + + Request a New Invite + + +
+

Oops.

+

Your email was not verified.

+

Please try again.

+

+ Note: If it still {"doesn't work"}, please reach out to us at + support@infisical.com +

+
+
+ ); +} diff --git a/frontend/pages/github.js b/frontend/pages/github.js new file mode 100644 index 000000000..af2b2b087 --- /dev/null +++ b/frontend/pages/github.js @@ -0,0 +1,37 @@ +import React, { useEffect } from "react"; +import Head from "next/head"; +import { useRouter } from "next/router"; +const queryString = require("query-string"); +import AuthorizeIntegration from "./api/integrations/authorizeIntegration"; + +export default function Github() { + const router = useRouter(); + const parsedUrl = queryString.parse(router.asPath.split("?")[1]); + const code = parsedUrl.code; + const state = parsedUrl.state; + + /** + * Here we forward to the default workspace if a user opens this url + */ + // eslint-disable-next-line react-hooks/exhaustive-deps + useEffect(async () => { + try { + if (state === localStorage.getItem('latestCSRFToken')) { + localStorage.removeItem('latestCSRFToken'); + await AuthorizeIntegration({ + workspaceId: localStorage.getItem('projectData.id'), + code, + integration: "github", + }); + router.push("/integrations/" + localStorage.getItem("projectData.id")); + } + } catch (error) { + console.error('Github integration error: ', error); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return
; +} + +Github.requireAuth = true; diff --git a/frontend/pages/heroku.js b/frontend/pages/heroku.js index 82947b1b9..088c96500 100644 --- a/frontend/pages/heroku.js +++ b/frontend/pages/heroku.js @@ -16,16 +16,17 @@ export default function Heroku() { // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(async () => { try { - if (state == localStorage.getItem("latestCSRFToken")) { + if (state === localStorage.getItem('latestCSRFToken')) { + localStorage.removeItem('latestCSRFToken'); await AuthorizeIntegration({ - workspaceId: localStorage.getItem("projectData.id"), + workspaceId: localStorage.getItem('projectData.id'), code, integration: "heroku", }); router.push("/integrations/" + localStorage.getItem("projectData.id")); } } catch (error) { - console.log("Error - Not logged in yet"); + console.error('Heroku integration error: ', error); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/frontend/pages/home/[id].tsx b/frontend/pages/home/[id].tsx index 21f15fcc3..d433600d3 100644 --- a/frontend/pages/home/[id].tsx +++ b/frontend/pages/home/[id].tsx @@ -1,92 +1,134 @@ -import React, { useEffect, useState } from "react"; -import Link from "next/link"; -import { useRouter } from "next/router"; -import { IconProp } from "@fortawesome/fontawesome-svg-core"; -import { faSlack } from "@fortawesome/free-brands-svg-icons"; -import { faCheckCircle, faHandPeace, faNetworkWired, faPlug, faPlus, faStar, faUserPlus } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import React, { useEffect, useState } from 'react'; +import Link from 'next/link'; +import { useRouter } from 'next/router'; +import { IconProp } from '@fortawesome/fontawesome-svg-core'; +import { faSlack } from '@fortawesome/free-brands-svg-icons'; +import { + faCheckCircle, + faHandPeace, + faNetworkWired, + faPlug, + faPlus, + faStar, + faUserPlus +} from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import getOrganizationUsers from "../api/organization/GetOrgUsers"; -import checkUserAction from "../api/userActions/checkUserAction"; -import registerUserAction from "../api/userActions/registerUserAction"; +import onboardingCheck from '~/components/utilities/checks/OnboardingCheck'; -type ItemProps = { - text: string; - subText: string; - complete: boolean; - icon: IconProp; - time: string; +import registerUserAction from '../api/userActions/registerUserAction'; + +type ItemProps = { + text: string; + subText: string; + complete: boolean; + icon: IconProp; + time: string; userAction?: string; link?: string; }; -const learningItem = ({ text, subText, complete, icon, time, userAction, link }: ItemProps): JSX.Element => { +const learningItem = ({ + text, + subText, + complete, + icon, + time, + userAction, + link +}: ItemProps): JSX.Element => { if (link) { return ( - -
{ - if (userAction) { - await registerUserAction({ - action: userAction - }) + +
{ + if (userAction && userAction != 'first_time_secrets_pushed') { + await registerUserAction({ + action: userAction + }); } }} - className="relative bg-bunker-700 hover:bg-bunker-500 shadow-xl duration-200 rounded-md border border-dashed border-bunker-400 pl-2 pr-6 py-2 h-[5.5rem] w-full flex items-center justify-between overflow-hidden my-1.5 cursor-pointer"> + className="relative bg-bunker-700 hover:bg-bunker-500 shadow-xl duration-200 rounded-md border border-dashed border-bunker-400 pl-2 pr-6 py-2 h-[5.5rem] w-full flex items-center justify-between overflow-hidden my-1.5 cursor-pointer" + >
- {complete && -
- -
} + {complete && ( +
+ +
+ )}
{text}
{subText}
-
- {complete ? "Complete!" : "About " + time} +
+ {complete ? 'Complete!' : 'About ' + time}
- {complete &&
} + {complete && ( +
+ )}
); } else { return ( -
{ - if (userAction) { - await registerUserAction({ - action: userAction - }) - } - }} - className="relative bg-bunker-700 hover:bg-bunker-500 shadow-xl duration-200 rounded-md border border-dashed border-bunker-400 pl-2 pr-6 py-2 h-[5.5rem] w-full flex items-center justify-between overflow-hidden my-1.5 cursor-pointer"> +
{ + if (userAction) { + await registerUserAction({ + action: userAction + }); + } + }} + className="relative bg-bunker-700 hover:bg-bunker-500 shadow-xl duration-200 rounded-md border border-dashed border-bunker-400 pl-2 pr-6 py-2 h-[5.5rem] w-full flex items-center justify-between overflow-hidden my-1.5 cursor-pointer" + >
- {complete && -
- -
} + {complete && ( +
+ +
+ )}
{text}
{subText}
-
- {complete ? "Complete!" : "About " + time} +
+ {complete ? 'Complete!' : 'About ' + time}
- {complete &&
} + {complete && ( +
+ )}
); } -} +}; /** - * This tab is called Home because in the future it will include some company news, - * updates, roadmap, relavant blogs, etc. Currently it only has the setup instruction + * This tab is called Home because in the future it will include some company news, + * updates, roadmap, relavant blogs, etc. Currently it only has the setup instruction * for the new users */ export default function Home() { @@ -94,46 +136,90 @@ export default function Home() { const [hasUserClickedSlack, setHasUserClickedSlack] = useState(false); const [hasUserClickedIntro, setHasUserClickedIntro] = useState(false); const [hasUserStarred, setHasUserStarred] = useState(false); + const [hasUserPushedSecrets, setHasUserPushedSecrets] = useState(false); const [usersInOrg, setUsersInOrg] = useState(false); useEffect(() => { - const checkUserActionsFunction = async () => { - const userActionSlack = await checkUserAction({ - action: "slack_cta_clicked", - }); - setHasUserClickedSlack(userActionSlack ? true : false); - - const userActionIntro = await checkUserAction({ - action: "intro_cta_clicked", - }); - setHasUserClickedIntro(userActionIntro ? true : false); - - const userActionStar = await checkUserAction({ - action: "star_cta_clicked", - }); - setHasUserStarred(userActionStar ? true : false); - - const orgId = localStorage.getItem("orgData.id"); - const orgUsers = await getOrganizationUsers({ - orgId: orgId ? orgId : "", - }); - setUsersInOrg(orgUsers.length > 1) - }; - checkUserActionsFunction(); + onboardingCheck({ + setHasUserClickedIntro, + setHasUserClickedSlack, + setHasUserPushedSecrets, + setHasUserStarred, + setUsersInOrg + }); }, []); return (
-
Your quick start guide
-
Click on the items below and follow the instructions.
- {learningItem({ text: "Get to know Infisical", subText: "", complete: hasUserClickedIntro, icon: faHandPeace, time: "3 min", userAction: "intro_cta_clicked", link: "https://www.youtube.com/watch?v=JS3OKYU2078" })} - {learningItem({ text: "Add your secrets", subText: "Click to see example secrets, and add your own.", complete: false, icon: faPlus, time: "2 min", userAction: "first_time_secrets_pushed", link: "/dashboard/" + router.query.id })} - {learningItem({ text: "Inject secrets locally", subText: "Replace .env files with a more secure an efficient alternative.", complete: false, icon: faNetworkWired, time: "8 min", link: "https://infisical.com/docs/getting-started/quickstart" })} - {learningItem({ text: "Integrate Infisical with your infrastructure", subText: "Only a few integrations are currently available. Many more coming soon!", complete: false, icon: faPlug, time: "15 min", link: "https://infisical.com/docs/integrations/overview" })} - {learningItem({ text: "Invite your teammates", subText: "", complete: usersInOrg, icon: faUserPlus, time: "2 min", link: "/settings/org/" + router.query.id + "?invite" })} - {learningItem({ text: "Join Infisical Slack", subText: "Have any questions? Ask us!", complete: hasUserClickedSlack, icon: faSlack, time: "1 min", userAction: "slack_cta_clicked", link: "https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g" })} - {learningItem({ text: "Star Infisical on GitHub", subText: "Like what we're doing? You know what to do! :)", complete: hasUserStarred, icon: faStar, time: "1 min", userAction: "star_cta_clicked", link: "https://github.com/Infisical/infisical" })} +
+ Your quick start guide +
+
+ Click on the items below and follow the instructions. +
+ {learningItem({ + text: 'Get to know Infisical', + subText: '', + complete: hasUserClickedIntro, + icon: faHandPeace, + time: '3 min', + userAction: 'intro_cta_clicked', + link: 'https://www.youtube.com/watch?v=JS3OKYU2078' + })} + {learningItem({ + text: 'Add your secrets', + subText: 'Click to see example secrets, and add your own.', + complete: hasUserPushedSecrets, + icon: faPlus, + time: '2 min', + userAction: 'first_time_secrets_pushed', + link: '/dashboard/' + router.query.id + })} + {learningItem({ + text: 'Inject secrets locally', + subText: + 'Replace .env files with a more secure an efficient alternative.', + complete: false, + icon: faNetworkWired, + time: '8 min', + link: 'https://infisical.com/docs/getting-started/quickstart' + })} + {learningItem({ + text: 'Integrate Infisical with your infrastructure', + subText: + 'Only a few integrations are currently available. Many more coming soon!', + complete: false, + icon: faPlug, + time: '15 min', + link: 'https://infisical.com/docs/integrations/overview' + })} + {learningItem({ + text: 'Invite your teammates', + subText: '', + complete: usersInOrg, + icon: faUserPlus, + time: '2 min', + link: '/settings/org/' + router.query.id + '?invite' + })} + {learningItem({ + text: 'Join Infisical Slack', + subText: 'Have any questions? Ask us!', + complete: hasUserClickedSlack, + icon: faSlack, + time: '1 min', + userAction: 'slack_cta_clicked', + link: 'https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g' + })} + {learningItem({ + text: 'Star Infisical on GitHub', + subText: "Like what we're doing? You know what to do! :)", + complete: hasUserStarred, + icon: faStar, + time: '1 min', + userAction: 'star_cta_clicked', + link: 'https://github.com/Infisical/infisical' + })}
); diff --git a/frontend/pages/integrations/[id].js b/frontend/pages/integrations/[id].js index 2aa3d922c..161da2ca3 100644 --- a/frontend/pages/integrations/[id].js +++ b/frontend/pages/integrations/[id].js @@ -11,390 +11,228 @@ import { } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import Button from "~/components/basic/buttons/Button"; -import ListBox from "~/components/basic/Listbox"; +import ActivateBotDialog from "~/components/basic/dialog/ActivateBotDialog"; +import CloudIntegrationSection from "~/components/integrations/CloudIntegrationSection"; +import FrameworkIntegrationSection from "~/components/integrations/FrameworkIntegrationSection"; +import IntegrationSection from "~/components/integrations/IntegrationSection"; import NavHeader from "~/components/navigation/NavHeader"; -import getSecretsForProject from "~/components/utilities/secrets/getSecretsForProject"; -import pushKeysIntegration from "~/components/utilities/secrets/pushKeysIntegration"; -import guidGenerator from "~/utilities/randomId"; import { getTranslatedServerSideProps } from "~/utilities/withTranslateProps"; -import { - envMapping, - frameworks, - reverseEnvMapping, -} from "../../public/data/frequentConstants"; -import deleteIntegration from "../api/integrations/DeleteIntegration"; -import deleteIntegrationAuth from "../api/integrations/DeleteIntegrationAuth"; -import getIntegrationApps from "../api/integrations/GetIntegrationApps"; -import getIntegrations from "../api/integrations/GetIntegrations"; +import frameworkIntegrationOptions from "../../public/json/frameworkIntegrations.json"; +import getBot from "../api/bot/getBot"; +import setBotActiveStatus from "../api/bot/setBotActiveStatus"; +import getIntegrationOptions from "../api/integrations/GetIntegrationOptions"; import getWorkspaceAuthorizations from "../api/integrations/getWorkspaceAuthorizations"; import getWorkspaceIntegrations from "../api/integrations/getWorkspaceIntegrations"; -import startIntegration from "../api/integrations/StartIntegration"; - +import getLatestFileKey from "../api/workspace/getLatestFileKey"; +const { + decryptAssymmetric, + encryptAssymmetric +} = require('../../components/utilities/cryptography/crypto'); const crypto = require("crypto"); -const Integration = ({ projectIntegration }) => { - const [integrationEnvironment, setIntegrationEnvironment] = useState( - reverseEnvMapping[projectIntegration.environment] - ); - const [fileState, setFileState] = useState([]); - const [data, setData] = useState(); - const [isKeyAvailable, setIsKeyAvailable] = useState(true); - const router = useRouter(); - const [apps, setApps] = useState([]); - const [integrationApp, setIntegrationApp] = useState( - projectIntegration.app ? projectIntegration.app : apps[0] - ); - - const { t } = useTranslation(); - - useEffect(async () => { - const tempHerokuApps = await getIntegrationApps({ - integrationAuthId: projectIntegration.integrationAuth, - }); - const tempHerokuAppNames = tempHerokuApps.map((app) => app.name); - setApps(tempHerokuAppNames); - setIntegrationApp( - projectIntegration.app ? projectIntegration.app : tempHerokuAppNames[0] - ); - }, []); - - return ( -
-
-
-
-
- ENVIRONMENT -
- -
- -
-
- INTEGRATION -
-
- {projectIntegration.integration.charAt(0).toUpperCase() + - projectIntegration.integration.slice(1)} -
-
-
-
- HEROKU APP -
- -
-
-
- {projectIntegration.isActive ? ( -
- -
In Sync
-
- ) : ( -
-
-
-
- ); -}; - export default function Integrations() { - const [integrations, setIntegrations] = useState(); - const [projectIntegrations, setProjectIntegrations] = useState(); - const [authorizations, setAuthorizations] = useState(); + const [cloudIntegrationOptions, setCloudIntegrationOptions] = useState([]); + const [integrationAuths, setIntegrationAuths] = useState([]); + const [integrations, setIntegrations] = useState([]); + const [bot, setBot] = useState(null); + const [isActivateBotDialogOpen, setIsActivateBotDialogOpen] = useState(false); + // const [isIntegrationAccessTokenDialogOpen, setIntegrationAccessTokenDialogOpen] = useState(true); + const [selectedIntegrationOption, setSelectedIntegrationOption] = useState(null); + const router = useRouter(); - const [csrfToken, setCsrfToken] = useState(""); const { t } = useTranslation(); useEffect(async () => { - const tempCSRFToken = crypto.randomBytes(16).toString("hex"); - setCsrfToken(tempCSRFToken); - localStorage.setItem("latestCSRFToken", tempCSRFToken); - - let projectAuthorizations = await getWorkspaceAuthorizations({ - workspaceId: router.query.id, - }); - setAuthorizations(projectAuthorizations); - - const projectIntegrations = await getWorkspaceIntegrations({ - workspaceId: router.query.id, - }); - setProjectIntegrations(projectIntegrations); - try { - const integrationsData = await getIntegrations(); - setIntegrations(integrationsData); - } catch (error) { - console.log("Error", error); + // get cloud integration options + setCloudIntegrationOptions( + await getIntegrationOptions() + ); + + // get project integration authorizations + setIntegrationAuths( + await getWorkspaceAuthorizations({ + workspaceId: router.query.id, + }) + ); + + // get project integrations + setIntegrations( + await getWorkspaceIntegrations({ + workspaceId: router.query.id, + }) + ); + + // get project bot + setBot( + await getBot({ + workspaceId: router.query.id + } + )); + + } catch (err) { + console.log(err); } }, []); - return integrations ? ( + /** + * Activate bot for project by performing the following steps: + * 1. Get the (encrypted) project key + * 2. Decrypt project key with user's private key + * 3. Encrypt project key with bot's public key + * 4. Send encrypted project key to backend and set bot status to active + */ + const handleBotActivate = async () => { + let botKey; + try { + + if (bot) { + // case: there is a bot + const key = await getLatestFileKey({ workspaceId: router.query.id }); + const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY'); + + const WORKSPACE_KEY = decryptAssymmetric({ + ciphertext: key.latestKey.encryptedKey, + nonce: key.latestKey.nonce, + publicKey: key.latestKey.sender.publicKey, + privateKey: PRIVATE_KEY + }); + + const { ciphertext, nonce } = encryptAssymmetric({ + plaintext: WORKSPACE_KEY, + publicKey: bot.publicKey, + privateKey: PRIVATE_KEY + }); + + botKey = { + encryptedKey: ciphertext, + nonce + } + + setBot((await setBotActiveStatus({ + botId: bot._id, + isActive: bot.isActive ? false : true, + botKey + })).bot); + } + } catch (err) { + console.error(err); + } + } + + /** + * Start integration for a given integration option [integrationOption] + * @param {Object} obj + * @param {Object} obj.integrationOption - an integration option + * @param {String} obj.name + * @param {String} obj.type + * @param {String} obj.docsLink + * @returns + */ + const handleIntegrationOption = async ({ integrationOption }) => { + + console.log('handleIntegrationOption', integrationOption); + + try { + // generate CSRF token for OAuth2 code-token exchange integrations + const state = crypto.randomBytes(16).toString("hex"); + localStorage.setItem('latestCSRFToken', state); + + switch (integrationOption.name) { + case 'Heroku': + window.location = `https://id.heroku.com/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=write-protected&state=${state}`; + break; + case 'Vercel': + window.location = `https://vercel.com/integrations/${integrationOption.clientSlug}/new?state=${state}`; + break; + case 'Netlify': + window.location = `https://app.netlify.com/authorize?client_id=${integrationOption.clientId}&response_type=code&state=${state}&redirect_uri=${window.location.origin}/netlify`; + break; + case 'GitHub': + window.location = `https://github.com/login/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=repo&redirect_uri=${window.location.origin}/github&state=${state}`; + break; + // case 'Fly.io': + // console.log('fly.io'); + // setIntegrationAccessTokenDialogOpen(true); + // break; + } + } catch (err) { + console.log(err); + } + } + + /** + * Open dialog to activate bot if bot is not active. + * Otherwise, start integration [integrationOption] + * @param {Object} integrationOption - an integration option + * @param {String} integrationOption.name + * @param {String} integrationOption.type + * @param {String} integrationOption.docsLink + * @returns + */ + const integrationOptionPress = (integrationOption) => { + try { + if (bot.isActive) { + // case: bot is active -> proceed with integration + handleIntegrationOption({ integrationOption }); + return; + } + + // case: bot is not active -> open modal to activate bot + setIsActivateBotDialogOpen(true); + } catch (err) { + console.error(err); + } + } + + return (
- <title> {t("common:head-title", { title: t("integrations:title") })} - -
-
- + + setIsActivateBotDialogOpen(false)} + selectedIntegrationOption={selectedIntegrationOption} + handleBotActivate={handleBotActivate} + handleIntegrationOption={handleIntegrationOption} + /> + {/* setIntegrationAccessTokenDialogOpen(false)} + selectedIntegrationOption={selectedIntegrationOption} + handleBotActivate={handleBotActivate} + handleIntegrationOption={handleIntegrationOption} + /> */} + + {cloudIntegrationOptions.length > 0 ? ( + -
-
-

{t("integrations:title")}

-
-

- {t("integrations:description")} -

-
- {projectIntegrations.length > 0 ? ( - projectIntegrations.map((projectIntegration) => ( - - )) - ) : ( -
-
-
{t("integrations:no-integrations1")}
-
{t("integrations:no-integrations2")}
-
-
- )} -
-
-

- {t("integrations:available")} -

-
-

- {t("integrations:available-text1")} -

-

- {t("integrations:available-text2")} -

-
-
- {Object.keys(integrations).map((integration) => ( -
- - integration logo - {integrations[integration].name.split(" ").length > 2 ? ( -
-
{integrations[integration].name.split(" ")[0]}
-
- {integrations[integration].name.split(" ")[1]}{" "} - {integrations[integration].name.split(" ")[2]} -
-
- ) : ( -
- {integrations[integration].name} -
- )} -
- {["Heroku"].includes(integrations[integration].name) && - authorizations - .map((authorization) => authorization.integration) - .includes(integrations[integration].name.toLowerCase()) && ( -
-
{ - deleteIntegrationAuth({ - integrationAuthId: authorizations - .filter( - (authorization) => - authorization.integration == - integrations[integration].name.toLowerCase() - ) - .map((authorization) => authorization._id)[0], - }); - router.reload(); - }} - className="cursor-pointer w-max bg-red py-0.5 px-2 rounded-b-md text-xs flex flex-row items-center opacity-0 group-hover:opacity-100 duration-200" - > - - Revoke -
-
- - Authorized -
-
- )} - {!["Heroku"].includes(integrations[integration].name) && ( -
-
- Coming Soon -
-
- )} -
- ))} -
-
-
-

Framework Integrations

-
-

- Click on a framework to get the setup instructions. -

-
- -
+ ) : ( +
+ )} +
- ) : ( -
-
- loading animation -
); } diff --git a/frontend/pages/login.js b/frontend/pages/login.js deleted file mode 100644 index 32c5eeaf0..000000000 --- a/frontend/pages/login.js +++ /dev/null @@ -1,160 +0,0 @@ -import React, { useEffect, useState } from "react"; -import Head from "next/head"; -import Image from "next/image"; -import Link from "next/link"; -import Router, { useRouter } from "next/router"; -import { useTranslation } from "next-i18next"; -import { faWarning } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; - -import Button from "~/components/basic/buttons/Button"; -import Error from "~/components/basic/Error"; -import InputField from "~/components/basic/InputField"; -import ListBox from "~/components/basic/Listbox"; -import attemptLogin from "~/utilities/attemptLogin"; -import { getTranslatedStaticProps } from "~/utilities/withTranslateProps"; - -import getWorkspaces from "./api/workspace/getWorkspaces"; - -export default function Login(props) { - const [email, setEmail] = useState(""); - const [password, setPassword] = useState(""); - const [errorLogin, setErrorLogin] = useState(false); - const [isLoading, setIsLoading] = useState(false); - const router = useRouter(); - const { t } = useTranslation(); - const lang = router.locale ?? "en"; - - const setLanguage = async (to) => { - Router.push("/login", "/login", { locale: to }); - localStorage.setItem("lang", to); - }; - - useEffect(async () => { - let userWorkspace; - try { - const userWorkspaces = await getWorkspaces(); - userWorkspace = userWorkspaces[0]._id; - router.push("/dashboard/" + userWorkspace); - } catch (error) { - console.log("Error - Not logged in yet"); - } - }, []); - - /** - * This function check if the user entered the correct credentials and should be allowed to log in. - */ - const loginCheck = async () => { - setIsLoading(true); - await attemptLogin( - email, - password, - setErrorLogin, - router, - false, - true - ).then(() => { - setTimeout(function () { - setIsLoading(false); - }, 2000); - }); - }; - - return ( -
- - {t("common:head-title", { title: t("login:title") })} - - - - - - -
- long logo -
- -
-
-

- {t("login:login")} -

-
-

- {t("login:need-account")} -

-
-
- - - -
-
- -
-
- -
- {errorLogin && } -
-
-
-
- {/*
-

I may have forgotten my password.

-
*/} -
-
-
- -
-
-
- - {false && ( -
- - - {t("common:maintenance-alert")} -
- )} -
- ); -} - -export const getStaticProps = getTranslatedStaticProps(["auth", "login"]); diff --git a/frontend/pages/login.tsx b/frontend/pages/login.tsx new file mode 100644 index 000000000..036902de0 --- /dev/null +++ b/frontend/pages/login.tsx @@ -0,0 +1,151 @@ +import React, { useEffect, useState } from 'react'; +import Head from 'next/head'; +import Image from 'next/image'; +import Link from 'next/link'; +import { useRouter } from 'next/router'; +import { faWarning } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + +import Button from '~/components/basic/buttons/Button'; +import Error from '~/components/basic/Error'; +import InputField from '~/components/basic/InputField'; +import attemptLogin from '~/utilities/attemptLogin'; + +import getWorkspaces from './api/workspace/getWorkspaces'; + +export default function Login() { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [errorLogin, setErrorLogin] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const router = useRouter(); + + useEffect(() => { + const redirectToDashboard = async () => { + let userWorkspace; + try { + const userWorkspaces = await getWorkspaces(); + userWorkspace = userWorkspaces[0]._id; + router.push('/dashboard/' + userWorkspace); + } catch (error) { + console.log('Error - Not logged in yet'); + } + }; + redirectToDashboard(); + }, []); + + /** + * This function check if the user entered the correct credentials and should be allowed to log in. + */ + const loginCheck = async () => { + if (!email || !password) { + return; + } + + setIsLoading(true); + await attemptLogin( + email, + password, + setErrorLogin, + router, + false, + true, + ).then(() => { + setTimeout(function () { + setIsLoading(false); + }, 2000); + }); + }; + + return ( +
+ + Login + + + + + + +
+ long logo +
+ +
setErrorLogin(false)} onSubmit={(e) => e.preventDefault()} + > +
+

+ Log in to your account +

+
+ +
+
+ +
+ Forgot password? +
+
+ {!isLoading && errorLogin && } +
+
+
+
+ {/*
+

I may have forgotten my password.

+
*/} +
+ {false && ( +
+ + We are experiencing minor technical difficulties. We are working on + solving it right now. Please come back in a few minutes. +
+ )} +
+

+ Need an Infisical account? +

+ + + +
+
+
+ ); +} diff --git a/frontend/pages/netlify.js b/frontend/pages/netlify.js new file mode 100644 index 000000000..6907d6db4 --- /dev/null +++ b/frontend/pages/netlify.js @@ -0,0 +1,41 @@ +import React, { useEffect } from "react"; +import Head from "next/head"; +import { useRouter } from "next/router"; +const queryString = require("query-string"); +import AuthorizeIntegration from "./api/integrations/authorizeIntegration"; + +export default function Netlify() { + const router = useRouter(); + const parsedUrl = queryString.parse(router.asPath.split("?")[1]); + const code = parsedUrl.code; + const state = parsedUrl.state; + // modify comment here + + /** + * Here we forward to the default workspace if a user opens this url + */ + // eslint-disable-next-line react-hooks/exhaustive-deps + useEffect(async () => { + try { + if (state === localStorage.getItem('latestCSRFToken')) { + localStorage.removeItem('latestCSRFToken'); + + await AuthorizeIntegration({ + workspaceId: localStorage.getItem('projectData.id'), + code, + integration: "netlify" + }); + + router.push("/integrations/" + localStorage.getItem("projectData.id")); + } + } catch (err) { + console.error('Netlify integration error: ', err); + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return
; +} + +Netlify.requireAuth = true; diff --git a/frontend/pages/noprojects.js b/frontend/pages/noprojects.js index 2e55b0b4c..2aebcf5e0 100644 --- a/frontend/pages/noprojects.js +++ b/frontend/pages/noprojects.js @@ -1,21 +1,30 @@ import React from "react"; +import Image from "next/image"; import { faFolderOpen } from "@fortawesome/free-regular-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; export default function NoProjects() { return (
- -
- You are not part of any projects in this organization yet. When you do, - they will appear here. +
+ google logo
-
- Create a new project, or ask other organization members to give you - neccessary permissions. +
+
+ You are not part of any projects in this organization yet. When you do, + they will appear here. +
+
+ Create a new project, or ask other organization members to give you + neccessary permissions. +
); diff --git a/frontend/pages/password-reset.tsx b/frontend/pages/password-reset.tsx new file mode 100644 index 000000000..a09b85ab0 --- /dev/null +++ b/frontend/pages/password-reset.tsx @@ -0,0 +1,290 @@ +import React, { useState } from 'react'; +import Image from 'next/image'; +import { useRouter } from 'next/router'; +import { faCheck, faX } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + +import Button from '~/components/basic/buttons/Button'; +import InputField from '~/components/basic/InputField'; +import passwordCheck from '~/components/utilities/checks/PasswordCheck'; +import Aes256Gcm from '~/components/utilities/cryptography/aes-256-gcm'; + +import EmailVerifyOnPasswordReset from './api/auth/EmailVerifyOnPasswordReset'; +import getBackupEncryptedPrivateKey from './api/auth/getBackupEncryptedPrivateKey'; +import resetPasswordOnAccountRecovery from './api/auth/resetPasswordOnAccountRecovery'; + +const queryString = require('query-string'); +const nacl = require('tweetnacl'); +const jsrp = require('jsrp'); +nacl.util = require('tweetnacl-util'); +const client = new jsrp.client(); + +export default function PasswordReset() { + const router = useRouter(); + const parsedUrl = queryString.parse(router.asPath.split('?')[1]); + const token = parsedUrl.token; + const email = parsedUrl.to?.replace(' ', '+').trim(); + const [verificationToken, setVerificationToken] = useState(''); + const [step, setStep] = useState(1); + const [backupKey, setBackupKey] = useState(''); + const [privateKey, setPrivateKey] = useState(''); + const [newPassword, setNewPassword] = useState(''); + const [backupKeyError, setBackupKeyError] = useState(false); + const [passwordErrorLength, setPasswordErrorLength] = useState(false); + const [passwordErrorNumber, setPasswordErrorNumber] = useState(false); + const [passwordErrorLowerCase, setPasswordErrorLowerCase] = useState(false); + + // Unencrypt the private key with a backup key + const getEncryptedKeyHandler = async () => { + try { + const result = await getBackupEncryptedPrivateKey({ verificationToken }); + setPrivateKey( + Aes256Gcm.decrypt({ + ciphertext: result.encryptedPrivateKey, + iv: result.iv, + tag: result.tag, + secret: backupKey + }) + ); + setStep(3); + } catch { + setBackupKeyError(true); + } + }; + + // If everything is correct, reset the password + const resetPasswordHandler = async () => { + let errorCheck = false; + errorCheck = passwordCheck({ + password: newPassword, + setPasswordErrorLength, + setPasswordErrorNumber, + setPasswordErrorLowerCase, + currentErrorCheck: errorCheck + }); + + if (!errorCheck) { + // Generate a random pair of a public and a private key + const { ciphertext, iv, tag } = Aes256Gcm.encrypt({ + text: privateKey, + secret: newPassword + .slice(0, 32) + .padStart( + 32 + + (newPassword.slice(0, 32).length - new Blob([newPassword]).size), + '0' + ) + }) as { ciphertext: string; iv: string; tag: string }; + + client.init( + { + username: email, + password: newPassword + }, + async () => { + client.createVerifier( + async (err: any, result: { salt: string; verifier: string }) => { + const response = await resetPasswordOnAccountRecovery({ + verificationToken, + encryptedPrivateKey: ciphertext, + iv, + tag, + salt: result.salt, + verifier: result.verifier + }); + + // if everything works, go the main dashboard page. + if (response?.status === 200) { + router.push('/login'); + } + } + ); + } + ); + } + }; + + // Click a button to confirm email + const stepConfirmEmail = ( +
+

+ Confirm your email +

+ verify email +
+
+
+ ); + + // Input backup key + const stepInputBackupKey = ( +
+

+ Enter your backup key +

+
+

+ You can find it in your emrgency kit. You had to download the enrgency + kit during signup. +

+
+
+ +
+
+
+
+
+
+ ); + + // Enter new password + const stepEnterNewPassword = ( +
+

+ Enter new password +

+
+

+ Make sure you save it somewhere save. +

+
+
+ { + setNewPassword(password); + passwordCheck({ + password, + setPasswordErrorLength, + setPasswordErrorNumber, + setPasswordErrorLowerCase, + currentErrorCheck: false + }); + }} + type="password" + value={newPassword} + isRequired + error={ + passwordErrorLength && passwordErrorLowerCase && passwordErrorNumber + } + autoComplete="new-password" + id="new-password" + /> +
+ {passwordErrorLength || passwordErrorLowerCase || passwordErrorNumber ? ( +
+
+ Password should contain at least: +
+
+ {passwordErrorLength ? ( + + ) : ( + + )} +
+ 14 characters +
+
+
+ {passwordErrorLowerCase ? ( + + ) : ( + + )} +
+ 1 lowercase character +
+
+
+ {passwordErrorNumber ? ( + + ) : ( + + )} +
+ 1 number +
+
+
+ ) : ( +
+ )} +
+
+
+
+
+ ); + + return ( +
+ {step === 1 && stepConfirmEmail} + {step === 2 && stepInputBackupKey} + {step === 3 && stepEnterNewPassword} +
+ ); +} diff --git a/frontend/pages/settings/org/[id].js b/frontend/pages/settings/org/[id].js index 1c0853703..e2250aa26 100644 --- a/frontend/pages/settings/org/[id].js +++ b/frontend/pages/settings/org/[id].js @@ -5,10 +5,10 @@ import { useTranslation } from "next-i18next"; import { faMagnifyingGlass, faPlus, - faX, -} from "@fortawesome/free-solid-svg-icons"; -import { faCheck } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + faX +} from '@fortawesome/free-solid-svg-icons'; +import { faCheck } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import Button from "~/components/basic/buttons/Button"; import AddIncidentContactDialog from "~/components/basic/dialog/AddIncidentContactDialog"; @@ -19,48 +19,48 @@ import NavHeader from "~/components/navigation/NavHeader"; import guidGenerator from "~/utilities/randomId"; import { getTranslatedServerSideProps } from "~/utilities/withTranslateProps"; -import addUserToOrg from "../../api/organization/addUserToOrg"; -import deleteIncidentContact from "../../api/organization/deleteIncidentContact"; -import getIncidentContacts from "../../api/organization/getIncidentContacts"; -import getOrganization from "../../api/organization/GetOrg"; -import getOrganizationSubscriptions from "../../api/organization/GetOrgSubscription"; -import getOrganizationUsers from "../../api/organization/GetOrgUsers"; -import renameOrg from "../../api/organization/renameOrg"; -import getUser from "../../api/user/getUser"; -import deleteWorkspace from "../../api/workspace/deleteWorkspace"; -import getWorkspaces from "../../api/workspace/getWorkspaces"; +import addUserToOrg from '../../api/organization/addUserToOrg'; +import deleteIncidentContact from '../../api/organization/deleteIncidentContact'; +import getIncidentContacts from '../../api/organization/getIncidentContacts'; +import getOrganization from '../../api/organization/GetOrg'; +import getOrganizationSubscriptions from '../../api/organization/GetOrgSubscription'; +import getOrganizationUsers from '../../api/organization/GetOrgUsers'; +import renameOrg from '../../api/organization/renameOrg'; +import getUser from '../../api/user/getUser'; +import deleteWorkspace from '../../api/workspace/deleteWorkspace'; +import getWorkspaces from '../../api/workspace/getWorkspaces'; export default function SettingsOrg() { const [buttonReady, setButtonReady] = useState(false); const router = useRouter(); - const [orgName, setOrgName] = useState(""); - const [emailUser, setEmailUser] = useState(""); - const [workspaceToBeDeletedName, setWorkspaceToBeDeletedName] = useState(""); - const [searchUsers, setSearchUsers] = useState(""); - const [workspaceId, setWorkspaceId] = useState(""); + const [orgName, setOrgName] = useState(''); + const [emailUser, setEmailUser] = useState(''); + const [workspaceToBeDeletedName, setWorkspaceToBeDeletedName] = useState(''); + const [searchUsers, setSearchUsers] = useState(''); + const [workspaceId, setWorkspaceId] = useState(''); const [isAddIncidentContactOpen, setIsAddIncidentContactOpen] = useState(false); const [isAddUserOpen, setIsAddUserOpen] = useState( - router.asPath.split("?")[1] == "invite" + router.asPath.split('?')[1] == 'invite' ); const [incidentContacts, setIncidentContacts] = useState([]); - const [searchIncidentContact, setSearchIncidentContact] = useState(""); + const [searchIncidentContact, setSearchIncidentContact] = useState(''); const [userList, setUserList] = useState(); - const [personalEmail, setPersonalEmail] = useState(""); + const [personalEmail, setPersonalEmail] = useState(''); let workspaceIdTemp; - const [email, setEmail] = useState(""); - const [currentPlan, setCurrentPlan] = useState(""); + const [email, setEmail] = useState(''); + const [currentPlan, setCurrentPlan] = useState(''); const { t } = useTranslation(); useEffect(async () => { let org = await getOrganization({ - orgId: localStorage.getItem("orgData.id"), + orgId: localStorage.getItem('orgData.id') }); let orgData = org; setOrgName(orgData.name); let incidentContactsData = await getIncidentContacts( - localStorage.getItem("orgData.id") + localStorage.getItem('orgData.id') ); setIncidentContacts(incidentContactsData?.map((contact) => contact.email)); @@ -70,7 +70,7 @@ export default function SettingsOrg() { workspaceIdTemp = router.query.id; let orgUsers = await getOrganizationUsers({ - orgId: localStorage.getItem("orgData.id"), + orgId: localStorage.getItem('orgData.id') }); setUserList( orgUsers.map((user) => ({ @@ -82,11 +82,11 @@ export default function SettingsOrg() { status: user?.status, userId: user.user?._id, membershipId: user._id, - publicKey: user.user?.publicKey, + publicKey: user.user?.publicKey })) ); const subscriptions = await getOrganizationSubscriptions({ - orgId: localStorage.getItem("orgData.id"), + orgId: localStorage.getItem('orgData.id') }); setCurrentPlan(subscriptions.data[0].plan.product); }, []); @@ -97,7 +97,7 @@ export default function SettingsOrg() { }; const submitChanges = (newOrgName) => { - renameOrg(localStorage.getItem("orgData.id"), newOrgName); + renameOrg(localStorage.getItem('orgData.id'), newOrgName); setButtonReady(false); }; @@ -122,8 +122,8 @@ export default function SettingsOrg() { } async function submitAddUserModal(email) { - await addUserToOrg(email, localStorage.getItem("orgData.id")); - setEmail(""); + await addUserToOrg(email, localStorage.getItem('orgData.id')); + setEmail(''); setIsAddUserOpen(false); router.reload(); } @@ -132,7 +132,7 @@ export default function SettingsOrg() { setIncidentContacts( incidentContacts.filter((contact) => contact != incidentContact) ); - deleteIncidentContact(localStorage.getItem("orgData.id"), incidentContact); + deleteIncidentContact(localStorage.getItem('orgData.id'), incidentContact); }; /** @@ -152,7 +152,7 @@ export default function SettingsOrg() { ) { await deleteWorkspace(router.query.id); let userWorkspaces = await getWorkspaces(); - router.push("/dashboard/" + userWorkspaces[0]._id); + router.push('/dashboard/' + userWorkspaces[0]._id); } } }; diff --git a/frontend/pages/settings/personal/[id].js b/frontend/pages/settings/personal/[id].js index 5d170e670..f56281c50 100644 --- a/frontend/pages/settings/personal/[id].js +++ b/frontend/pages/settings/personal/[id].js @@ -1,7 +1,7 @@ import React, { useCallback, useEffect, useState } from "react"; import Head from "next/head"; +import { useRouter } from "next/router"; import { useTranslation } from "next-i18next"; -import setLanguage from "next-translate/setLanguage"; import { faCheck, faX } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -30,7 +30,14 @@ export default function PersonalSettings() { const [backupKeyIssued, setBackupKeyIssued] = useState(false); const [backupKeyError, setBackupKeyError] = useState(false); - const { t, lang } = useTranslation(); + const { t } = useTranslation(); + const router = useRouter(); + const lang = router.locale ?? "en"; + + const setLanguage = async (to) => { + router.push(router.asPath, router.asPath, { locale: to }); + localStorage.setItem("lang", to); + }; useEffect(async () => { let user = await getUser(); @@ -145,19 +152,21 @@ export default function PersonalSettings() { isRequired error={currentPasswordError} errorText={t("section-password:current-wrong")} + autoComplete="current-password" + id="current-password" />
{ setNewPassword(password); - passwordCheck( + passwordCheck({ password, setPasswordErrorLength, setPasswordErrorNumber, setPasswordErrorLowerCase, - false - ); + currentErrorCheck: false, + }); }} type="password" value={newPassword} @@ -167,6 +176,8 @@ export default function PersonalSettings() { passwordErrorLowerCase && passwordErrorNumber } + autoComplete="new-password" + id="new-password" />
{passwordErrorLength || @@ -307,6 +318,8 @@ export default function PersonalSettings() { isRequired error={backupKeyError} errorText={t("section-password:current-wrong")} + autoComplete="current-password" + id="current-password" />
diff --git a/frontend/pages/signup.tsx b/frontend/pages/signup.tsx index 95e2840a7..ff11aa6f2 100644 --- a/frontend/pages/signup.tsx +++ b/frontend/pages/signup.tsx @@ -1,10 +1,10 @@ -import React, { useEffect, useRef, useState } from "react"; +import React, { useEffect, useState } from "react"; import ReactCodeInput from "react-code-input"; -import dynamic from "next/dynamic"; import Head from "next/head"; import Image from "next/image"; import Link from "next/link"; import { useRouter } from "next/router"; +import { useTranslation } from "next-i18next"; import { faCheck, faWarning, faX } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -13,6 +13,7 @@ import Error from "~/components/basic/Error"; import InputField from "~/components/basic/InputField"; import Aes256Gcm from "~/components/utilities/cryptography/aes-256-gcm"; import issueBackupKey from "~/components/utilities/cryptography/issueBackupKey"; +import { getTranslatedStaticProps } from "~/components/utilities/withTranslateProps"; import attemptLogin from "~/utilities/attemptLogin"; import passwordCheck from "~/utilities/checks/PasswordCheck"; @@ -20,8 +21,6 @@ import checkEmailVerificationCode from "./api/auth/CheckEmailVerificationCode"; import completeAccountInformationSignup from "./api/auth/CompleteAccountInformationSignup"; import sendVerificationEmail from "./api/auth/SendVerificationEmail"; import getWorkspaces from "./api/workspace/getWorkspaces"; -import { Trans, useTranslation } from "next-i18next"; -import { getTranslatedStaticProps } from "~/components/utilities/withTranslateProps"; // const ReactCodeInput = dynamic(import("react-code-input")); const nacl = require("tweetnacl"); @@ -84,8 +83,10 @@ export default function SignUp() { const router = useRouter(); const [errorLogin, setErrorLogin] = useState(false); const [isLoading, setIsLoading] = useState(false); + const [isResendingVerificationEmail, setIsResendingVerificationEmail] = + useState(false); const [backupKeyError, setBackupKeyError] = useState(false); - const [verificationToken, setVerificationToken] = useState(); + const [verificationToken, setVerificationToken] = useState(""); const [backupKeyIssued, setBackupKeyIssued] = useState(false); const { t } = useTranslation(); @@ -113,7 +114,7 @@ export default function SignUp() { setStep(2); } else if (step == 2) { // Checking if the code matches the email. - const response = await checkEmailVerificationCode(email, code); + const response = await checkEmailVerificationCode({ email, code }); if (response.status === 200 || code == "111222") { setVerificationToken((await response.json()).token); setStep(3); @@ -153,7 +154,8 @@ export default function SignUp() { } }; - // Verifies if the imformation that the users entered (name, workspace) is there, and if the password matched the criteria. + // Verifies if the imformation that the users entered (name, workspace) is there, and if the password matched the + // criteria. const signupErrorCheck = async () => { setIsLoading(true); let errorCheck = false; @@ -185,15 +187,15 @@ export default function SignUp() { const PRIVATE_KEY = nacl.util.encodeBase64(secretKeyUint8Array); const PUBLIC_KEY = nacl.util.encodeBase64(publicKeyUint8Array); - const { ciphertext, iv, tag } = Aes256Gcm.encrypt( - PRIVATE_KEY, - password + const { ciphertext, iv, tag } = Aes256Gcm.encrypt({ + text: PRIVATE_KEY, + secret: password .slice(0, 32) .padStart( 32 + (password.slice(0, 32).length - new Blob([password]).size), "0" - ) - ) as { ciphertext: string; iv: string; tag: string }; + ), + }) as { ciphertext: string; iv: string; tag: string }; localStorage.setItem("PRIVATE_KEY", PRIVATE_KEY); @@ -251,15 +253,28 @@ export default function SignUp() { } }; + const resendVerificationEmail = async () => { + setIsResendingVerificationEmail(true); + setIsLoading(true); + await sendVerificationEmail(email); + setTimeout(() => { + setIsLoading(false); + setIsResendingVerificationEmail(false); + }, 2000); + }; + // Step 1 of the sign up process (enter the email or choose google authentication) const step1 = (

- {t("signup:step1-start")} + {"Let'"}s get started

-
{/*
@@ -287,11 +303,7 @@ export default function SignUp() { {t("signup:step1-privacy")}

-
@@ -300,17 +312,12 @@ export default function SignUp() { // Step 2 of the signup process (enter the email verification code) const step2 = (
- , - email: ( -

- ), - }} - values={{ email }} - /> - +

+ {"We've"} sent a verification email to{" "} +

+

+ {email}{" "} +

}
- {/* - - */} +
+ Not seeing an email? + + + +

{t("signup:step2-spam-alert")}

@@ -375,6 +389,7 @@ export default function SignUp() { }) as string } error={firstNameError} + autoComplete="given-name" />
@@ -390,6 +405,7 @@ export default function SignUp() { }) as string } error={lastNameError} + autoComplete="family-name" />
@@ -411,6 +427,8 @@ export default function SignUp() { error={ passwordErrorLength && passwordErrorNumber && passwordErrorLowerCase } + autoComplete="new-password" + id="new-password" /> {passwordErrorLength || passwordErrorLowerCase || @@ -486,7 +504,7 @@ export default function SignUp() {
); diff --git a/frontend/pages/signupinvite.js b/frontend/pages/signupinvite.js index 15c511739..a41010aea 100644 --- a/frontend/pages/signupinvite.js +++ b/frontend/pages/signupinvite.js @@ -1,39 +1,39 @@ -import React, { useState } from "react"; -import Head from "next/head"; -import Image from "next/image"; -import Link from "next/link"; -import { useRouter } from "next/router"; -import { faCheck, faWarning, faX } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import React, { useState } from 'react'; +import Head from 'next/head'; +import Image from 'next/image'; +import Link from 'next/link'; +import { useRouter } from 'next/router'; +import { faCheck, faWarning, faX } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import Button from "~/components/basic/buttons/Button"; -import InputField from "~/components/basic/InputField"; -import Aes256Gcm from "~/components/utilities/cryptography/aes-256-gcm"; -import issueBackupKey from "~/components/utilities/cryptography/issueBackupKey"; -import attemptLogin from "~/utilities/attemptLogin"; -import passwordCheck from "~/utilities/checks/PasswordCheck"; +import Button from '~/components/basic/buttons/Button'; +import InputField from '~/components/basic/InputField'; +import Aes256Gcm from '~/components/utilities/cryptography/aes-256-gcm'; +import issueBackupKey from '~/components/utilities/cryptography/issueBackupKey'; +import attemptLogin from '~/utilities/attemptLogin'; +import passwordCheck from '~/utilities/checks/PasswordCheck'; -import completeAccountInformationSignupInvite from "./api/auth/CompleteAccountInformationSignupInvite"; -import verifySignupInvite from "./api/auth/VerifySignupInvite"; +import completeAccountInformationSignupInvite from './api/auth/CompleteAccountInformationSignupInvite'; +import verifySignupInvite from './api/auth/VerifySignupInvite'; -const nacl = require("tweetnacl"); -const jsrp = require("jsrp"); -nacl.util = require("tweetnacl-util"); +const nacl = require('tweetnacl'); +const jsrp = require('jsrp'); +nacl.util = require('tweetnacl-util'); const client = new jsrp.client(); -const queryString = require("query-string"); +const queryString = require('query-string'); export default function SignupInvite() { - const [password, setPassword] = useState(""); - const [firstName, setFirstName] = useState(""); - const [lastName, setLastName] = useState(""); + const [password, setPassword] = useState(''); + const [firstName, setFirstName] = useState(''); + const [lastName, setLastName] = useState(''); const [firstNameError, setFirstNameError] = useState(false); const [lastNameError, setLastNameError] = useState(false); const [passwordErrorLength, setPasswordErrorLength] = useState(false); const [passwordErrorNumber, setPasswordErrorNumber] = useState(false); const [passwordErrorLowerCase, setPasswordErrorLowerCase] = useState(false); const router = useRouter(); - const parsedUrl = queryString.parse(router.asPath.split("?")[1]); - const [email, setEmail] = useState(parsedUrl.to); + const parsedUrl = queryString.parse(router.asPath.split('?')[1]); + const [email, setEmail] = useState(parsedUrl.to?.replace(' ', '+').trim()); const token = parsedUrl.token; const [errorLogin, setErrorLogin] = useState(false); const [isLoading, setIsLoading] = useState(false); @@ -58,13 +58,13 @@ export default function SignupInvite() { } else { setLastNameError(false); } - errorCheck = passwordCheck( + errorCheck = passwordCheck({ password, setPasswordErrorLength, setPasswordErrorNumber, setPasswordErrorLowerCase, errorCheck - ); + }); if (!errorCheck) { // Generate a random pair of a public and a private key @@ -74,21 +74,22 @@ export default function SignupInvite() { const PRIVATE_KEY = nacl.util.encodeBase64(secretKeyUint8Array); const PUBLIC_KEY = nacl.util.encodeBase64(publicKeyUint8Array); - const { ciphertext, iv, tag } = Aes256Gcm.encrypt( - PRIVATE_KEY, - password + const { ciphertext, iv, tag } = Aes256Gcm.encrypt({ + text: PRIVATE_KEY, + secret: password .slice(0, 32) .padStart( 32 + (password.slice(0, 32).length - new Blob([password]).size), - "0" + '0' ) - ); - localStorage.setItem("PRIVATE_KEY", PRIVATE_KEY); + }); + + localStorage.setItem('PRIVATE_KEY', PRIVATE_KEY); client.init( { username: email, - password: password, + password: password }, async () => { client.createVerifier(async (err, result) => { @@ -102,17 +103,17 @@ export default function SignupInvite() { tag, salt: result.salt, verifier: result.verifier, - token: verificationToken, + token: verificationToken }); // if everything works, go the main dashboard page. - if (!errorCheck && response.status == "200") { + if (!errorCheck && response.status == '200') { response = await response.json(); - localStorage.setItem("publicKey", PUBLIC_KEY); - localStorage.setItem("encryptedPrivateKey", ciphertext); - localStorage.setItem("iv", iv); - localStorage.setItem("tag", tag); + localStorage.setItem('publicKey', PUBLIC_KEY); + localStorage.setItem('encryptedPrivateKey', ciphertext); + localStorage.setItem('iv', iv); + localStorage.setItem('tag', tag); try { await attemptLogin( @@ -126,7 +127,7 @@ export default function SignupInvite() { setStep(3); } catch (error) { setIsLoading(false); - console.log("Error", error); + console.log('Error', error); } } }); @@ -149,20 +150,20 @@ export default function SignupInvite() { width={410} alt="verify email" > -
+
@@ -197,6 +199,7 @@ export default function SignupInvite() { isRequired errorText="Please input your last name." error={lastNameError} + autoComplete="family-name" />
@@ -204,13 +207,13 @@ export default function SignupInvite() { label="Password" onChangeHandler={(password) => { setPassword(password); - passwordCheck( + passwordCheck({ password, setPasswordErrorLength, setPasswordErrorNumber, setPasswordErrorLowerCase, - false - ); + currentErrorCheck: false + }); }} type="password" value={password} @@ -218,6 +221,8 @@ export default function SignupInvite() { error={ passwordErrorLength && passwordErrorNumber && passwordErrorLowerCase } + autoComplete="new-password" + id="new-password" /> {passwordErrorLength || passwordErrorLowerCase || @@ -240,7 +245,7 @@ export default function SignupInvite() { )}
14 characters @@ -260,7 +265,7 @@ export default function SignupInvite() { )}
1 lowercase character @@ -280,7 +285,7 @@ export default function SignupInvite() { )}
1 number @@ -324,18 +329,18 @@ export default function SignupInvite() { It contains your Secret Key which we cannot access or recover for you if you lose it.
-
+
+
+
+ )} + {step == 2 && ( +
+

+ Look for an email in your inbox. +

+
+

+ An email with instructions has been sent to {email}. +

+
+
+ )} +
+ ); +} diff --git a/frontend/public/data/frequentConstants.ts b/frontend/public/data/frequentConstants.ts index fbc0034d3..d2beee6d2 100644 --- a/frontend/public/data/frequentConstants.ts +++ b/frontend/public/data/frequentConstants.ts @@ -1,82 +1,37 @@ -const envMapping = { +interface Mapping { + [key: string]: string; +} + +const envMapping: Mapping = { Development: "dev", Staging: "staging", Production: "prod", Testing: "test", }; - -const reverseEnvMapping = { + +const reverseEnvMapping: Mapping = { dev: "Development", staging: "Staging", prod: "Production", test: "Testing", }; -const frameworks = [{ - "name": "Docker", - "image": "Docker", - "link": "https://infisical.com/docs/integrations/platforms/docker" - }, { - "name": "Docker Compose", - "image": "Docker Compose", - "link": "https://infisical.com/docs/integrations/platforms/docker-compose" - }, { - "name": "React", - "image": "React", - "link": "https://infisical.com/docs/integrations/frameworks/react" - }, { - "name": "Vue", - "image": "Vue", - "link": "https://infisical.com/docs/integrations/frameworks/vue" - }, { - "image": "Express", - "link": "https://infisical.com/docs/integrations/frameworks/express" - },{ - "image": "Next.js", - "link": "https://infisical.com/docs/integrations/frameworks/nextjs" - }, { - "name": "Django", - "image": "Django", - "link": "https://infisical.com/docs/integrations/frameworks/django" - }, { - "name": "NestJS", - "image": "NestJS", - "link": "https://infisical.com/docs/integrations/frameworks/nestjs" - }, { - "name": "Nuxt", - "image": "Nuxt", - "link": "https://infisical.com/docs/integrations/frameworks/nuxt" - }, { - "name": "Gatsby", - "image": "Gatsby", - "link": "https://infisical.com/docs/integrations/frameworks/gatsby" - }, { - "name": "Remix", - "image": "Remix", - "link": "https://infisical.com/docs/integrations/frameworks/remix" - }, { - "name": "Vite", - "image": "Vite", - "link": "https://infisical.com/docs/integrations/frameworks/vite" - }, { - "image": "Fiber", - "link": "https://infisical.com/docs/integrations/frameworks/fiber" - }, { - "name": "Flask", - "image": "Flask", - "link": "https://infisical.com/docs/integrations/frameworks/flask" - }, { - "name": "Laravel", - "image": "Laravel", - "link": "https://infisical.com/docs/integrations/frameworks/laravel" - }, { - "image": "Rails", - "link": "https://infisical.com/docs/integrations/frameworks/rails" - } -] +const contextNetlifyMapping: Mapping = { + "dev": "Local development", + "branch-deploy": "Branch deploys", + "deploy-review": "Deploy Previews", + "production": "Production" +} + +const reverseContextNetlifyMapping: Mapping = { + "Local development": "dev", + "Branch deploys": "branch-deploy", + "Deploy Previews": "deploy-preview", + "Production": "production" +} export { + contextNetlifyMapping, envMapping, - frameworks, - reverseEnvMapping -}; + reverseContextNetlifyMapping, + reverseEnvMapping} diff --git a/frontend/public/images/dragon-404.svg b/frontend/public/images/dragon-404.svg new file mode 100644 index 000000000..dcd7f715a --- /dev/null +++ b/frontend/public/images/dragon-404.svg @@ -0,0 +1,475 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/public/images/dragon-cant-find.png b/frontend/public/images/dragon-cant-find.png new file mode 100644 index 000000000..a6cde8dcb Binary files /dev/null and b/frontend/public/images/dragon-cant-find.png differ diff --git a/frontend/public/images/integrations/GitHub.png b/frontend/public/images/integrations/GitHub.png new file mode 100644 index 000000000..9490ffc6d Binary files /dev/null and b/frontend/public/images/integrations/GitHub.png differ diff --git a/frontend/public/images/integrations/Vercel.png b/frontend/public/images/integrations/Vercel.png new file mode 100644 index 000000000..7bdcd2a19 Binary files /dev/null and b/frontend/public/images/integrations/Vercel.png differ diff --git a/frontend/public/images/progress-0.svg b/frontend/public/images/progress-0.svg new file mode 100644 index 000000000..8d338b719 --- /dev/null +++ b/frontend/public/images/progress-0.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/images/progress-14.svg b/frontend/public/images/progress-14.svg new file mode 100644 index 000000000..33d8522b9 --- /dev/null +++ b/frontend/public/images/progress-14.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/public/images/progress-28.svg b/frontend/public/images/progress-28.svg new file mode 100644 index 000000000..59273c32b --- /dev/null +++ b/frontend/public/images/progress-28.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/images/progress-43.svg b/frontend/public/images/progress-43.svg new file mode 100644 index 000000000..fd963c69e --- /dev/null +++ b/frontend/public/images/progress-43.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/images/progress-57.svg b/frontend/public/images/progress-57.svg new file mode 100644 index 000000000..e878c2049 --- /dev/null +++ b/frontend/public/images/progress-57.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/images/progress-71.svg b/frontend/public/images/progress-71.svg new file mode 100644 index 000000000..1c1643e70 --- /dev/null +++ b/frontend/public/images/progress-71.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/json/frameworkIntegrations.json b/frontend/public/json/frameworkIntegrations.json new file mode 100644 index 000000000..fb8b30c9e --- /dev/null +++ b/frontend/public/json/frameworkIntegrations.json @@ -0,0 +1,98 @@ +[ + { + "name": "Docker", + "slug": "docker", + "image": "Docker", + "docsLink": "https://infisical.com/docs/integrations/platforms/docker" + }, + { + "name": "Docker Compose", + "slug": "docker-compose", + "image": "Docker Compose", + "docsLink": "https://infisical.com/docs/integrations/platforms/docker-compose" + }, + { + "name": "React", + "slug": "react", + "image": "React", + "docsLink": "https://infisical.com/docs/integrations/frameworks/react" + }, + { + "name": "Vue", + "slug": "vue", + "image": "Vue", + "docsLink": "https://infisical.com/docs/integrations/frameworks/vue" + }, + { + "name": "Express", + "slug": "express", + "image": "Express", + "docsLink": "https://infisical.com/docs/integrations/frameworks/express" + }, + { + "name": "Next.js", + "slug": "nextjs", + "image": "Next.js", + "docsLink": "https://infisical.com/docs/integrations/frameworks/nextjs" + }, + { + "name": "Django", + "slug": "django", + "image": "Django", + "docsLink": "https://infisical.com/docs/integrations/frameworks/django" + }, + { + "name": "NestJS", + "slug": "nestjs", + "image": "NestJS", + "docsLink": "https://infisical.com/docs/integrations/frameworks/nestjs" + }, + { + "name": "Nuxt", + "slug": "nuxt", + "image": "Nuxt", + "docsLink": "https://infisical.com/docs/integrations/frameworks/nuxt" + }, + { + "name": "Gatsby", + "slug": "gatsby", + "image": "Gatsby", + "docsLink": "https://infisical.com/docs/integrations/frameworks/gatsby" + }, + { + "name": "Remix", + "slug": "remix", + "image": "Remix", + "docsLink": "https://infisical.com/docs/integrations/frameworks/remix" + }, + { + "name": "Vite", + "slug": "vite", + "image": "Vite", + "docsLink": "https://infisical.com/docs/integrations/frameworks/vite" + }, + { + "name": "Fiber", + "slug": "fiber", + "image": "Fiber", + "docsLink": "https://infisical.com/docs/integrations/frameworks/fiber" + }, + { + "name": "Flask", + "slug": "flask", + "image": "Flask", + "docsLink": "https://infisical.com/docs/integrations/frameworks/flask" + }, + { + "name": "Laravel", + "slug": "laravel", + "image": "Laravel", + "docsLink": "https://infisical.com/docs/integrations/frameworks/laravel" + }, + { + "name": "Rails", + "slug": "rails", + "image": "Rails", + "docsLink": "https://infisical.com/docs/integrations/frameworks/rails" + } +] \ No newline at end of file diff --git a/frontend/scripts/healthcheck.js b/frontend/scripts/healthcheck.js new file mode 100644 index 000000000..30a964d9d --- /dev/null +++ b/frontend/scripts/healthcheck.js @@ -0,0 +1,23 @@ +const http = require('http'); +const options = { + host: 'localhost', + port: 3000, + timeout: 2000, + path: '/' +}; + +const healthCheck = http.request(options, (res) => { + console.log(`HEALTHCHECK STATUS: ${res.statusCode}`); + if (res.statusCode == 200) { + process.exit(0); + } else { + process.exit(1); + } +}); + +healthCheck.on('error', function (err) { + console.error(`HEALTH CHECK ERROR: ${err}`); + process.exit(1); +}); + +healthCheck.end(); diff --git a/frontend/scripts/replace-variable.sh b/frontend/scripts/replace-variable.sh new file mode 100644 index 000000000..f03bff93c --- /dev/null +++ b/frontend/scripts/replace-variable.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +ORIGINAL=$1 +REPLACEMENT=$2 + +if [ "${ORIGINAL}" = "${REPLACEMENT}" ]; then + echo "Environment variable replacement is the same, skipping.." + exit 0 +fi + +echo "Replacing pre-baked value.." + +find /app/public /app/.next -type f -name "*.js" | +while read file; do + sed -i "s|$ORIGINAL|$REPLACEMENT|g" "$file" +done diff --git a/frontend/scripts/set-telemetry.sh b/frontend/scripts/set-telemetry.sh new file mode 100644 index 000000000..594f4d196 --- /dev/null +++ b/frontend/scripts/set-telemetry.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +VALUE=$1 + +find /app/public /app/.next -type f -name "*.js" | +while read file; do + sed -i "s|TELEMETRY_CAPTURING_ENABLED|$VALUE|g" "$file" +done diff --git a/frontend/scripts/start.sh b/frontend/scripts/start.sh new file mode 100644 index 000000000..05a1f219a --- /dev/null +++ b/frontend/scripts/start.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +scripts/replace-variable.sh "$BAKED_NEXT_PUBLIC_POSTHOG_API_KEY" "$NEXT_PUBLIC_POSTHOG_API_KEY" + +if [ "$INFISICAL_TELEMETRY_ENABLED" != "false" ]; then + echo "Telemetry is enabled" + scripts/set-telemetry.sh true +else + echo "Client opted out of telemetry" + scripts/set-telemetry.sh false +fi + + +node server.js diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 16844a8cb..e936a69a2 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -2,25 +2,13 @@ "compilerOptions": { "baseUrl": ".", "paths": { - "~/components/*": [ - "components/*" - ], - "~/utilities/*": [ - "components/utilities/*" - ], - "~/*": [ - "const" - ], - "~/pages/*": [ - "pages/*" - ] + "~/components/*": ["components/*"], + "~/utilities/*": ["components/utilities/*"], + "~/*": ["const"], + "~/pages/*": ["pages/*"] }, - "target": "es5", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "target": "ESNext", + "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -35,12 +23,6 @@ "jsx": "preserve", "incremental": true }, - "include": [ - "next-env.d.ts", - "**/*.ts", - "**/*.tsx", - ], - "exclude": [ - "node_modules" - ] -} \ No newline at end of file + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] +} diff --git a/helm-charts/README.md b/helm-charts/README.md index 097464028..468275e3f 100644 --- a/helm-charts/README.md +++ b/helm-charts/README.md @@ -14,3 +14,4 @@ helm install infisical-helm-charts/ #### Available chart names - infisical +- secrets-operator diff --git a/helm-charts/infisical/Chart.yaml b/helm-charts/infisical/Chart.yaml index ecef711f1..3b56dcfc2 100644 --- a/helm-charts/infisical/Chart.yaml +++ b/helm-charts/infisical/Chart.yaml @@ -7,7 +7,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.0 +version: 0.1.6 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to diff --git a/helm-charts/infisical/templates/backend-deployment.yaml b/helm-charts/infisical/templates/backend-deployment.yaml index 9bc72eaf5..437995e58 100644 --- a/helm-charts/infisical/templates/backend-deployment.yaml +++ b/helm-charts/infisical/templates/backend-deployment.yaml @@ -20,13 +20,18 @@ spec: imagePullPolicy: {{ .Values.backend.image.pullPolicy }} ports: - containerPort: 4000 + {{- if .Values.backend.kubeSecretRef }} + envFrom: + - secretRef: + name: {{ .Values.backend.kubeSecretRef }} + {{- end }} env: - {{- range $key, $value := .Values.secrets }} - {{- if eq $value "MUST_REPLACE" }} + {{- range $key, $value := .Values.backendEnvironmentVariables }} + {{- if $value | quote | eq "MUST_REPLACE" }} {{ fail "Environment variables are not set. Please set all environment variables to continue." }} {{ end }} - name: {{ $key }} - value: {{ $value }} + value: {{ quote $value }} {{- end }} --- diff --git a/helm-charts/infisical/templates/frontend-deployment.yaml b/helm-charts/infisical/templates/frontend-deployment.yaml index e0dbdae0d..14be95506 100644 --- a/helm-charts/infisical/templates/frontend-deployment.yaml +++ b/helm-charts/infisical/templates/frontend-deployment.yaml @@ -18,8 +18,23 @@ spec: - name: frontend image: infisical/frontend imagePullPolicy: {{ .Values.frontend.image.pullPolicy }} + {{- if .Values.frontend.kubeSecretRef }} + envFrom: + - secretRef: + name: {{ .Values.frontend.kubeSecretRef }} + {{- end }} + {{- if .Values.frontendEnvironmentVariables }} + env: + {{- range $key, $value := .Values.frontendEnvironmentVariables }} + {{- if $value | quote | eq "MUST_REPLACE" }} + {{ fail "Environment variables are not set. Please set all environment variables to continue." }} + {{ end }} + - name: {{ $key }} + value: {{ quote $value }} + {{- end }} + {{- end }} ports: - - containerPort: 4000 + - containerPort: 3000 --- apiVersion: v1 kind: Service diff --git a/helm-charts/infisical/values.yaml b/helm-charts/infisical/values.yaml index 94bce75ef..743c1e25b 100644 --- a/helm-charts/infisical/values.yaml +++ b/helm-charts/infisical/values.yaml @@ -3,14 +3,14 @@ # PLEASE REPLACE VALUES/EDIT AS REQUIRED ##### -namespace: infisical - frontend: replicaCount: 1 image: repository: pullPolicy: IfNotPresent tag: "latest" + # kubeSecretRef: some-kube-secret-name + backend: replicaCount: 1 @@ -18,10 +18,12 @@ backend: repository: pullPolicy: IfNotPresent tag: "latest" + # kubeSecretRef: some-kube-secret-name ingress: enabled: true - annotations: {} + annotations: + kubernetes.io/ingress.class: "nginx" hostName: example.com frontend: path: / @@ -34,10 +36,10 @@ ingress: ## Complete Ingress example # ingress: # enabled: true -# annotations: +# annotations: # kubernetes.io/ingress.class: "nginx" # cert-manager.io/issuer: letsencrypt-nginx -# hostName: example.com +# hostName: k8.infisical.com # frontend: # path: / # pathType: Prefix @@ -45,17 +47,15 @@ ingress: # path: /api # pathType: Prefix # tls: -# hosts: -# - k8.infisical.com -# secretName: letsencrypt-nginx +# - secretName: letsencrypt-nginx +# hosts: +# - k8.infisical.com ### ### YOU MUST FILL IN ALL SECRETS BELOW ### -secrets: +backendEnvironmentVariables: # Required keys for platform encryption/decryption ops. Replace with nacl sk keys - PRIVATE_KEY: MUST_REPLACE - PUBLIC_KEY: MUST_REPLACE ENCRYPTION_KEY: MUST_REPLACE # JWT @@ -71,6 +71,8 @@ secrets: SMTP_USERNAME: MUST_REPLACE SMTP_PASSWORD: MUST_REPLACE - # You may replace with Mongo Cloud URI + # Recommended to replace with Mongo Cloud URI as the DB instance in the cluster does not have persistence yet MONGO_URL: mongodb://root:root@mongodb-service:27017/ + +# frontendEnvironmentVariables: \ No newline at end of file diff --git a/helm-charts/secrets-operator/.helmignore b/helm-charts/secrets-operator/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/helm-charts/secrets-operator/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/helm-charts/secrets-operator/Chart.yaml b/helm-charts/secrets-operator/Chart.yaml new file mode 100644 index 000000000..26a402da3 --- /dev/null +++ b/helm-charts/secrets-operator/Chart.yaml @@ -0,0 +1,21 @@ +apiVersion: v2 +name: secrets-operator +description: A Helm chart for Infisical secrets +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "0.1.0" diff --git a/helm-charts/secrets-operator/templates/_helpers.tpl b/helm-charts/secrets-operator/templates/_helpers.tpl new file mode 100644 index 000000000..44e464d93 --- /dev/null +++ b/helm-charts/secrets-operator/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "secrets-operator.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "secrets-operator.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "secrets-operator.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "secrets-operator.labels" -}} +helm.sh/chart: {{ include "secrets-operator.chart" . }} +{{ include "secrets-operator.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "secrets-operator.selectorLabels" -}} +app.kubernetes.io/name: {{ include "secrets-operator.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "secrets-operator.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "secrets-operator.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/helm-charts/secrets-operator/templates/deployment.yaml b/helm-charts/secrets-operator/templates/deployment.yaml new file mode 100644 index 000000000..026728bfa --- /dev/null +++ b/helm-charts/secrets-operator/templates/deployment.yaml @@ -0,0 +1,108 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "secrets-operator.fullname" . }}-controller-manager + labels: + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: k8-operator + app.kubernetes.io/part-of: k8-operator + {{- include "secrets-operator.labels" . | nindent 4 }} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "secrets-operator.fullname" . }}-controller-manager + labels: + app.kubernetes.io/component: manager + app.kubernetes.io/created-by: k8-operator + app.kubernetes.io/part-of: k8-operator + control-plane: controller-manager + {{- include "secrets-operator.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.controllerManager.replicas }} + selector: + matchLabels: + control-plane: controller-manager + {{- include "secrets-operator.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + control-plane: controller-manager + {{- include "secrets-operator.selectorLabels" . | nindent 8 }} + annotations: + kubectl.kubernetes.io/default-container: manager + spec: + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/arch + operator: In + values: + - amd64 + - arm64 + - ppc64le + - s390x + - key: kubernetes.io/os + operator: In + values: + - linux + containers: + - args: + - --secure-listen-address=0.0.0.0:8443 + - --upstream=http://127.0.0.1:8080/ + - --logtostderr=true + - --v=0 + env: + - name: KUBERNETES_CLUSTER_DOMAIN + value: {{ .Values.kubernetesClusterDomain }} + image: {{ .Values.controllerManager.kubeRbacProxy.image.repository }}:{{ .Values.controllerManager.kubeRbacProxy.image.tag + | default .Chart.AppVersion }} + name: kube-rbac-proxy + ports: + - containerPort: 8443 + name: https + protocol: TCP + resources: {{- toYaml .Values.controllerManager.kubeRbacProxy.resources | nindent + 10 }} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + - args: + - --health-probe-bind-address=:8081 + - --metrics-bind-address=127.0.0.1:8080 + - --leader-elect + command: + - /manager + env: + - name: KUBERNETES_CLUSTER_DOMAIN + value: {{ .Values.kubernetesClusterDomain }} + image: {{ .Values.controllerManager.manager.image.repository }}:{{ .Values.controllerManager.manager.image.tag + | default .Chart.AppVersion }} + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + name: manager + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: {{- toYaml .Values.controllerManager.manager.resources | nindent 10 + }} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + securityContext: + runAsNonRoot: true + serviceAccountName: {{ include "secrets-operator.fullname" . }}-controller-manager + terminationGracePeriodSeconds: 10 \ No newline at end of file diff --git a/helm-charts/secrets-operator/templates/infisicalsecret-crd.yaml b/helm-charts/secrets-operator/templates/infisicalsecret-crd.yaml new file mode 100644 index 000000000..c5b82fa14 --- /dev/null +++ b/helm-charts/secrets-operator/templates/infisicalsecret-crd.yaml @@ -0,0 +1,160 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: infisicalsecrets.secrets.infisical.com + annotations: + controller-gen.kubebuilder.io/version: v0.10.0 + labels: + {{- include "secrets-operator.labels" . | nindent 4 }} +spec: + group: secrets.infisical.com + names: + kind: InfisicalSecret + listKind: InfisicalSecretList + plural: infisicalsecrets + singular: infisicalsecret + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: InfisicalSecret is the Schema for the infisicalsecrets API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: InfisicalSecretSpec defines the desired state of InfisicalSecret + properties: + environment: + description: The Infisical environment such as dev, prod, testing + type: string + hostAPI: + default: https://app.infisical.com/api + description: Infisical host to pull secrets from + type: string + managedSecretReference: + properties: + secretName: + description: The name of the Kubernetes Secret + type: string + secretNamespace: + description: The name space where the Kubernetes Secret is located + type: string + required: + - secretName + - secretNamespace + type: object + projectId: + description: The Infisical project id + type: string + tokenSecretReference: + properties: + secretName: + description: The name of the Kubernetes Secret + type: string + secretNamespace: + description: The name space where the Kubernetes Secret is located + type: string + required: + - secretName + - secretNamespace + type: object + required: + - environment + - projectId + type: object + status: + description: InfisicalSecretStatus defines the observed state of InfisicalSecret + properties: + conditions: + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a foo's + current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating details + about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers of + specific condition types may define expected values and meanings + for this field, and whether the values are considered a guaranteed + API. The value should be a CamelCase string. This field may + not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + required: + - conditions + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] \ No newline at end of file diff --git a/helm-charts/secrets-operator/templates/leader-election-rbac.yaml b/helm-charts/secrets-operator/templates/leader-election-rbac.yaml new file mode 100644 index 000000000..dc41acf14 --- /dev/null +++ b/helm-charts/secrets-operator/templates/leader-election-rbac.yaml @@ -0,0 +1,59 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ include "secrets-operator.fullname" . }}-leader-election-role + labels: + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: k8-operator + app.kubernetes.io/part-of: k8-operator + {{- include "secrets-operator.labels" . | nindent 4 }} +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ include "secrets-operator.fullname" . }}-leader-election-rolebinding + labels: + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: k8-operator + app.kubernetes.io/part-of: k8-operator + {{- include "secrets-operator.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: '{{ include "secrets-operator.fullname" . }}-leader-election-role' +subjects: +- kind: ServiceAccount + name: '{{ include "secrets-operator.fullname" . }}-controller-manager' + namespace: '{{ .Release.Namespace }}' \ No newline at end of file diff --git a/helm-charts/secrets-operator/templates/manager-rbac.yaml b/helm-charts/secrets-operator/templates/manager-rbac.yaml new file mode 100644 index 000000000..a560790f6 --- /dev/null +++ b/helm-charts/secrets-operator/templates/manager-rbac.yaml @@ -0,0 +1,71 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "secrets-operator.fullname" . }}-manager-role + labels: + {{- include "secrets-operator.labels" . | nindent 4 }} +rules: +- apiGroups: + - "" + resources: + - secrets + verbs: + - create + - delete + - get + - list + - update + - watch +- apiGroups: + - apps + resources: + - deployments + verbs: + - get + - list + - update + - watch +- apiGroups: + - secrets.infisical.com + resources: + - infisicalsecrets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - secrets.infisical.com + resources: + - infisicalsecrets/finalizers + verbs: + - update +- apiGroups: + - secrets.infisical.com + resources: + - infisicalsecrets/status + verbs: + - get + - patch + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "secrets-operator.fullname" . }}-manager-rolebinding + labels: + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: k8-operator + app.kubernetes.io/part-of: k8-operator + {{- include "secrets-operator.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: '{{ include "secrets-operator.fullname" . }}-manager-role' +subjects: +- kind: ServiceAccount + name: '{{ include "secrets-operator.fullname" . }}-controller-manager' + namespace: '{{ .Release.Namespace }}' \ No newline at end of file diff --git a/helm-charts/secrets-operator/templates/metrics-reader-rbac.yaml b/helm-charts/secrets-operator/templates/metrics-reader-rbac.yaml new file mode 100644 index 000000000..7d7ceba46 --- /dev/null +++ b/helm-charts/secrets-operator/templates/metrics-reader-rbac.yaml @@ -0,0 +1,14 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "secrets-operator.fullname" . }}-metrics-reader + labels: + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: k8-operator + app.kubernetes.io/part-of: k8-operator + {{- include "secrets-operator.labels" . | nindent 4 }} +rules: +- nonResourceURLs: + - /metrics + verbs: + - get \ No newline at end of file diff --git a/helm-charts/secrets-operator/templates/metrics-service.yaml b/helm-charts/secrets-operator/templates/metrics-service.yaml new file mode 100644 index 000000000..ebf7ce549 --- /dev/null +++ b/helm-charts/secrets-operator/templates/metrics-service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "secrets-operator.fullname" . }}-controller-manager-metrics-service + labels: + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: k8-operator + app.kubernetes.io/part-of: k8-operator + control-plane: controller-manager + {{- include "secrets-operator.labels" . | nindent 4 }} +spec: + type: {{ .Values.metricsService.type }} + selector: + control-plane: controller-manager + {{- include "secrets-operator.selectorLabels" . | nindent 4 }} + ports: + {{- .Values.metricsService.ports | toYaml | nindent 2 -}} \ No newline at end of file diff --git a/helm-charts/secrets-operator/templates/proxy-rbac.yaml b/helm-charts/secrets-operator/templates/proxy-rbac.yaml new file mode 100644 index 000000000..5f07e2908 --- /dev/null +++ b/helm-charts/secrets-operator/templates/proxy-rbac.yaml @@ -0,0 +1,40 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "secrets-operator.fullname" . }}-proxy-role + labels: + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: k8-operator + app.kubernetes.io/part-of: k8-operator + {{- include "secrets-operator.labels" . | nindent 4 }} +rules: +- apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create +- apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "secrets-operator.fullname" . }}-proxy-rolebinding + labels: + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: k8-operator + app.kubernetes.io/part-of: k8-operator + {{- include "secrets-operator.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: '{{ include "secrets-operator.fullname" . }}-proxy-role' +subjects: +- kind: ServiceAccount + name: '{{ include "secrets-operator.fullname" . }}-controller-manager' + namespace: '{{ .Release.Namespace }}' \ No newline at end of file diff --git a/helm-charts/secrets-operator/values.yaml b/helm-charts/secrets-operator/values.yaml new file mode 100644 index 000000000..32ae2f789 --- /dev/null +++ b/helm-charts/secrets-operator/values.yaml @@ -0,0 +1,32 @@ +controllerManager: + kubeRbacProxy: + image: + repository: gcr.io/kubebuilder/kube-rbac-proxy + tag: v0.13.1 + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 5m + memory: 64Mi + manager: + image: + repository: infisical/kubernetes-operator + tag: latest + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 10m + memory: 64Mi + replicas: 1 +kubernetesClusterDomain: cluster.local +metricsService: + ports: + - name: https + port: 8443 + protocol: TCP + targetPort: https + type: ClusterIP diff --git a/k8-operator/.dockerignore b/k8-operator/.dockerignore new file mode 100644 index 000000000..0f046820f --- /dev/null +++ b/k8-operator/.dockerignore @@ -0,0 +1,4 @@ +# More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file +# Ignore build and test binaries. +bin/ +testbin/ diff --git a/k8-operator/.gitignore b/k8-operator/.gitignore new file mode 100644 index 000000000..e917e5cef --- /dev/null +++ b/k8-operator/.gitignore @@ -0,0 +1,26 @@ + +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +bin +testbin/* +Dockerfile.cross + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Kubernetes Generated files - skip generated files, except for vendored files + +!vendor/**/zz_generated.* + +# editor and IDE paraphernalia +.idea +*.swp +*.swo +*~ diff --git a/k8-operator/Dockerfile b/k8-operator/Dockerfile new file mode 100644 index 000000000..6a5d70189 --- /dev/null +++ b/k8-operator/Dockerfile @@ -0,0 +1,34 @@ +# Build the manager binary +FROM golang:1.19 as builder +ARG TARGETOS +ARG TARGETARCH + +WORKDIR /workspace +# Copy the Go Modules manifests +COPY go.mod go.mod +COPY go.sum go.sum +# cache deps before building and copying source so that we don't need to re-download as much +# and so that source changes don't invalidate our downloaded layer +RUN go mod download + +# Copy the go source +COPY main.go main.go +COPY api/ api/ +COPY controllers/ controllers/ +COPY packages/ packages/ + +# Build +# the GOARCH has not a default value to allow the binary be built according to the host where the command +# was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO +# the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, +# by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. +RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager main.go + +# Use distroless as minimal base image to package the manager binary +# Refer to https://github.com/GoogleContainerTools/distroless for more details +FROM gcr.io/distroless/static:nonroot +WORKDIR / +COPY --from=builder /workspace/manager . +USER 65532:65532 + +ENTRYPOINT ["/manager"] diff --git a/k8-operator/Makefile b/k8-operator/Makefile new file mode 100644 index 000000000..a8b056376 --- /dev/null +++ b/k8-operator/Makefile @@ -0,0 +1,168 @@ + +# Image URL to use all building/pushing image targets +IMG ?= infisical/kubernetes-operator:latest +# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. +ENVTEST_K8S_VERSION = 1.25.0 + +# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) +ifeq (,$(shell go env GOBIN)) +GOBIN=$(shell go env GOPATH)/bin +else +GOBIN=$(shell go env GOBIN) +endif + +# Setting SHELL to bash allows bash commands to be executed by recipes. +# Options are set to exit when a recipe line exits non-zero or a piped command fails. +SHELL = /usr/bin/env bash -o pipefail +.SHELLFLAGS = -ec + +.PHONY: all +all: build + +##@ General + +# The help target prints out all targets with their descriptions organized +# beneath their categories. The categories are represented by '##@' and the +# target descriptions by '##'. The awk commands is responsible for reading the +# entire set of makefiles included in this invocation, looking for lines of the +# file as xyz: ## something, and then pretty-format the target and help. Then, +# if there's a line with ##@ something, that gets pretty-printed as a category. +# More info on the usage of ANSI control characters for terminal formatting: +# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters +# More info on the awk command: +# http://linuxcommand.org/lc3_adv_awk.php + +.PHONY: help +help: ## Display this help. + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + + +## Chart +helm-chart: + $(KUSTOMIZE) build config/default | helmify ../helm-charts/secrets-operator + +## Yaml for Kubectl +kubectl-install: manifests kustomize + mkdir -p kubectl-install + cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} + $(KUSTOMIZE) build config/default > kubectl-install/install-secrets-operator.yaml + +##@ Development + +.PHONY: manifests +manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. + $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + +.PHONY: generate +generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. + $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." + +.PHONY: fmt +fmt: ## Run go fmt against code. + go fmt ./... + +.PHONY: vet +vet: ## Run go vet against code. + go vet ./... + +.PHONY: test +test: manifests generate fmt vet envtest ## Run tests. + KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test ./... -coverprofile cover.out + +##@ Build + +.PHONY: build +build: manifests generate fmt vet ## Build manager binary. + go build -o bin/manager main.go + +.PHONY: run +run: manifests generate fmt vet ## Run a controller from your host. + go run ./main.go + +# If you wish built the manager image targeting other platforms you can use the --platform flag. +# (i.e. docker build --platform linux/arm64 ). However, you must enable docker buildKit for it. +# More info: https://docs.docker.com/develop/develop-images/build_enhancements/ +.PHONY: docker-build +docker-build: test ## Build docker image with the manager. + docker build -t ${IMG} . + +.PHONY: docker-push +docker-push: ## Push docker image with the manager. + docker push ${IMG} + +# PLATFORMS defines the target platforms for the manager image be build to provide support to multiple +# architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to: +# - able to use docker buildx . More info: https://docs.docker.com/build/buildx/ +# - have enable BuildKit, More info: https://docs.docker.com/develop/develop-images/build_enhancements/ +# - be able to push the image for your registry (i.e. if you do not inform a valid value via IMG=> then the export will fail) +# To properly provided solutions that supports more than one platform you should use this option. +PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le +.PHONY: docker-buildx +docker-buildx: test ## Build and push docker image for the manager for cross-platform support + # copy existing Dockerfile and insert --platform=${BUILDPLATFORM} into Dockerfile.cross, and preserve the original Dockerfile + sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross + - docker buildx create --name project-v3-builder + docker buildx use project-v3-builder + - docker buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross . + - docker buildx rm project-v3-builder + rm Dockerfile.cross + +##@ Deployment + +ifndef ignore-not-found + ignore-not-found = false +endif + +.PHONY: install +install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. + $(KUSTOMIZE) build config/crd | kubectl apply -f - + +.PHONY: uninstall +uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. + $(KUSTOMIZE) build config/crd | kubectl delete --ignore-not-found=$(ignore-not-found) -f - + +.PHONY: deploy +deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. + cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} + $(KUSTOMIZE) build config/default | kubectl apply -f - + +.PHONY: undeploy +undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. + $(KUSTOMIZE) build config/default | kubectl delete --ignore-not-found=$(ignore-not-found) -f - + +##@ Build Dependencies + +## Location to install dependencies to +LOCALBIN ?= $(shell pwd)/bin +$(LOCALBIN): + mkdir -p $(LOCALBIN) + +## Tool Binaries +KUSTOMIZE ?= $(LOCALBIN)/kustomize +CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen +ENVTEST ?= $(LOCALBIN)/setup-envtest + +## Tool Versions +KUSTOMIZE_VERSION ?= v3.8.7 +CONTROLLER_TOOLS_VERSION ?= v0.10.0 + +KUSTOMIZE_INSTALL_SCRIPT ?= "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" +.PHONY: kustomize +kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. If wrong version is installed, it will be removed before downloading. +$(KUSTOMIZE): $(LOCALBIN) + @if test -x $(LOCALBIN)/kustomize && ! $(LOCALBIN)/kustomize version | grep -q $(KUSTOMIZE_VERSION); then \ + echo "$(LOCALBIN)/kustomize version is not expected $(KUSTOMIZE_VERSION). Removing it before installing."; \ + rm -rf $(LOCALBIN)/kustomize; \ + fi + test -s $(LOCALBIN)/kustomize || { curl -Ss $(KUSTOMIZE_INSTALL_SCRIPT) | bash -s -- $(subst v,,$(KUSTOMIZE_VERSION)) $(LOCALBIN); } + +.PHONY: controller-gen +controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. If wrong version is installed, it will be overwritten. +$(CONTROLLER_GEN): $(LOCALBIN) + test -s $(LOCALBIN)/controller-gen && $(LOCALBIN)/controller-gen --version | grep -q $(CONTROLLER_TOOLS_VERSION) || \ + GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION) + +.PHONY: envtest +envtest: $(ENVTEST) ## Download envtest-setup locally if necessary. +$(ENVTEST): $(LOCALBIN) + test -s $(LOCALBIN)/setup-envtest || GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest diff --git a/k8-operator/PROJECT b/k8-operator/PROJECT new file mode 100644 index 000000000..968a9a3d1 --- /dev/null +++ b/k8-operator/PROJECT @@ -0,0 +1,16 @@ +domain: infisical.com +layout: +- go.kubebuilder.io/v3 +projectName: k8-operator +repo: github.com/Infisical/infisical/k8-operator +resources: +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: infisical.com + group: secrets + kind: InfisicalSecret + path: github.com/Infisical/infisical/k8-operator/api/v1alpha1 + version: v1alpha1 +version: "3" diff --git a/k8-operator/README.md b/k8-operator/README.md new file mode 100644 index 000000000..807476c0c --- /dev/null +++ b/k8-operator/README.md @@ -0,0 +1,78 @@ +# k8-operator +// TODO + +## Description +// TODO + +## Getting Started +Youโ€™ll need a Kubernetes cluster to run against. You can use [KIND](https://sigs.k8s.io/kind) to get a local cluster for testing, or run against a remote cluster. +**Note:** Your controller will automatically use the current context in your kubeconfig file (i.e. whatever cluster `kubectl cluster-info` shows). + +### Running on the cluster +1. Install Instances of Custom Resources: + +```sh +kubectl apply -f config/samples/ +``` + +2. Build and push your image to the location specified by `IMG`: + +```sh +make docker-build docker-push IMG=/k8-operator:tag +``` + +3. Deploy the controller to the cluster with the image specified by `IMG`: + +```sh +make deploy IMG=/k8-operator:tag +``` + +### Uninstall CRDs +To delete the CRDs from the cluster: + +```sh +make uninstall +``` + +### Undeploy controller +UnDeploy the controller to the cluster: + +```sh +make undeploy +``` + +## Contributing +// TODO + +### How it works +This project aims to follow the Kubernetes [Operator pattern](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/) + +It uses [Controllers](https://kubernetes.io/docs/concepts/architecture/controller/) +which provides a reconcile function responsible for synchronizing resources untile the desired state is reached on the cluster + +### Test It Out +1. Install the CRDs into the cluster: + +```sh +make install +``` + +2. Run your controller (this will run in the foreground, so switch to a new terminal if you want to leave it running): + +```sh +make run +``` + +**NOTE:** You can also run this in one step by running: `make install run` + +### Modifying the API definitions +If you are editing the API definitions, generate the manifests such as CRs or CRDs using: + +```sh +make manifests +``` + +**NOTE:** Run `make --help` for more information on all potential `make` targets + +More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html) + diff --git a/k8-operator/api/v1alpha1/groupversion_info.go b/k8-operator/api/v1alpha1/groupversion_info.go new file mode 100644 index 000000000..36ebd80ce --- /dev/null +++ b/k8-operator/api/v1alpha1/groupversion_info.go @@ -0,0 +1,20 @@ +// Package v1alpha1 contains API Schema definitions for the secrets v1alpha1 API group +// +kubebuilder:object:generate=true +// +groupName=secrets.infisical.com +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "secrets.infisical.com", Version: "v1alpha1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/k8-operator/api/v1alpha1/infisicalsecret_types.go b/k8-operator/api/v1alpha1/infisicalsecret_types.go new file mode 100644 index 000000000..7940474f9 --- /dev/null +++ b/k8-operator/api/v1alpha1/infisicalsecret_types.go @@ -0,0 +1,63 @@ +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type KubeSecretReference struct { + // The name of the Kubernetes Secret + // +kubebuilder:validation:Required + SecretName string `json:"secretName"` + + // The name space where the Kubernetes Secret is located + // +kubebuilder:validation:Required + SecretNamespace string `json:"secretNamespace"` +} + +// InfisicalSecretSpec defines the desired state of InfisicalSecret +type InfisicalSecretSpec struct { + TokenSecretReference KubeSecretReference `json:"tokenSecretReference,omitempty"` + ManagedSecretReference KubeSecretReference `json:"managedSecretReference,omitempty"` + + // The Infisical project id + // +kubebuilder:validation:Required + ProjectId string `json:"projectId"` + + // The Infisical environment such as dev, prod, testing + // +kubebuilder:validation:Required + Environment string `json:"environment"` + + // Infisical host to pull secrets from + // +kubebuilder:default="https://app.infisical.com/api" + HostAPI string `json:"hostAPI,omitempty"` +} + +// InfisicalSecretStatus defines the observed state of InfisicalSecret +type InfisicalSecretStatus struct { + Conditions []metav1.Condition `json:"conditions"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// InfisicalSecret is the Schema for the infisicalsecrets API +type InfisicalSecret struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec InfisicalSecretSpec `json:"spec,omitempty"` + Status InfisicalSecretStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// InfisicalSecretList contains a list of InfisicalSecret +type InfisicalSecretList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []InfisicalSecret `json:"items"` +} + +func init() { + SchemeBuilder.Register(&InfisicalSecret{}, &InfisicalSecretList{}) +} diff --git a/k8-operator/api/v1alpha1/zz_generated.deepcopy.go b/k8-operator/api/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 000000000..af9eff318 --- /dev/null +++ b/k8-operator/api/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,140 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright 2022. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InfisicalSecret) DeepCopyInto(out *InfisicalSecret) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InfisicalSecret. +func (in *InfisicalSecret) DeepCopy() *InfisicalSecret { + if in == nil { + return nil + } + out := new(InfisicalSecret) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *InfisicalSecret) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InfisicalSecretList) DeepCopyInto(out *InfisicalSecretList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]InfisicalSecret, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InfisicalSecretList. +func (in *InfisicalSecretList) DeepCopy() *InfisicalSecretList { + if in == nil { + return nil + } + out := new(InfisicalSecretList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *InfisicalSecretList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InfisicalSecretSpec) DeepCopyInto(out *InfisicalSecretSpec) { + *out = *in + out.TokenSecretReference = in.TokenSecretReference + out.ManagedSecretReference = in.ManagedSecretReference +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InfisicalSecretSpec. +func (in *InfisicalSecretSpec) DeepCopy() *InfisicalSecretSpec { + if in == nil { + return nil + } + out := new(InfisicalSecretSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InfisicalSecretStatus) DeepCopyInto(out *InfisicalSecretStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InfisicalSecretStatus. +func (in *InfisicalSecretStatus) DeepCopy() *InfisicalSecretStatus { + if in == nil { + return nil + } + out := new(InfisicalSecretStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KubeSecretReference) DeepCopyInto(out *KubeSecretReference) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubeSecretReference. +func (in *KubeSecretReference) DeepCopy() *KubeSecretReference { + if in == nil { + return nil + } + out := new(KubeSecretReference) + in.DeepCopyInto(out) + return out +} diff --git a/k8-operator/config/crd/bases/secrets.infisical.com_infisicalsecrets.yaml b/k8-operator/config/crd/bases/secrets.infisical.com_infisicalsecrets.yaml new file mode 100644 index 000000000..c885fbdde --- /dev/null +++ b/k8-operator/config/crd/bases/secrets.infisical.com_infisicalsecrets.yaml @@ -0,0 +1,154 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.10.0 + creationTimestamp: null + name: infisicalsecrets.secrets.infisical.com +spec: + group: secrets.infisical.com + names: + kind: InfisicalSecret + listKind: InfisicalSecretList + plural: infisicalsecrets + singular: infisicalsecret + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: InfisicalSecret is the Schema for the infisicalsecrets API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: InfisicalSecretSpec defines the desired state of InfisicalSecret + properties: + environment: + description: The Infisical environment such as dev, prod, testing + type: string + hostAPI: + default: https://app.infisical.com/api + description: Infisical host to pull secrets from + type: string + managedSecretReference: + properties: + secretName: + description: The name of the Kubernetes Secret + type: string + secretNamespace: + description: The name space where the Kubernetes Secret is located + type: string + required: + - secretName + - secretNamespace + type: object + projectId: + description: The Infisical project id + type: string + tokenSecretReference: + properties: + secretName: + description: The name of the Kubernetes Secret + type: string + secretNamespace: + description: The name space where the Kubernetes Secret is located + type: string + required: + - secretName + - secretNamespace + type: object + required: + - environment + - projectId + type: object + status: + description: InfisicalSecretStatus defines the observed state of InfisicalSecret + properties: + conditions: + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + required: + - conditions + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/k8-operator/config/crd/kustomization.yaml b/k8-operator/config/crd/kustomization.yaml new file mode 100644 index 000000000..ab2a736e1 --- /dev/null +++ b/k8-operator/config/crd/kustomization.yaml @@ -0,0 +1,21 @@ +# This kustomization.yaml is not intended to be run by itself, +# since it depends on service name and namespace that are out of this kustomize package. +# It should be run by config/default +resources: +- bases/secrets.infisical.com_infisicalsecrets.yaml +#+kubebuilder:scaffold:crdkustomizeresource + +patchesStrategicMerge: +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. +# patches here are for enabling the conversion webhook for each CRD +#- patches/webhook_in_infisicalsecrets.yaml +#+kubebuilder:scaffold:crdkustomizewebhookpatch + +# [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. +# patches here are for enabling the CA injection for each CRD +#- patches/cainjection_in_infisicalsecrets.yaml +#+kubebuilder:scaffold:crdkustomizecainjectionpatch + +# the following config is for teaching kustomize how to do kustomization for CRDs. +configurations: +- kustomizeconfig.yaml diff --git a/k8-operator/config/crd/kustomizeconfig.yaml b/k8-operator/config/crd/kustomizeconfig.yaml new file mode 100644 index 000000000..ec5c150a9 --- /dev/null +++ b/k8-operator/config/crd/kustomizeconfig.yaml @@ -0,0 +1,19 @@ +# This file is for teaching kustomize how to substitute name and namespace reference in CRD +nameReference: +- kind: Service + version: v1 + fieldSpecs: + - kind: CustomResourceDefinition + version: v1 + group: apiextensions.k8s.io + path: spec/conversion/webhook/clientConfig/service/name + +namespace: +- kind: CustomResourceDefinition + version: v1 + group: apiextensions.k8s.io + path: spec/conversion/webhook/clientConfig/service/namespace + create: false + +varReference: +- path: metadata/annotations diff --git a/k8-operator/config/crd/patches/cainjection_in_infisicalsecrets.yaml b/k8-operator/config/crd/patches/cainjection_in_infisicalsecrets.yaml new file mode 100644 index 000000000..79efe831a --- /dev/null +++ b/k8-operator/config/crd/patches/cainjection_in_infisicalsecrets.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: infisicalsecrets.secrets.infisical.com diff --git a/k8-operator/config/crd/patches/webhook_in_infisicalsecrets.yaml b/k8-operator/config/crd/patches/webhook_in_infisicalsecrets.yaml new file mode 100644 index 000000000..706d26708 --- /dev/null +++ b/k8-operator/config/crd/patches/webhook_in_infisicalsecrets.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: infisicalsecrets.secrets.infisical.com +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/k8-operator/config/default/kustomization.yaml b/k8-operator/config/default/kustomization.yaml new file mode 100644 index 000000000..1237b893d --- /dev/null +++ b/k8-operator/config/default/kustomization.yaml @@ -0,0 +1,72 @@ +# Adds namespace to all resources. +namespace: infisical-operator-system + +# Value of this field is prepended to the +# names of all resources, e.g. a deployment named +# "wordpress" becomes "alices-wordpress". +# Note that it should also match with the prefix (text before '-') of the namespace +# field above. +namePrefix: infisical-operator- + +# Labels to add to all resources and selectors. +#commonLabels: +# someName: someValue + +bases: +- ../crd +- ../rbac +- ../manager +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in +# crd/kustomization.yaml +#- ../webhook +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. +#- ../certmanager +# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. +#- ../prometheus + +patchesStrategicMerge: +# Protect the /metrics endpoint by putting it behind auth. +# If you want your controller-manager to expose the /metrics +# endpoint w/o any authn/z, please comment the following line. +- manager_auth_proxy_patch.yaml + + + +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in +# crd/kustomization.yaml +#- manager_webhook_patch.yaml + +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. +# Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. +# 'CERTMANAGER' needs to be enabled to use ca injection +#- webhookcainjection_patch.yaml + +# the following config is for teaching kustomize how to do var substitution +vars: +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. +#- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR +# objref: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert # this name should match the one in certificate.yaml +# fieldref: +# fieldpath: metadata.namespace +#- name: CERTIFICATE_NAME +# objref: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert # this name should match the one in certificate.yaml +#- name: SERVICE_NAMESPACE # namespace of the service +# objref: +# kind: Service +# version: v1 +# name: webhook-service +# fieldref: +# fieldpath: metadata.namespace +#- name: SERVICE_NAME +# objref: +# kind: Service +# version: v1 +# name: webhook-service diff --git a/k8-operator/config/default/manager_auth_proxy_patch.yaml b/k8-operator/config/default/manager_auth_proxy_patch.yaml new file mode 100644 index 000000000..b75126616 --- /dev/null +++ b/k8-operator/config/default/manager_auth_proxy_patch.yaml @@ -0,0 +1,55 @@ +# This patch inject a sidecar container which is a HTTP proxy for the +# controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/arch + operator: In + values: + - amd64 + - arm64 + - ppc64le + - s390x + - key: kubernetes.io/os + operator: In + values: + - linux + containers: + - name: kube-rbac-proxy + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - "ALL" + image: gcr.io/kubebuilder/kube-rbac-proxy:v0.13.1 + args: + - "--secure-listen-address=0.0.0.0:8443" + - "--upstream=http://127.0.0.1:8080/" + - "--logtostderr=true" + - "--v=0" + ports: + - containerPort: 8443 + protocol: TCP + name: https + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 5m + memory: 64Mi + - name: manager + args: + - "--health-probe-bind-address=:8081" + - "--metrics-bind-address=127.0.0.1:8080" + - "--leader-elect" diff --git a/k8-operator/config/default/manager_config_patch.yaml b/k8-operator/config/default/manager_config_patch.yaml new file mode 100644 index 000000000..f6f589169 --- /dev/null +++ b/k8-operator/config/default/manager_config_patch.yaml @@ -0,0 +1,10 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - name: manager diff --git a/k8-operator/config/manager/kustomization.yaml b/k8-operator/config/manager/kustomization.yaml new file mode 100644 index 000000000..96ea36924 --- /dev/null +++ b/k8-operator/config/manager/kustomization.yaml @@ -0,0 +1,8 @@ +resources: +- manager.yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +images: +- name: controller + newName: infisical/kubernetes-operator + newTag: latest diff --git a/k8-operator/config/manager/manager.yaml b/k8-operator/config/manager/manager.yaml new file mode 100644 index 000000000..60ba38105 --- /dev/null +++ b/k8-operator/config/manager/manager.yaml @@ -0,0 +1,102 @@ +apiVersion: v1 +kind: Namespace +metadata: + labels: + control-plane: controller-manager + app.kubernetes.io/name: namespace + app.kubernetes.io/instance: system + app.kubernetes.io/component: manager + app.kubernetes.io/created-by: k8-operator + app.kubernetes.io/part-of: k8-operator + app.kubernetes.io/managed-by: kustomize + name: system +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system + labels: + control-plane: controller-manager + app.kubernetes.io/name: deployment + app.kubernetes.io/instance: controller-manager + app.kubernetes.io/component: manager + app.kubernetes.io/created-by: k8-operator + app.kubernetes.io/part-of: k8-operator + app.kubernetes.io/managed-by: kustomize +spec: + selector: + matchLabels: + control-plane: controller-manager + replicas: 1 + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + labels: + control-plane: controller-manager + spec: + # TODO(user): Uncomment the following code to configure the nodeAffinity expression + # according to the platforms which are supported by your solution. + # It is considered best practice to support multiple architectures. You can + # build your manager image using the makefile target docker-buildx. + # affinity: + # nodeAffinity: + # requiredDuringSchedulingIgnoredDuringExecution: + # nodeSelectorTerms: + # - matchExpressions: + # - key: kubernetes.io/arch + # operator: In + # values: + # - amd64 + # - arm64 + # - ppc64le + # - s390x + # - key: kubernetes.io/os + # operator: In + # values: + # - linux + securityContext: + runAsNonRoot: true + # TODO(user): For common cases that do not require escalating privileges + # it is recommended to ensure that all your Pods/Containers are restrictive. + # More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted + # Please uncomment the following code if your project does NOT have to work on old Kubernetes + # versions < 1.19 or on vendors versions which do NOT support this field by default (i.e. Openshift < 4.11 ). + # seccompProfile: + # type: RuntimeDefault + containers: + - command: + - /manager + args: + - --leader-elect + image: controller:latest + name: manager + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - "ALL" + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + # TODO(user): Configure the resources accordingly based on the project requirements. + # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 10m + memory: 64Mi + serviceAccountName: controller-manager + terminationGracePeriodSeconds: 10 diff --git a/k8-operator/config/prometheus/kustomization.yaml b/k8-operator/config/prometheus/kustomization.yaml new file mode 100644 index 000000000..ed137168a --- /dev/null +++ b/k8-operator/config/prometheus/kustomization.yaml @@ -0,0 +1,2 @@ +resources: +- monitor.yaml diff --git a/k8-operator/config/prometheus/monitor.yaml b/k8-operator/config/prometheus/monitor.yaml new file mode 100644 index 000000000..2f3526185 --- /dev/null +++ b/k8-operator/config/prometheus/monitor.yaml @@ -0,0 +1,26 @@ + +# Prometheus Monitor Service (Metrics) +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + labels: + control-plane: controller-manager + app.kubernetes.io/name: servicemonitor + app.kubernetes.io/instance: controller-manager-metrics-monitor + app.kubernetes.io/component: metrics + app.kubernetes.io/created-by: k8-operator + app.kubernetes.io/part-of: k8-operator + app.kubernetes.io/managed-by: kustomize + name: controller-manager-metrics-monitor + namespace: system +spec: + endpoints: + - path: /metrics + port: https + scheme: https + bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token + tlsConfig: + insecureSkipVerify: true + selector: + matchLabels: + control-plane: controller-manager diff --git a/k8-operator/config/rbac/auth_proxy_client_clusterrole.yaml b/k8-operator/config/rbac/auth_proxy_client_clusterrole.yaml new file mode 100644 index 000000000..fc7ce7735 --- /dev/null +++ b/k8-operator/config/rbac/auth_proxy_client_clusterrole.yaml @@ -0,0 +1,16 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: metrics-reader + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: k8-operator + app.kubernetes.io/part-of: k8-operator + app.kubernetes.io/managed-by: kustomize + name: metrics-reader +rules: +- nonResourceURLs: + - "/metrics" + verbs: + - get diff --git a/k8-operator/config/rbac/auth_proxy_role.yaml b/k8-operator/config/rbac/auth_proxy_role.yaml new file mode 100644 index 000000000..7b0469d27 --- /dev/null +++ b/k8-operator/config/rbac/auth_proxy_role.yaml @@ -0,0 +1,24 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: proxy-role + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: k8-operator + app.kubernetes.io/part-of: k8-operator + app.kubernetes.io/managed-by: kustomize + name: proxy-role +rules: +- apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create +- apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create diff --git a/k8-operator/config/rbac/auth_proxy_role_binding.yaml b/k8-operator/config/rbac/auth_proxy_role_binding.yaml new file mode 100644 index 000000000..8d9a7c035 --- /dev/null +++ b/k8-operator/config/rbac/auth_proxy_role_binding.yaml @@ -0,0 +1,19 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/name: clusterrolebinding + app.kubernetes.io/instance: proxy-rolebinding + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: k8-operator + app.kubernetes.io/part-of: k8-operator + app.kubernetes.io/managed-by: kustomize + name: proxy-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: proxy-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/k8-operator/config/rbac/auth_proxy_service.yaml b/k8-operator/config/rbac/auth_proxy_service.yaml new file mode 100644 index 000000000..5a1c43b6d --- /dev/null +++ b/k8-operator/config/rbac/auth_proxy_service.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + control-plane: controller-manager + app.kubernetes.io/name: service + app.kubernetes.io/instance: controller-manager-metrics-service + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: k8-operator + app.kubernetes.io/part-of: k8-operator + app.kubernetes.io/managed-by: kustomize + name: controller-manager-metrics-service + namespace: system +spec: + ports: + - name: https + port: 8443 + protocol: TCP + targetPort: https + selector: + control-plane: controller-manager diff --git a/k8-operator/config/rbac/infisicalsecret_editor_role.yaml b/k8-operator/config/rbac/infisicalsecret_editor_role.yaml new file mode 100644 index 000000000..7107057b5 --- /dev/null +++ b/k8-operator/config/rbac/infisicalsecret_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit infisicalsecrets. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: infisicalsecret-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: k8-operator + app.kubernetes.io/part-of: k8-operator + app.kubernetes.io/managed-by: kustomize + name: infisicalsecret-editor-role +rules: +- apiGroups: + - secrets.infisical.com + resources: + - infisicalsecrets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - secrets.infisical.com + resources: + - infisicalsecrets/status + verbs: + - get diff --git a/k8-operator/config/rbac/infisicalsecret_viewer_role.yaml b/k8-operator/config/rbac/infisicalsecret_viewer_role.yaml new file mode 100644 index 000000000..ead9de98a --- /dev/null +++ b/k8-operator/config/rbac/infisicalsecret_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view infisicalsecrets. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: infisicalsecret-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: k8-operator + app.kubernetes.io/part-of: k8-operator + app.kubernetes.io/managed-by: kustomize + name: infisicalsecret-viewer-role +rules: +- apiGroups: + - secrets.infisical.com + resources: + - infisicalsecrets + verbs: + - get + - list + - watch +- apiGroups: + - secrets.infisical.com + resources: + - infisicalsecrets/status + verbs: + - get diff --git a/k8-operator/config/rbac/kustomization.yaml b/k8-operator/config/rbac/kustomization.yaml new file mode 100644 index 000000000..731832a6a --- /dev/null +++ b/k8-operator/config/rbac/kustomization.yaml @@ -0,0 +1,18 @@ +resources: +# All RBAC will be applied under this service account in +# the deployment namespace. You may comment out this resource +# if your manager will use a service account that exists at +# runtime. Be sure to update RoleBinding and ClusterRoleBinding +# subjects if changing service account names. +- service_account.yaml +- role.yaml +- role_binding.yaml +- leader_election_role.yaml +- leader_election_role_binding.yaml +# Comment the following 4 lines if you want to disable +# the auth proxy (https://github.com/brancz/kube-rbac-proxy) +# which protects your /metrics endpoint. +- auth_proxy_service.yaml +- auth_proxy_role.yaml +- auth_proxy_role_binding.yaml +- auth_proxy_client_clusterrole.yaml diff --git a/k8-operator/config/rbac/leader_election_role.yaml b/k8-operator/config/rbac/leader_election_role.yaml new file mode 100644 index 000000000..d1174d650 --- /dev/null +++ b/k8-operator/config/rbac/leader_election_role.yaml @@ -0,0 +1,44 @@ +# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/name: role + app.kubernetes.io/instance: leader-election-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: k8-operator + app.kubernetes.io/part-of: k8-operator + app.kubernetes.io/managed-by: kustomize + name: leader-election-role +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch diff --git a/k8-operator/config/rbac/leader_election_role_binding.yaml b/k8-operator/config/rbac/leader_election_role_binding.yaml new file mode 100644 index 000000000..5202c011f --- /dev/null +++ b/k8-operator/config/rbac/leader_election_role_binding.yaml @@ -0,0 +1,19 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/name: rolebinding + app.kubernetes.io/instance: leader-election-rolebinding + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: k8-operator + app.kubernetes.io/part-of: k8-operator + app.kubernetes.io/managed-by: kustomize + name: leader-election-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: leader-election-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/k8-operator/config/rbac/role.yaml b/k8-operator/config/rbac/role.yaml new file mode 100644 index 000000000..0aa618262 --- /dev/null +++ b/k8-operator/config/rbac/role.yaml @@ -0,0 +1,53 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + creationTimestamp: null + name: manager-role +rules: +- apiGroups: + - "" + resources: + - secrets + verbs: + - create + - delete + - get + - list + - update + - watch +- apiGroups: + - apps + resources: + - deployments + verbs: + - get + - list + - update + - watch +- apiGroups: + - secrets.infisical.com + resources: + - infisicalsecrets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - secrets.infisical.com + resources: + - infisicalsecrets/finalizers + verbs: + - update +- apiGroups: + - secrets.infisical.com + resources: + - infisicalsecrets/status + verbs: + - get + - patch + - update diff --git a/k8-operator/config/rbac/role_binding.yaml b/k8-operator/config/rbac/role_binding.yaml new file mode 100644 index 000000000..62aee486d --- /dev/null +++ b/k8-operator/config/rbac/role_binding.yaml @@ -0,0 +1,19 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/name: clusterrolebinding + app.kubernetes.io/instance: manager-rolebinding + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: k8-operator + app.kubernetes.io/part-of: k8-operator + app.kubernetes.io/managed-by: kustomize + name: manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: manager-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/k8-operator/config/rbac/service_account.yaml b/k8-operator/config/rbac/service_account.yaml new file mode 100644 index 000000000..da689a67d --- /dev/null +++ b/k8-operator/config/rbac/service_account.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/name: serviceaccount + app.kubernetes.io/instance: controller-manager + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: k8-operator + app.kubernetes.io/part-of: k8-operator + app.kubernetes.io/managed-by: kustomize + name: controller-manager + namespace: system diff --git a/k8-operator/config/samples/sample.yaml b/k8-operator/config/samples/sample.yaml new file mode 100644 index 000000000..ad352d3e7 --- /dev/null +++ b/k8-operator/config/samples/sample.yaml @@ -0,0 +1,13 @@ +apiVersion: secrets.infisical.com/v1alpha1 +kind: InfisicalSecret +metadata: + name: infisicalsecret-sample +spec: + projectId: 62faf98ae0b05e8529b5da46 + environment: dev + tokenSecretReference: + secretName: service-token + secretNamespace: first-project + managedSecretReference: + secretName: managed-secret + secretNamespace: first-project diff --git a/k8-operator/controllers/infisicalsecret_controller.go b/k8-operator/controllers/infisicalsecret_controller.go new file mode 100644 index 000000000..f6422e177 --- /dev/null +++ b/k8-operator/controllers/infisicalsecret_controller.go @@ -0,0 +1,78 @@ +package controllers + +import ( + "context" + "time" + + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + "github.com/Infisical/infisical/k8-operator/api/v1alpha1" + secretsv1alpha1 "github.com/Infisical/infisical/k8-operator/api/v1alpha1" +) + +// InfisicalSecretReconciler reconciles a InfisicalSecret object +type InfisicalSecretReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +//+kubebuilder:rbac:groups=secrets.infisical.com,resources=infisicalsecrets,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=secrets.infisical.com,resources=infisicalsecrets/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=secrets.infisical.com,resources=infisicalsecrets/finalizers,verbs=update +//+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;delete +//+kubebuilder:rbac:groups=apps,resources=deployments,verbs=list;watch;get;update + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.13.1/pkg/reconcile +func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := log.FromContext(ctx) + + var infisicalSecretCR v1alpha1.InfisicalSecret + err := r.Get(ctx, req.NamespacedName, &infisicalSecretCR) + + requeueTime := time.Minute * 5 + + if err != nil { + if errors.IsNotFound(err) { + log.Info("Infisical Secret not found") + return ctrl.Result{}, nil + } else { + log.Error(err, "Unable to fetch Infisical Secret from cluster. Will retry") + return ctrl.Result{ + RequeueAfter: requeueTime, + }, nil + } + } + + // Check if the resource is already marked for deletion + if infisicalSecretCR.GetDeletionTimestamp() != nil { + return ctrl.Result{}, nil + } + + err = r.ReconcileInfisicalSecret(ctx, infisicalSecretCR) + r.SetReadyToSyncSecretsConditions(ctx, &infisicalSecretCR, err) + if err != nil { + log.Error(err, "Unable to reconcile Infisical Secret and will try again") + return ctrl.Result{ + RequeueAfter: requeueTime, + }, nil + } + + // Sync again after the specified time + return ctrl.Result{ + RequeueAfter: requeueTime, + }, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *InfisicalSecretReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&secretsv1alpha1.InfisicalSecret{}). // TODO we should also be watching secrets with the name specifed + Complete(r) +} diff --git a/k8-operator/controllers/infisicalsecret_helper.go b/k8-operator/controllers/infisicalsecret_helper.go new file mode 100644 index 000000000..be4e35dd5 --- /dev/null +++ b/k8-operator/controllers/infisicalsecret_helper.go @@ -0,0 +1,173 @@ +package controllers + +import ( + "context" + "fmt" + "strings" + + "github.com/Infisical/infisical/k8-operator/api/v1alpha1" + api "github.com/Infisical/infisical/k8-operator/packages/api" + models "github.com/Infisical/infisical/k8-operator/packages/models" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +const INFISICAL_TOKEN_SECRET_KEY_NAME = "infisicalToken" + +func (r *InfisicalSecretReconciler) GetKubeSecretByNamespacedName(ctx context.Context, namespacedName types.NamespacedName) (*corev1.Secret, error) { + kubeSecret := &corev1.Secret{} + err := r.Client.Get(ctx, namespacedName, kubeSecret) + if err != nil { + kubeSecret = nil + } + + return kubeSecret, err +} + +func (r *InfisicalSecretReconciler) GetInfisicalToken(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret) (string, error) { + tokenSecret, err := r.GetKubeSecretByNamespacedName(ctx, types.NamespacedName{ + Namespace: infisicalSecret.Spec.TokenSecretReference.SecretNamespace, + Name: infisicalSecret.Spec.TokenSecretReference.SecretName, + }) + + if err != nil { + return "", fmt.Errorf("failed to read Infisical token secret from secret named [%s] in namespace [%s]: with error [%w]", infisicalSecret.Spec.ManagedSecretReference.SecretName, infisicalSecret.Spec.ManagedSecretReference.SecretNamespace, err) + } + + infisicalServiceToken := tokenSecret.Data[INFISICAL_TOKEN_SECRET_KEY_NAME] + if infisicalServiceToken == nil { + return "", fmt.Errorf("the Infisical token is not set in the Kubernetes secret. Please add the key [%s] with the corresponding token value", INFISICAL_TOKEN_SECRET_KEY_NAME) + } + + return strings.Replace(string(infisicalServiceToken), " ", "", -1), nil +} + +func (r *InfisicalSecretReconciler) CreateInfisicalManagedKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret, secretsFromAPI []models.SingleEnvironmentVariable) error { + plainProcessedSecrets := make(map[string][]byte) + for _, secret := range secretsFromAPI { + plainProcessedSecrets[secret.Key] = []byte(secret.Value) // plain process + } + + // create a new secret as specified by the managed secret spec of CRD + newKubeSecretInstance := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: infisicalSecret.Spec.ManagedSecretReference.SecretName, + Namespace: infisicalSecret.Spec.ManagedSecretReference.SecretNamespace, + }, + Type: "Opaque", + Data: plainProcessedSecrets, + } + + err := r.Client.Create(ctx, newKubeSecretInstance) + if err != nil { + return fmt.Errorf("unable to create the managed Kubernetes secret : %w", err) + } + + fmt.Println("Successfully created a managed Kubernetes secret with your Infisical secrets") + return nil +} + +func (r *InfisicalSecretReconciler) UpdateInfisicalManagedKubeSecret(ctx context.Context, managedKubeSecret corev1.Secret, secretsFromAPI []models.SingleEnvironmentVariable) error { + plainProcessedSecrets := make(map[string][]byte) + for _, secret := range secretsFromAPI { + plainProcessedSecrets[secret.Key] = []byte(secret.Value) + } + + managedKubeSecret.Data = plainProcessedSecrets + err := r.Client.Update(ctx, &managedKubeSecret) + if err != nil { + return fmt.Errorf("unable to update Kubernetes secret because [%w]", err) + } + + fmt.Println("successfully updated managed Kubernetes secret") + return nil +} + +func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret) error { + infisicalToken, err := r.GetInfisicalToken(ctx, infisicalSecret) + r.SetInfisicalTokenLoadCondition(ctx, &infisicalSecret, err) + if err != nil { + return fmt.Errorf("unable to load Infisical Token from the specified Kubernetes secret with error [%w]", err) + } + + managedKubeSecret, err := r.GetKubeSecretByNamespacedName(ctx, types.NamespacedName{ + Name: infisicalSecret.Spec.ManagedSecretReference.SecretName, + Namespace: infisicalSecret.Spec.ManagedSecretReference.SecretNamespace, + }) + + if err != nil && !errors.IsNotFound(err) { + return fmt.Errorf("something went wrong when fetching the managed Kubernetes secret [%w]", err) + } + + secretsFromApi, err := api.GetAllEnvironmentVariables(infisicalSecret.Spec.ProjectId, infisicalSecret.Spec.Environment, infisicalToken, infisicalSecret.Spec.HostAPI) + + if err != nil { + return err + } + + if managedKubeSecret == nil { + return r.CreateInfisicalManagedKubeSecret(ctx, infisicalSecret, secretsFromApi) + } else { + return r.UpdateInfisicalManagedKubeSecret(ctx, *managedKubeSecret, secretsFromApi) + } + +} + +// Conditions + +func (r *InfisicalSecretReconciler) SetReadyToSyncSecretsConditions(ctx context.Context, infisicalSecret *v1alpha1.InfisicalSecret, errorToConditionOn error) { + if infisicalSecret.Status.Conditions == nil { + infisicalSecret.Status.Conditions = []metav1.Condition{} + } + + if errorToConditionOn == nil { + meta.SetStatusCondition(&infisicalSecret.Status.Conditions, metav1.Condition{ + Type: "secrets.infisical.com/ReadyToSyncSecrets", + Status: metav1.ConditionTrue, + Reason: "OK", + Message: "Infisical controller has started syncing your secrets", + }) + } else { + meta.SetStatusCondition(&infisicalSecret.Status.Conditions, metav1.Condition{ + Type: "secrets.infisical.com/ReadyToSyncSecrets", + Status: metav1.ConditionFalse, + Reason: "Error", + Message: fmt.Sprintf("Failed to update secret because: %v", errorToConditionOn), + }) + } + + err := r.Client.Status().Update(ctx, infisicalSecret) + if err != nil { + fmt.Println("Could not set condition", err) + } +} + +func (r *InfisicalSecretReconciler) SetInfisicalTokenLoadCondition(ctx context.Context, infisicalSecret *v1alpha1.InfisicalSecret, errorToConditionOn error) { + if infisicalSecret.Status.Conditions == nil { + infisicalSecret.Status.Conditions = []metav1.Condition{} + } + + if errorToConditionOn == nil { + meta.SetStatusCondition(&infisicalSecret.Status.Conditions, metav1.Condition{ + Type: "secrets.infisical.com/LoadedInfisicalToken", + Status: metav1.ConditionTrue, + Reason: "OK", + Message: "Infisical controller has located the Infisical token in provided Kubernetes secret", + }) + } else { + meta.SetStatusCondition(&infisicalSecret.Status.Conditions, metav1.Condition{ + Type: "secrets.infisical.com/LoadedInfisicalToken", + Status: metav1.ConditionFalse, + Reason: "Error", + Message: fmt.Sprintf("Failed to load Infisical Token because: %v", errorToConditionOn), + }) + } + + err := r.Client.Status().Update(ctx, infisicalSecret) + if err != nil { + fmt.Println("Could not set condition for LoadedInfisicalToken") + } +} diff --git a/k8-operator/controllers/suite_test.go b/k8-operator/controllers/suite_test.go new file mode 100644 index 000000000..bd46b22e0 --- /dev/null +++ b/k8-operator/controllers/suite_test.go @@ -0,0 +1,64 @@ +package controllers + +import ( + "path/filepath" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + + secretsv1alpha1 "github.com/Infisical/infisical/k8-operator/api/v1alpha1" + //+kubebuilder:scaffold:imports +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +var cfg *rest.Config +var k8sClient client.Client +var testEnv *envtest.Environment + +func TestAPIs(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "Controller Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, + ErrorIfCRDPathMissing: true, + } + + var err error + // cfg is defined in this file globally. + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + err = secretsv1alpha1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + //+kubebuilder:scaffold:scheme + + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient).NotTo(BeNil()) + +}) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +}) diff --git a/k8-operator/go.mod b/k8-operator/go.mod new file mode 100644 index 000000000..023cc9da7 --- /dev/null +++ b/k8-operator/go.mod @@ -0,0 +1,82 @@ +module github.com/Infisical/infisical/k8-operator + +go 1.19 + +require ( + github.com/onsi/ginkgo/v2 v2.1.4 + github.com/onsi/gomega v1.19.0 + k8s.io/apimachinery v0.25.0 + k8s.io/client-go v0.25.0 + sigs.k8s.io/controller-runtime v0.13.1 +) + +require ( + cloud.google.com/go v0.97.0 // indirect + github.com/Azure/go-autorest v14.2.0+incompatible // indirect + github.com/Azure/go-autorest/autorest v0.11.27 // indirect + github.com/Azure/go-autorest/autorest/adal v0.9.20 // indirect + github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect + github.com/Azure/go-autorest/logger v0.2.1 // indirect + github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/PuerkitoBio/purell v1.1.1 // indirect + github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/emicklei/go-restful/v3 v3.8.0 // indirect + github.com/evanphx/json-patch/v5 v5.6.0 // indirect + github.com/fsnotify/fsnotify v1.5.4 // indirect + github.com/go-logr/logr v1.2.3 // indirect + github.com/go-logr/zapr v1.2.3 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.19.5 // indirect + github.com/go-openapi/swag v0.19.14 // indirect + github.com/go-resty/resty/v2 v2.7.0 + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v4 v4.2.0 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/google/gnostic v0.5.7-v3refs // indirect + github.com/google/go-cmp v0.5.8 // indirect + github.com/google/gofuzz v1.1.0 // indirect + github.com/google/uuid v1.1.2 // indirect + github.com/imdario/mergo v0.3.12 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/mailru/easyjson v0.7.6 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_golang v1.12.2 // indirect + github.com/prometheus/client_model v0.2.0 // indirect + github.com/prometheus/common v0.32.1 // indirect + github.com/prometheus/procfs v0.7.3 // indirect + github.com/spf13/pflag v1.0.5 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + go.uber.org/zap v1.21.0 // indirect + golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd + golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect + golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect + golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect + golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect + golang.org/x/text v0.3.7 // indirect + golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect + gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.28.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/api v0.25.0 + k8s.io/apiextensions-apiserver v0.25.0 // indirect + k8s.io/component-base v0.25.0 // indirect + k8s.io/klog/v2 v2.70.1 // indirect + k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1 // indirect + k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed // indirect + sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect + sigs.k8s.io/yaml v1.3.0 // indirect +) diff --git a/k8-operator/go.sum b/k8-operator/go.sum new file mode 100644 index 000000000..a0d400d6d --- /dev/null +++ b/k8-operator/go.sum @@ -0,0 +1,799 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= +cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= +cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= +cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= +cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= +cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= +cloud.google.com/go v0.97.0 h1:3DXvAyifywvq64LfkKaMOmkWPS1CikIQdMe2lY9vxU8= +cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/autorest v0.11.27 h1:F3R3q42aWytozkV8ihzcgMO4OA4cuqr3bNlsEuF6//A= +github.com/Azure/go-autorest/autorest v0.11.27/go.mod h1:7l8ybrIdUmGqZMTD0sRtAr8NvbHjfofbf8RSP2q7w7U= +github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ= +github.com/Azure/go-autorest/autorest/adal v0.9.20 h1:gJ3E98kMpFB1MFqQCvA1yFab8vthOeD4VlFRQULxahg= +github.com/Azure/go-autorest/autorest/adal v0.9.20/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ= +github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= +github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= +github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/autorest/mocks v0.4.2 h1:PGN4EDXnuQbojHbU0UWoNvmu9AGVwYHG9/fkDYhtAfw= +github.com/Azure/go-autorest/autorest/mocks v0.4.2/go.mod h1:Vy7OitM9Kei0i1Oj+LvyAWMXJHeKH1MVlzFugfVrmyU= +github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= +github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= +github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/emicklei/go-restful/v3 v3.8.0 h1:eCZ8ulSerjdAiaNpF7GxXIE7ZCMo1moN1qX+S609eVw= +github.com/emicklei/go-restful/v3 v3.8.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= +github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= +github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= +github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= +github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/zapr v1.2.3 h1:a9vnzlIBPQBBkeaR9IuMUfmVOrQlkoC4YfPoFkX3T7A= +github.com/go-logr/zapr v1.2.3/go.mod h1:eIauM6P8qSvTw5o2ez6UEAfGjQKrxQTl5EoK+Qa2oG4= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.19.5 h1:1WJP/wi4OjB4iV8KVbH73rQaoialJrqv8gitZLxGLtM= +github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng= +github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY= +github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU= +github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54= +github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= +github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= +github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo/v2 v2.1.4 h1:GNapqRSid3zijZ9H77KrgVG4/8KqiyRsxcSxe+7ApXY= +github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU= +github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.12.2 h1:51L9cDoUHVrXx4zWYlcLQIZ+d+VXHgqnYKkIuq4g/34= +github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= +github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= +github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= +go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= +go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd h1:XcWmESyNjXJMLahc3mqVQJcgSTDxFxhETVlfk9uGc38= +golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 h1:RerP+noqYHUQ8CMRcPlC2nvTa4dcBIjegkuWdcUDuqg= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20220609170525-579cf78fd858 h1:Dpdu/EMxGMFgq0CeYMh4fazTD2vtlZRYE7wyynxJb9U= +golang.org/x/time v0.0.0-20220609170525-579cf78fd858/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.2.0 h1:4pT439QV83L+G9FkcCriY6EkpcK6r6bK+A5FBUMI7qY= +gomodules.xyz/jsonpatch/v2 v2.2.0/go.mod h1:WXp+iVDkoLQqPudfQ9GBlwB2eZ5DKOnjQZCYdOS8GPY= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= +google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= +google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= +google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= +google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= +google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= +google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +k8s.io/api v0.25.0 h1:H+Q4ma2U/ww0iGB78ijZx6DRByPz6/733jIuFpX70e0= +k8s.io/api v0.25.0/go.mod h1:ttceV1GyV1i1rnmvzT3BST08N6nGt+dudGrquzVQWPk= +k8s.io/apiextensions-apiserver v0.25.0 h1:CJ9zlyXAbq0FIW8CD7HHyozCMBpDSiH7EdrSTCZcZFY= +k8s.io/apiextensions-apiserver v0.25.0/go.mod h1:3pAjZiN4zw7R8aZC5gR0y3/vCkGlAjCazcg1me8iB/E= +k8s.io/apimachinery v0.25.0 h1:MlP0r6+3XbkUG2itd6vp3oxbtdQLQI94fD5gCS+gnoU= +k8s.io/apimachinery v0.25.0/go.mod h1:qMx9eAk0sZQGsXGu86fab8tZdffHbwUfsvzqKn4mfB0= +k8s.io/client-go v0.25.0 h1:CVWIaCETLMBNiTUta3d5nzRbXvY5Hy9Dpl+VvREpu5E= +k8s.io/client-go v0.25.0/go.mod h1:lxykvypVfKilxhTklov0wz1FoaUZ8X4EwbhS6rpRfN8= +k8s.io/component-base v0.25.0 h1:haVKlLkPCFZhkcqB6WCvpVxftrg6+FK5x1ZuaIDaQ5Y= +k8s.io/component-base v0.25.0/go.mod h1:F2Sumv9CnbBlqrpdf7rKZTmmd2meJq0HizeyY/yAFxk= +k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= +k8s.io/klog/v2 v2.70.1 h1:7aaoSdahviPmR+XkS7FyxlkkXs6tHISSG03RxleQAVQ= +k8s.io/klog/v2 v2.70.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1 h1:MQ8BAZPZlWk3S9K4a9NCkIFQtZShWqoha7snGixVgEA= +k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1/go.mod h1:C/N6wCaBHeBHkHUesQOQy2/MZqGgMAFPqGsGQLdbZBU= +k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed h1:jAne/RjBTyawwAy0utX5eqigAwz/lQhTmy+Hr/Cpue4= +k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/controller-runtime v0.13.1 h1:tUsRCSJVM1QQOOeViGeX3GMT3dQF1eePPw6sEE3xSlg= +sigs.k8s.io/controller-runtime v0.13.1/go.mod h1:Zbz+el8Yg31jubvAEyglRZGdLAjplZl+PgtYNI6WNTI= +sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 h1:iXTIw73aPyC+oRdyqqvVJuloN1p0AC/kzH07hu3NE+k= +sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/k8-operator/hack/boilerplate.go.txt b/k8-operator/hack/boilerplate.go.txt new file mode 100644 index 000000000..29c55ecda --- /dev/null +++ b/k8-operator/hack/boilerplate.go.txt @@ -0,0 +1,15 @@ +/* +Copyright 2022. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ \ No newline at end of file diff --git a/k8-operator/kubectl-install/install-secrets-operator.yaml b/k8-operator/kubectl-install/install-secrets-operator.yaml new file mode 100644 index 000000000..6a3cb8e6d --- /dev/null +++ b/k8-operator/kubectl-install/install-secrets-operator.yaml @@ -0,0 +1,475 @@ +apiVersion: v1 +kind: Namespace +metadata: + labels: + app.kubernetes.io/component: manager + app.kubernetes.io/created-by: k8-operator + app.kubernetes.io/instance: system + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: namespace + app.kubernetes.io/part-of: k8-operator + control-plane: controller-manager + name: infisical-operator-system +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.10.0 + creationTimestamp: null + name: infisicalsecrets.secrets.infisical.com +spec: + group: secrets.infisical.com + names: + kind: InfisicalSecret + listKind: InfisicalSecretList + plural: infisicalsecrets + singular: infisicalsecret + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: InfisicalSecret is the Schema for the infisicalsecrets API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: InfisicalSecretSpec defines the desired state of InfisicalSecret + properties: + environment: + description: The Infisical environment such as dev, prod, testing + type: string + hostAPI: + default: https://app.infisical.com/api + description: Infisical host to pull secrets from + type: string + managedSecretReference: + properties: + secretName: + description: The name of the Kubernetes Secret + type: string + secretNamespace: + description: The name space where the Kubernetes Secret is located + type: string + required: + - secretName + - secretNamespace + type: object + projectId: + description: The Infisical project id + type: string + tokenSecretReference: + properties: + secretName: + description: The name of the Kubernetes Secret + type: string + secretNamespace: + description: The name space where the Kubernetes Secret is located + type: string + required: + - secretName + - secretNamespace + type: object + required: + - environment + - projectId + type: object + status: + description: InfisicalSecretStatus defines the observed state of InfisicalSecret + properties: + conditions: + items: + description: "Condition contains details for one aspect of the current state of this API Resource. --- This struct is intended for direct use as an array at the field path .status.conditions. For example, \n type FooStatus struct{ // Represents the observations of a foo's current state. // Known .status.conditions.type are: \"Available\", \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge // +listType=map // +listMapKey=type Conditions []metav1.Condition `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. --- Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be useful (see .node.status.conditions), the ability to deconflict is important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + required: + - conditions + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: k8-operator + app.kubernetes.io/instance: controller-manager + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: serviceaccount + app.kubernetes.io/part-of: k8-operator + name: infisical-operator-controller-manager + namespace: infisical-operator-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: k8-operator + app.kubernetes.io/instance: leader-election-role + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: role + app.kubernetes.io/part-of: k8-operator + name: infisical-operator-leader-election-role + namespace: infisical-operator-system +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + creationTimestamp: null + name: infisical-operator-manager-role +rules: +- apiGroups: + - "" + resources: + - secrets + verbs: + - create + - delete + - get + - list + - update + - watch +- apiGroups: + - apps + resources: + - deployments + verbs: + - get + - list + - update + - watch +- apiGroups: + - secrets.infisical.com + resources: + - infisicalsecrets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - secrets.infisical.com + resources: + - infisicalsecrets/finalizers + verbs: + - update +- apiGroups: + - secrets.infisical.com + resources: + - infisicalsecrets/status + verbs: + - get + - patch + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: k8-operator + app.kubernetes.io/instance: metrics-reader + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: clusterrole + app.kubernetes.io/part-of: k8-operator + name: infisical-operator-metrics-reader +rules: +- nonResourceURLs: + - /metrics + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: k8-operator + app.kubernetes.io/instance: proxy-role + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: clusterrole + app.kubernetes.io/part-of: k8-operator + name: infisical-operator-proxy-role +rules: +- apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create +- apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: k8-operator + app.kubernetes.io/instance: leader-election-rolebinding + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: rolebinding + app.kubernetes.io/part-of: k8-operator + name: infisical-operator-leader-election-rolebinding + namespace: infisical-operator-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: infisical-operator-leader-election-role +subjects: +- kind: ServiceAccount + name: infisical-operator-controller-manager + namespace: infisical-operator-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: k8-operator + app.kubernetes.io/instance: manager-rolebinding + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: clusterrolebinding + app.kubernetes.io/part-of: k8-operator + name: infisical-operator-manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: infisical-operator-manager-role +subjects: +- kind: ServiceAccount + name: infisical-operator-controller-manager + namespace: infisical-operator-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: k8-operator + app.kubernetes.io/instance: proxy-rolebinding + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: clusterrolebinding + app.kubernetes.io/part-of: k8-operator + name: infisical-operator-proxy-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: infisical-operator-proxy-role +subjects: +- kind: ServiceAccount + name: infisical-operator-controller-manager + namespace: infisical-operator-system +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: k8-operator + app.kubernetes.io/instance: controller-manager-metrics-service + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: service + app.kubernetes.io/part-of: k8-operator + control-plane: controller-manager + name: infisical-operator-controller-manager-metrics-service + namespace: infisical-operator-system +spec: + ports: + - name: https + port: 8443 + protocol: TCP + targetPort: https + selector: + control-plane: controller-manager +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app.kubernetes.io/component: manager + app.kubernetes.io/created-by: k8-operator + app.kubernetes.io/instance: controller-manager + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: deployment + app.kubernetes.io/part-of: k8-operator + control-plane: controller-manager + name: infisical-operator-controller-manager + namespace: infisical-operator-system +spec: + replicas: 1 + selector: + matchLabels: + control-plane: controller-manager + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + labels: + control-plane: controller-manager + spec: + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/arch + operator: In + values: + - amd64 + - arm64 + - ppc64le + - s390x + - key: kubernetes.io/os + operator: In + values: + - linux + containers: + - args: + - --secure-listen-address=0.0.0.0:8443 + - --upstream=http://127.0.0.1:8080/ + - --logtostderr=true + - --v=0 + image: gcr.io/kubebuilder/kube-rbac-proxy:v0.13.1 + name: kube-rbac-proxy + ports: + - containerPort: 8443 + name: https + protocol: TCP + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 5m + memory: 64Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + - args: + - --health-probe-bind-address=:8081 + - --metrics-bind-address=127.0.0.1:8080 + - --leader-elect + command: + - /manager + image: infisical/kubernetes-operator:latest + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + name: manager + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 10m + memory: 64Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + securityContext: + runAsNonRoot: true + serviceAccountName: infisical-operator-controller-manager + terminationGracePeriodSeconds: 10 diff --git a/k8-operator/main.go b/k8-operator/main.go new file mode 100644 index 000000000..50c0cda00 --- /dev/null +++ b/k8-operator/main.go @@ -0,0 +1,99 @@ +package main + +import ( + "flag" + "os" + + // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) + // to ensure that exec-entrypoint and run can make use of them. + _ "k8s.io/client-go/plugin/pkg/client/auth" + + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/healthz" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + + secretsv1alpha1 "github.com/Infisical/infisical/k8-operator/api/v1alpha1" + "github.com/Infisical/infisical/k8-operator/controllers" + //+kubebuilder:scaffold:imports +) + +var ( + scheme = runtime.NewScheme() + setupLog = ctrl.Log.WithName("setup") +) + +func init() { + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + + utilruntime.Must(secretsv1alpha1.AddToScheme(scheme)) + //+kubebuilder:scaffold:scheme +} + +func main() { + var metricsAddr string + var enableLeaderElection bool + var probeAddr string + flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") + flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") + flag.BoolVar(&enableLeaderElection, "leader-elect", false, + "Enable leader election for controller manager. "+ + "Enabling this will ensure there is only one active controller manager.") + opts := zap.Options{ + Development: true, + } + opts.BindFlags(flag.CommandLine) + flag.Parse() + + ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ + Scheme: scheme, + MetricsBindAddress: metricsAddr, + Port: 9443, + HealthProbeBindAddress: probeAddr, + LeaderElection: enableLeaderElection, + LeaderElectionID: "cf2b8c44.infisical.com", + // LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily + // when the Manager ends. This requires the binary to immediately end when the + // Manager is stopped, otherwise, this setting is unsafe. Setting this significantly + // speeds up voluntary leader transitions as the new leader don't have to wait + // LeaseDuration time first. + // + // In the default scaffold provided, the program ends immediately after + // the manager stops, so would be fine to enable this option. However, + // if you are doing or is intended to do any operation such as perform cleanups + // after the manager stops then its usage might be unsafe. + // LeaderElectionReleaseOnCancel: true, + }) + if err != nil { + setupLog.Error(err, "unable to start manager") + os.Exit(1) + } + + if err = (&controllers.InfisicalSecretReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "InfisicalSecret") + os.Exit(1) + } + //+kubebuilder:scaffold:builder + + if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up health check") + os.Exit(1) + } + if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up ready check") + os.Exit(1) + } + + setupLog.Info("starting manager") + if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { + setupLog.Error(err, "problem running manager") + os.Exit(1) + } +} diff --git a/k8-operator/packages/api/api.go b/k8-operator/packages/api/api.go new file mode 100644 index 000000000..64f8add46 --- /dev/null +++ b/k8-operator/packages/api/api.go @@ -0,0 +1,177 @@ +package api + +import ( + "encoding/base64" + "errors" + "fmt" + "regexp" + "strings" + + "github.com/Infisical/infisical/k8-operator/packages/crypto" + "github.com/Infisical/infisical/k8-operator/packages/models" + "github.com/go-resty/resty/v2" + "golang.org/x/crypto/nacl/box" +) + +func GetAllEnvironmentVariables(projectId string, envName string, infisicalToken string, hostAPI string) ([]models.SingleEnvironmentVariable, error) { + envsFromApi, err := GetSecretsFromAPIUsingInfisicalToken(infisicalToken, envName, projectId, hostAPI) + if err != nil { + return nil, err + } + + return SubstituteSecrets(envsFromApi), nil +} + +func GetSecretsFromAPIUsingInfisicalToken(infisicalToken string, envName string, projectId string, hostAPI string) ([]models.SingleEnvironmentVariable, error) { + if infisicalToken == "" || projectId == "" || envName == "" { + return nil, errors.New("infisical token, project id and or environment name cannot be empty") + } + + splitToken := strings.Split(infisicalToken, ",") + JTWToken := splitToken[0] + temPrivateKey := splitToken[1] + + // create http client + httpClient := resty.New(). + SetAuthToken(JTWToken). + SetHeader("Accept", "application/json") + + var pullSecretsByInfisicalTokenResponse models.PullSecretsByInfisicalTokenResponse + response, err := httpClient. + R(). + SetQueryParam("environment", envName). + SetQueryParam("channel", "cli"). + SetResult(&pullSecretsByInfisicalTokenResponse). + Get(fmt.Sprintf("%v/v1/secret/%v/service-token", hostAPI, projectId)) + + if err != nil { + return nil, err + } + + if response.StatusCode() > 299 { + return nil, fmt.Errorf(response.Status()) + } + + // Get workspace key + workspaceKey, err := base64.StdEncoding.DecodeString(pullSecretsByInfisicalTokenResponse.Key.EncryptedKey) + if err != nil { + return nil, err + } + + nonce, err := base64.StdEncoding.DecodeString(pullSecretsByInfisicalTokenResponse.Key.Nonce) + if err != nil { + return nil, err + } + + senderPublicKey, err := base64.StdEncoding.DecodeString(pullSecretsByInfisicalTokenResponse.Key.Sender.PublicKey) + if err != nil { + return nil, err + } + + currentUsersPrivateKey, err := base64.StdEncoding.DecodeString(temPrivateKey) + if err != nil { + return nil, err + } + + workspaceKeyInBytes, _ := box.Open(nil, workspaceKey, (*[24]byte)(nonce), (*[32]byte)(senderPublicKey), (*[32]byte)(currentUsersPrivateKey)) + var listOfEnv []models.SingleEnvironmentVariable + + for _, secret := range pullSecretsByInfisicalTokenResponse.Secrets { + key_iv, _ := base64.StdEncoding.DecodeString(secret.SecretKey.Iv) + key_tag, _ := base64.StdEncoding.DecodeString(secret.SecretKey.Tag) + key_ciphertext, _ := base64.StdEncoding.DecodeString(secret.SecretKey.Ciphertext) + + plainTextKey, err := crypto.DecryptSymmetric(workspaceKeyInBytes, key_ciphertext, key_tag, key_iv) + if err != nil { + return nil, err + } + + value_iv, _ := base64.StdEncoding.DecodeString(secret.SecretValue.Iv) + value_tag, _ := base64.StdEncoding.DecodeString(secret.SecretValue.Tag) + value_ciphertext, _ := base64.StdEncoding.DecodeString(secret.SecretValue.Ciphertext) + + plainTextValue, err := crypto.DecryptSymmetric(workspaceKeyInBytes, value_ciphertext, value_tag, value_iv) + if err != nil { + return nil, err + } + + env := models.SingleEnvironmentVariable{ + Key: string(plainTextKey), + Value: string(plainTextValue), + } + + listOfEnv = append(listOfEnv, env) + } + + return listOfEnv, nil +} + +func getExpandedEnvVariable(secrets []models.SingleEnvironmentVariable, variableWeAreLookingFor string, hashMapOfCompleteVariables map[string]string, hashMapOfSelfRefs map[string]string) string { + if value, found := hashMapOfCompleteVariables[variableWeAreLookingFor]; found { + return value + } + + for _, secret := range secrets { + if secret.Key == variableWeAreLookingFor { + regex := regexp.MustCompile(`\${([^\}]*)}`) + variablesToPopulate := regex.FindAllString(secret.Value, -1) + + // case: variable is a constant so return its value + if len(variablesToPopulate) == 0 { + return secret.Value + } + + valueToEdit := secret.Value + for _, variableWithSign := range variablesToPopulate { + variableWithoutSign := strings.Trim(variableWithSign, "}") + variableWithoutSign = strings.Trim(variableWithoutSign, "${") + + // case: reference to self + if variableWithoutSign == secret.Key { + hashMapOfSelfRefs[variableWithoutSign] = variableWithoutSign + continue + } else { + var expandedVariableValue string + + if preComputedVariable, found := hashMapOfCompleteVariables[variableWithoutSign]; found { + expandedVariableValue = preComputedVariable + } else { + expandedVariableValue = getExpandedEnvVariable(secrets, variableWithoutSign, hashMapOfCompleteVariables, hashMapOfSelfRefs) + hashMapOfCompleteVariables[variableWithoutSign] = expandedVariableValue + } + + // If after expanding all the vars above, is the current var a self ref? if so no replacement needed for it + if _, found := hashMapOfSelfRefs[variableWithoutSign]; found { + continue + } else { + valueToEdit = strings.ReplaceAll(valueToEdit, variableWithSign, expandedVariableValue) + } + } + } + + return valueToEdit + + } else { + continue + } + } + + return "${" + variableWeAreLookingFor + "}" +} + +func SubstituteSecrets(secrets []models.SingleEnvironmentVariable) []models.SingleEnvironmentVariable { + hashMapOfCompleteVariables := make(map[string]string) + hashMapOfSelfRefs := make(map[string]string) + expandedSecrets := []models.SingleEnvironmentVariable{} + + for _, secret := range secrets { + expandedVariable := getExpandedEnvVariable(secrets, secret.Key, hashMapOfCompleteVariables, hashMapOfSelfRefs) + expandedSecrets = append(expandedSecrets, models.SingleEnvironmentVariable{ + Key: secret.Key, + Value: expandedVariable, + }) + + } + + return expandedSecrets +} diff --git a/k8-operator/packages/crypto/crypto.go b/k8-operator/packages/crypto/crypto.go new file mode 100644 index 000000000..d5b0a9955 --- /dev/null +++ b/k8-operator/packages/crypto/crypto.go @@ -0,0 +1,28 @@ +package crypto + +import ( + "crypto/aes" + "crypto/cipher" +) + +func DecryptSymmetric(key []byte, encryptedPrivateKey []byte, tag []byte, IV []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + aesgcm, err := cipher.NewGCMWithNonceSize(block, len(IV)) + if err != nil { + return nil, err + } + + var nonce = IV + var ciphertext = append(encryptedPrivateKey, tag...) + + plaintext, err := aesgcm.Open(nil, nonce, ciphertext, nil) + if err != nil { + return nil, err + } + + return plaintext, nil +} diff --git a/k8-operator/packages/models/api.go b/k8-operator/packages/models/api.go new file mode 100644 index 000000000..57420dac5 --- /dev/null +++ b/k8-operator/packages/models/api.go @@ -0,0 +1,51 @@ +package models + +import "time" + +type PullSecretsByInfisicalTokenResponse struct { + Secrets []struct { + ID string `json:"_id"` + Workspace string `json:"workspace"` + Type string `json:"type"` + Environment string `json:"environment"` + SecretKey struct { + Workspace string `json:"workspace"` + Ciphertext string `json:"ciphertext"` + Iv string `json:"iv"` + Tag string `json:"tag"` + Hash string `json:"hash"` + } `json:"secretKey"` + SecretValue struct { + Workspace string `json:"workspace"` + Ciphertext string `json:"ciphertext"` + Iv string `json:"iv"` + Tag string `json:"tag"` + Hash string `json:"hash"` + } `json:"secretValue"` + } `json:"secrets"` + Key struct { + EncryptedKey string `json:"encryptedKey"` + Nonce string `json:"nonce"` + Sender struct { + PublicKey string `json:"publicKey"` + } `json:"sender"` + Receiver struct { + RefreshVersion int `json:"refreshVersion"` + ID string `json:"_id"` + Email string `json:"email"` + CustomerID string `json:"customerId"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + V int `json:"__v"` + FirstName string `json:"firstName"` + LastName string `json:"lastName"` + PublicKey string `json:"publicKey"` + } `json:"receiver"` + Workspace string `json:"workspace"` + } `json:"key"` +} + +type SingleEnvironmentVariable struct { + Key string `json:"key"` + Value string `json:"value"` +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..cf02a1760 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1941 @@ +{ + "name": "infisical", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "infisical", + "license": "ISC", + "devDependencies": { + "eslint": "^8.29.0", + "husky": "^8.0.2" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.3.tgz", + "integrity": "sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.4.0", + "globals": "^13.15.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.7", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz", + "integrity": "sha512-kBbPWzN8oVMLb0hOUYXhmxggL/1cJE6ydvjDIGi9EnAGUyA7cLVKQg+d/Dsm+KZwx2czGHrCmMVLiyg8s5JPKw==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/acorn": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", + "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.29.0.tgz", + "integrity": "sha512-isQ4EEiyUjZFbEKvEGJKKGBwXtvXX+zJbkVKCgTuB9t/+jUBcy8avhkEwWJecI15BkRkOYmvIM5ynbhRjEkoeg==", + "dev": true, + "dependencies": { + "@eslint/eslintrc": "^1.3.3", + "@humanwhocodes/config-array": "^0.11.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.1.1", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.4.0", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.15.0", + "grapheme-splitter": "^1.0.4", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-sdsl": "^4.1.4", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "regexpp": "^3.2.0", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", + "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/espree": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz", + "integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==", + "dev": true, + "dependencies": { + "acorn": "^8.8.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.14.0.tgz", + "integrity": "sha512-eR2D+V9/ExcbF9ls441yIuN6TI2ED1Y2ZcA5BmMtJsOkWOFRJQ0Jt0g1UwqXJJVAb+V+umH5Dfr8oh4EVP7VVg==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "dev": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.18.0.tgz", + "integrity": "sha512-/mR4KI8Ps2spmoc0Ulu9L7agOF0du1CZNQ3dke8yItYlyKNmGrkONemBbd6V8UTc1Wgcqn21t3WYB7dbRmh6/A==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/husky": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.2.tgz", + "integrity": "sha512-Tkv80jtvbnkK3mYWxPZePGFpQ/tT3HNSs/sasF9P2YfkMezDl3ON37YN6jUUI4eTg5LcyVynlb6r4eyvOmspvg==", + "dev": true, + "bin": { + "husky": "lib/bin.js" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/ignore": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.1.tgz", + "integrity": "sha512-d2qQLzTJ9WxQftPAuEQpSPmKqzxePjzVbpAVv62AQ64NTL+wR4JkrVqR/LqFsFEUsHDAiId52mJteHDFuDkElA==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/js-sdsl": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.2.0.tgz", + "integrity": "sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + }, + "dependencies": { + "@eslint/eslintrc": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.3.tgz", + "integrity": "sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.4.0", + "globals": "^13.15.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + } + }, + "@humanwhocodes/config-array": { + "version": "0.11.7", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz", + "integrity": "sha512-kBbPWzN8oVMLb0hOUYXhmxggL/1cJE6ydvjDIGi9EnAGUyA7cLVKQg+d/Dsm+KZwx2czGHrCmMVLiyg8s5JPKw==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + } + }, + "@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true + }, + "@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "acorn": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", + "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==", + "dev": true + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "requires": {} + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "eslint": { + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.29.0.tgz", + "integrity": "sha512-isQ4EEiyUjZFbEKvEGJKKGBwXtvXX+zJbkVKCgTuB9t/+jUBcy8avhkEwWJecI15BkRkOYmvIM5ynbhRjEkoeg==", + "dev": true, + "requires": { + "@eslint/eslintrc": "^1.3.3", + "@humanwhocodes/config-array": "^0.11.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.1.1", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.4.0", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.15.0", + "grapheme-splitter": "^1.0.4", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-sdsl": "^4.1.4", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "regexpp": "^3.2.0", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0" + } + }, + "eslint-scope": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", + "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^2.0.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true + } + } + }, + "eslint-visitor-keys": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "dev": true + }, + "espree": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz", + "integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==", + "dev": true, + "requires": { + "acorn": "^8.8.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.3.0" + } + }, + "esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "fastq": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.14.0.tgz", + "integrity": "sha512-eR2D+V9/ExcbF9ls441yIuN6TI2ED1Y2ZcA5BmMtJsOkWOFRJQ0Jt0g1UwqXJJVAb+V+umH5Dfr8oh4EVP7VVg==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "globals": { + "version": "13.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.18.0.tgz", + "integrity": "sha512-/mR4KI8Ps2spmoc0Ulu9L7agOF0du1CZNQ3dke8yItYlyKNmGrkONemBbd6V8UTc1Wgcqn21t3WYB7dbRmh6/A==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "husky": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.2.tgz", + "integrity": "sha512-Tkv80jtvbnkK3mYWxPZePGFpQ/tT3HNSs/sasF9P2YfkMezDl3ON37YN6jUUI4eTg5LcyVynlb6r4eyvOmspvg==", + "dev": true + }, + "ignore": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.1.tgz", + "integrity": "sha512-d2qQLzTJ9WxQftPAuEQpSPmKqzxePjzVbpAVv62AQ64NTL+wR4JkrVqR/LqFsFEUsHDAiId52mJteHDFuDkElA==", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "js-sdsl": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.2.0.tgz", + "integrity": "sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ==", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 000000000..4d042a13d --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "infisical", + "repository": { + "type": "git", + "url": "git+https://github.com/infisical/infisical.git" + }, + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/infisical/infisical/issues" + }, + "homepage": "https://github.com/infisical/infisical#readme", + "scripts": { + "prepare": "husky install" + }, + "lint-staged": { + "*.{js,jsx,ts,tsx}": [ + "eslint --fix" + ] + }, + "devDependencies": { + "eslint": "^8.29.0", + "husky": "^8.0.2" + } +}