diff --git a/.env.example b/.env.example index ec832f9e9..169e6db0d 100644 --- a/.env.example +++ b/.env.example @@ -1,15 +1,13 @@ # 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_SERVICE_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 @@ -33,26 +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_HOST= # required +SMTP_USERNAME= # required +SMTP_PASSWORD= # required SMTP_PORT=587 -SMTP_NAME=Team -SMTP_USERNAME=team@infisical.com -SMTP_PASSWORD= +SMTP_SECURE=false +SMTP_FROM_ADDRESS= # required +SMTP_FROM_NAME=Infisical # Integration # Optional only if integration is used 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/.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 index f17d8c5c8..8022a25bc 100644 --- a/.github/workflows/check-be-pull-request.yml +++ b/.github/workflows/check-be-pull-request.yml @@ -1,41 +1,42 @@ -name: Check Backend Pull Request +name: "Check Backend Pull Request" on: pull_request: - types: [ opened, synchronize ] + types: [opened, synchronize] paths: - - 'backend/**' - - '!backend/README.md' - - '!backend/.*' - - 'backend/.eslintrc.js' - + - "backend/**" + - "!backend/README.md" + - "!backend/.*" + - "backend/.eslintrc.js" jobs: - check-be-pr: name: Check runs-on: ubuntu-latest steps: - - - name: โ˜๏ธ Checkout source + - name: โ˜๏ธ Checkout source uses: actions/checkout@v3 - - - name: ๐Ÿ”ง Setup Node 16 + - name: ๐Ÿ”ง Setup Node 16 uses: actions/setup-node@v3 with: - node-version: '16' - cache: 'npm' + node-version: "16" + cache: "npm" cache-dependency-path: backend/package-lock.json - - - name: ๐Ÿ“ฆ Install dependencies + - 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: ๐Ÿ—๏ธ Run build + - 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/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/release_build.yml b/.github/workflows/release_build.yml index 695b0ea24..da2a29435 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 @@ -32,6 +41,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }} FURY_TOKEN: ${{ secrets.FURYPUSHTOKEN }} + AUR_KEY: ${{ secrets.AUR_KEY }} - uses: actions/setup-python@v4 - run: pip install --upgrade cloudsmith-cli - name: Publish to CloudSmith 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 9ac38524b..0b348b71a 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -7,28 +7,37 @@ # # 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 - openbsd - windows goarch: - - 386 + - "386" - amd64 - arm - arm64 goarm: - - 6 - - 7 + - "6" + - "7" ignore: - - goos: darwin - goarch: "386" - goos: windows goarch: "386" - goos: freebsd @@ -71,12 +80,12 @@ nfpms: - id: infisical package_name: infisical builds: - - infisical + - all-other-builds vendor: Infisical, Inc homepage: https://infisical.com/ maintainer: Infisical, Inc description: The offical Infisical CLI - license: Apache 2.0 + license: MIT formats: - rpm - deb @@ -92,7 +101,23 @@ scoop: email: ai@infisical.com homepage: "https://infisical.com" description: "The official Infisical CLI" - license: Apache-2.0 + license: MIT +aurs: + - + name: infisical-bin + homepage: "https://infisical.com" + description: "The official Infisical CLI" + maintainers: + - Infisical, Inc + license: MIT + private_key: '{{ .Env.AUR_KEY }}' + git_url: 'ssh://aur@aur.archlinux.org/infisical-bin.git' + package: |- + # bin + install -Dm755 "./infisical" "${pkgdir}/usr/bin/infisical" + # license + install -Dm644 "./LICENSE" "${pkgdir}/usr/share/licenses/infisical/LICENSE" + # dockers: # - dockerfile: goreleaser.dockerfile # goos: linux diff --git a/README.md b/README.md index caf79191e..292f5fe94 100644 --- a/README.md +++ b/README.md @@ -146,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 @@ -179,7 +181,9 @@ We're currently setting the foundation and building [integrations](https://infis ๐Ÿ”œ TravisCI - ๐Ÿ”œ Netlify (https://github.com/Infisical/infisical/issues/55) + + โœ”๏ธ Netlify + ๐Ÿ”œ Railway @@ -317,4 +321,4 @@ Infisical officially launched as v.1.0 on November 21st, 2022. However, a lot of - + 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 853f52e5b..291a33550 100644 --- a/backend/environment.d.ts +++ b/backend/environment.d.ts @@ -14,6 +14,8 @@ declare global { JWT_SIGNUP_SECRET: string; MONGO_URL: string; NODE_ENV: 'development' | 'staging' | 'testing' | 'production'; + VERBOSE_ERROR_OUTPUT: string; + LOKI_HOST: string; CLIENT_ID_HEROKU: string; CLIENT_ID_VERCEL: string; CLIENT_ID_NETLIFY: string; @@ -22,8 +24,6 @@ declare global { 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/package-lock.json b/backend/package-lock.json index f74a71505..85b168c1e 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -10,9 +10,11 @@ "license": "ISC", "dependencies": { "@godaddy/terminus": "^4.11.2", + "@octokit/rest": "^19.0.5", "@sentry/node": "^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", @@ -24,35 +26,45 @@ "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", + "libsodium-wrappers": "^0.7.10", "mongoose": "^6.7.2", "nodemailer": "^6.8.0", - "posthog-node": "^2.1.0", + "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.9.3" + "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", "install": "^0.13.0", "jest": "^29.3.1", + "jest-junit": "^15.0.0", "nodemon": "^2.0.19", "npm": "^8.19.3", + "supertest": "^6.3.3", + "ts-jest": "^29.0.3", "ts-node": "^10.9.1" } }, @@ -1073,6 +1085,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" @@ -1088,6 +1101,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", @@ -1991,6 +2005,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", @@ -2003,6 +2025,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", @@ -2555,6 +2587,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", @@ -2590,6 +2637,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", @@ -2599,6 +2793,60 @@ "@maxmind/geoip2-node": "^3.4.0" } }, + "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": { + "@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.19.0", "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.19.0.tgz", @@ -2813,6 +3061,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", @@ -2880,6 +3134,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", @@ -2895,6 +3159,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", @@ -2915,6 +3189,12 @@ "@types/node": "*" } }, + "node_modules/@types/prettier": { + "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": { "version": "6.9.7", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", @@ -2949,6 +3229,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", @@ -3346,6 +3645,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", @@ -3355,6 +3660,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", @@ -3485,6 +3803,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", @@ -3593,6 +3916,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", @@ -3613,6 +3948,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", @@ -3761,7 +4107,6 @@ "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", - "fsevents": "~2.3.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", @@ -3838,6 +4183,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", @@ -3853,8 +4207,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", @@ -3867,6 +4251,12 @@ "node": ">= 0.8" } }, + "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", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -3930,6 +4320,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", @@ -3966,6 +4362,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", @@ -4054,6 +4468,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", @@ -4072,6 +4491,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", @@ -4159,6 +4588,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", @@ -4615,6 +5049,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", @@ -4649,6 +5089,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", @@ -4746,6 +5191,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", @@ -4778,6 +5228,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", @@ -4968,7 +5433,6 @@ "minimist": "^1.2.5", "neo-async": "^2.6.0", "source-map": "^0.6.1", - "uglify-js": "^3.1.4", "wordwrap": "^1.0.0" }, "bin": { @@ -5033,6 +5497,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", @@ -5297,11 +5770,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" }, @@ -5618,7 +6098,6 @@ "@types/node": "*", "anymatch": "^3.0.3", "fb-watchman": "^2.0.0", - "fsevents": "^2.3.2", "graceful-fs": "^4.2.9", "jest-regex-util": "^29.2.0", "jest-util": "^29.3.1", @@ -5633,6 +6112,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", @@ -5835,6 +6329,7 @@ "@jest/transform": "^29.3.1", "@jest/types": "^29.3.1", "@types/babel__traverse": "^7.0.6", + "@types/prettier": "^2.1.5", "babel-preset-current-node-syntax": "^1.0.0", "chalk": "^4.0.0", "expect": "^29.3.1", @@ -6014,32 +6509,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": { @@ -6100,6 +6581,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", @@ -6122,6 +6608,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", @@ -6148,35 +6647,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, - "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.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", @@ -6184,10 +6659,22 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, - "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/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/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", @@ -6198,7 +6685,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" }, @@ -6394,6 +6880,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", @@ -6409,11 +6907,9 @@ "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.11.0.tgz", "integrity": "sha512-9l9n4Nk2BYZzljW3vHah3Z0rfS5npKw6ktnkmFgTcnzaXH1DRm3pDl6VMHu84EVb1lzmSaJC4OzWZqTkB5i2wg==", "dependencies": { - "@aws-sdk/credential-providers": "^3.186.0", "bson": "^4.7.0", "denque": "^2.1.0", "mongodb-connection-string-url": "^2.5.4", - "saslprep": "^1.0.3", "socks": "^2.7.1" }, "engines": { @@ -6508,6 +7004,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", @@ -6605,6 +7139,9 @@ }, "bin": { "nopt": "bin/nopt.js" + }, + "engines": { + "node": "*" } }, "node_modules/normalize-path": { @@ -6693,129 +7230,7 @@ "treeverse", "validate-npm-package-name", "which", - "write-file-atomic", - "@colors/colors", - "@gar/promisify", - "@npmcli/disparity-colors", - "@npmcli/git", - "@npmcli/installed-package-contents", - "@npmcli/metavuln-calculator", - "@npmcli/move-file", - "@npmcli/name-from-folder", - "@npmcli/node-gyp", - "@npmcli/promise-spawn", - "@npmcli/query", - "@tootallnate/once", - "agent-base", - "agentkeepalive", - "aggregate-error", - "ansi-regex", - "ansi-styles", - "aproba", - "are-we-there-yet", - "asap", - "balanced-match", - "bin-links", - "binary-extensions", - "brace-expansion", - "builtins", - "cidr-regex", - "clean-stack", - "clone", - "cmd-shim", - "color-convert", - "color-name", - "color-support", - "common-ancestor-path", - "concat-map", - "console-control-strings", - "cssesc", - "debug", - "debuglog", - "defaults", - "delegates", - "depd", - "dezalgo", - "diff", - "emoji-regex", - "encoding", - "env-paths", - "err-code", - "fs.realpath", - "function-bind", - "gauge", - "has", - "has-flag", - "has-unicode", - "http-cache-semantics", - "http-proxy-agent", - "https-proxy-agent", - "humanize-ms", - "iconv-lite", - "ignore-walk", - "imurmurhash", - "indent-string", - "infer-owner", - "inflight", - "inherits", - "ip", - "ip-regex", - "is-core-module", - "is-fullwidth-code-point", - "is-lambda", - "isexe", - "json-stringify-nice", - "jsonparse", - "just-diff", - "just-diff-apply", - "lru-cache", - "minipass-collect", - "minipass-fetch", - "minipass-flush", - "minipass-json-stream", - "minipass-sized", - "minizlib", - "mute-stream", - "negotiator", - "normalize-package-data", - "npm-bundled", - "npm-normalize-package-bin", - "npm-packlist", - "once", - "path-is-absolute", - "postcss-selector-parser", - "promise-all-reject-late", - "promise-call-limit", - "promise-inflight", - "promise-retry", - "promzard", - "read-cmd-shim", - "readable-stream", - "retry", - "safe-buffer", - "safer-buffer", - "set-blocking", - "signal-exit", - "smart-buffer", - "socks", - "socks-proxy-agent", - "spdx-correct", - "spdx-exceptions", - "spdx-expression-parse", - "spdx-license-ids", - "string_decoder", - "string-width", - "strip-ansi", - "supports-color", - "unique-filename", - "unique-slug", - "util-deprecate", - "validate-npm-package-license", - "walk-up-path", - "wcwidth", - "wide-align", - "wrappy", - "yallist" + "write-file-atomic" ], "dev": true, "dependencies": { @@ -7458,7 +7873,6 @@ "inBundle": true, "license": "MIT", "dependencies": { - "@colors/colors": "1.5.0", "string-width": "^4.2.0" }, "engines": { @@ -8254,7 +8668,6 @@ "inBundle": true, "license": "MIT", "dependencies": { - "encoding": "^0.1.13", "minipass": "^3.1.6", "minipass-sized": "^1.0.3", "minizlib": "^2.1.2" @@ -9353,6 +9766,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", @@ -9600,9 +10021,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" }, @@ -9667,6 +10088,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", @@ -9974,6 +10420,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 +10449,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 +10576,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 +10634,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 +10716,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", @@ -10363,6 +10865,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", @@ -10401,6 +10949,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", @@ -10474,6 +11027,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", @@ -10622,6 +11223,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", @@ -10682,7 +11288,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" } @@ -10797,6 +11403,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", @@ -10846,6 +11500,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", @@ -10858,8 +11518,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/yargs": { "version": "17.6.2", @@ -12649,6 +13308,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", @@ -12658,6 +13322,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", @@ -13105,6 +13779,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", @@ -13131,6 +13811,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", @@ -13140,6 +13932,60 @@ "@maxmind/geoip2-node": "^3.4.0" } }, + "@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": { + "@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.19.0", "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.19.0.tgz", @@ -13334,6 +14180,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", @@ -13401,6 +14253,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", @@ -13416,6 +14278,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", @@ -13436,6 +14308,12 @@ "@types/node": "*" } }, + "@types/prettier": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz", + "integrity": "sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg==", + "dev": true + }, "@types/qs": { "version": "6.9.7", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", @@ -13470,6 +14348,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", @@ -13732,12 +14629,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", @@ -13833,6 +14746,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", @@ -13917,6 +14835,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", @@ -13934,6 +14861,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", @@ -14085,6 +15017,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", @@ -14097,8 +15053,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", @@ -14108,6 +15081,12 @@ "delayed-stream": "~1.0.0" } }, + "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", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -14158,6 +15137,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", @@ -14191,6 +15176,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", @@ -14253,6 +15247,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", @@ -14264,6 +15263,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", @@ -14330,6 +15339,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", @@ -14684,6 +15698,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", @@ -14711,6 +15731,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", @@ -14789,6 +15814,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", @@ -14804,6 +15834,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", @@ -14976,6 +16018,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", @@ -15163,11 +16211,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", @@ -15407,6 +16459,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", @@ -15572,6 +16636,7 @@ "@jest/transform": "^29.3.1", "@jest/types": "^29.3.1", "@types/babel__traverse": "^7.0.6", + "@types/prettier": "^2.1.5", "babel-preset-current-node-syntax": "^1.0.0", "chalk": "^4.0.0", "expect": "^29.3.1", @@ -15717,27 +16782,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": { @@ -15792,6 +16844,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", @@ -15808,6 +16865,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", @@ -15828,35 +16898,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, - "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.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", @@ -15864,10 +16910,22 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, - "lodash.once": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + "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" + } + }, + "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", @@ -15878,7 +16936,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" } @@ -16021,6 +17078,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", @@ -16110,6 +17173,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", @@ -18016,6 +19108,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", @@ -18190,9 +19290,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" }, @@ -18243,6 +19343,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", @@ -18437,6 +19557,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", @@ -18455,7 +19580,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" } @@ -18563,6 +19687,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", @@ -18597,6 +19736,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", @@ -18641,6 +19801,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", @@ -18744,6 +19909,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", @@ -18770,6 +19971,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", @@ -18825,6 +20031,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", @@ -18917,6 +20144,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", @@ -18955,7 +20187,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", @@ -19045,6 +20277,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", @@ -19082,6 +20354,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", @@ -19091,8 +20369,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==" }, "yargs": { "version": "17.6.2", diff --git a/backend/package.json b/backend/package.json index bad2908d9..d1a03a74c 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,9 +1,11 @@ { "dependencies": { "@godaddy/terminus": "^4.11.2", + "@octokit/rest": "^19.0.5", "@sentry/node": "^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", @@ -15,17 +17,20 @@ "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", + "libsodium-wrappers": "^0.7.10", "mongoose": "^6.7.2", "nodemailer": "^6.8.0", - "posthog-node": "^2.1.0", + "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.9.3" + "typescript": "^4.9.3", + "winston": "^3.8.2", + "winston-loki": "^6.0.6" }, "name": "infisical-api", "version": "1.0.0", @@ -37,7 +42,11 @@ "build": "rimraf ./build && tsc && cp -R ./src/templates ./build", "lint": "eslint . --ext .ts", "lint-and-fix": "eslint . --ext .ts --fix", - "lint-staged": "lint-staged" + "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", @@ -51,22 +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", "install": "^0.13.0", "jest": "^29.3.1", + "jest-junit": "^15.0.0", "nodemon": "^2.0.19", "npm": "^8.19.3", + "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..8320a7d76 --- /dev/null +++ b/backend/src/app.ts @@ -0,0 +1,109 @@ +// eslint-disable-next-line @typescript-eslint/no-var-requires +const { patchRouterParam } = require('./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 { + workspace as eeWorkspaceRouter, + secret as eeSecretRouter +} from './ee/routes/v1'; +import { + signup as v1SignupRouter, + auth as v1AuthRouter, + bot as v1BotRouter, + organization as v1OrganizationRouter, + workspace as v1WorkspaceRouter, + membershipOrg as v1MembershipOrgRouter, + membership as v1MembershipRouter, + key as v1KeyRouter, + inviteOrg as v1InviteOrgRouter, + user as v1UserRouter, + userAction as v1UserActionRouter, + secret as v1SecretRouter, + serviceToken as v1ServiceTokenRouter, + password as v1PasswordRouter, + stripe as v1StripeRouter, + integration as v1IntegrationRouter, + integrationAuth as v1IntegrationAuthRouter +} from './routes/v1'; +import { + secret as v2SecretRouter, + workspace as v2WorkspaceRouter +} from './routes/v2'; + +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()); +} + +// (EE) routes +app.use('/api/v1/secret', eeSecretRouter); +app.use('/api/v1/workspace', eeWorkspaceRouter); + +// v1 routes +app.use('/api/v1/signup', v1SignupRouter); +app.use('/api/v1/auth', v1AuthRouter); +app.use('/api/v1/bot', v1BotRouter); +app.use('/api/v1/user', v1UserRouter); +app.use('/api/v1/user-action', v1UserActionRouter); +app.use('/api/v1/organization', v1OrganizationRouter); +app.use('/api/v1/workspace', v1WorkspaceRouter); +app.use('/api/v1/membership-org', v1MembershipOrgRouter); +app.use('/api/v1/membership', v1MembershipRouter); +app.use('/api/v1/key', v1KeyRouter); +app.use('/api/v1/invite-org', v1InviteOrgRouter); +app.use('/api/v1/secret', v1SecretRouter); +app.use('/api/v1/service-token', v1ServiceTokenRouter); +app.use('/api/v1/password', v1PasswordRouter); +app.use('/api/v1/stripe', v1StripeRouter); +app.use('/api/v1/integration', v1IntegrationRouter); +app.use('/api/v1/integration-auth', v1IntegrationAuthRouter); + +// v2 routes +app.use('/api/v2/workspace', v2WorkspaceRouter); +app.use('/api/v2/secret', v2SecretRouter); + + +//* 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 e3d62c42c..3fb475099 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -10,26 +10,30 @@ 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 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 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_PORT = process.env.SMTP_PORT! || 587; -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!; @@ -37,6 +41,7 @@ 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 LICENSE_KEY = process.env.LICENSE_KEY!; export { PORT, @@ -51,29 +56,34 @@ export { 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, - PRIVATE_KEY, - PUBLIC_KEY, SENTRY_DSN, SITE_URL, SMTP_HOST, SMTP_PORT, - SMTP_NAME, + 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 + TELEMETRY_ENABLED, + LICENSE_KEY }; diff --git a/backend/src/controllers/authController.ts b/backend/src/controllers/v1/authController.ts similarity index 97% rename from backend/src/controllers/authController.ts rename to backend/src/controllers/v1/authController.ts index 20ac813d4..defd03d8a 100644 --- a/backend/src/controllers/authController.ts +++ b/backend/src/controllers/v1/authController.ts @@ -4,14 +4,14 @@ import jwt from 'jsonwebtoken'; import * as Sentry from '@sentry/node'; import * as bigintConversion from 'bigint-conversion'; const jsrp = require('jsrp'); -import { User } from '../models'; -import { createToken, issueTokens, clearTokens } from '../helpers/auth'; +import { User } from '../../models'; +import { createToken, issueTokens, clearTokens } from '../../helpers/auth'; import { NODE_ENV, JWT_AUTH_LIFETIME, JWT_AUTH_SECRET, JWT_REFRESH_SECRET -} from '../config'; +} from '../../config'; declare module 'jsonwebtoken' { export interface UserIDJwtPayload extends jwt.JwtPayload { diff --git a/backend/src/controllers/botController.ts b/backend/src/controllers/v1/botController.ts similarity index 96% rename from backend/src/controllers/botController.ts rename to backend/src/controllers/v1/botController.ts index 7819e32df..ab86897cb 100644 --- a/backend/src/controllers/botController.ts +++ b/backend/src/controllers/v1/botController.ts @@ -1,7 +1,7 @@ import { Request, Response } from 'express'; import * as Sentry from '@sentry/node'; -import { Bot, BotKey } from '../models'; -import { createBot } from '../helpers/bot'; +import { Bot, BotKey } from '../../models'; +import { createBot } from '../../helpers/bot'; interface BotKey { encryptedKey: string; diff --git a/backend/src/controllers/index.ts b/backend/src/controllers/v1/index.ts similarity index 100% rename from backend/src/controllers/index.ts rename to backend/src/controllers/v1/index.ts diff --git a/backend/src/controllers/integrationAuthController.ts b/backend/src/controllers/v1/integrationAuthController.ts similarity index 92% rename from backend/src/controllers/integrationAuthController.ts rename to backend/src/controllers/v1/integrationAuthController.ts index c242c239a..95d0066ae 100644 --- a/backend/src/controllers/integrationAuthController.ts +++ b/backend/src/controllers/v1/integrationAuthController.ts @@ -2,10 +2,10 @@ import { Request, Response } from 'express'; import * as Sentry from '@sentry/node'; import axios from 'axios'; import { readFileSync } from 'fs'; -import { IntegrationAuth, Integration } from '../models'; -import { INTEGRATION_SET, INTEGRATION_OPTIONS, ENV_DEV } from '../variables'; -import { IntegrationService } from '../services'; -import { getApps, revokeAccess } from '../integrations'; +import { IntegrationAuth, Integration } from '../../models'; +import { INTEGRATION_SET, INTEGRATION_OPTIONS, ENV_DEV } from '../../variables'; +import { IntegrationService } from '../../services'; +import { getApps, revokeAccess } from '../../integrations'; export const getIntegrationOptions = async ( req: Request, diff --git a/backend/src/controllers/integrationController.ts b/backend/src/controllers/v1/integrationController.ts similarity index 94% rename from backend/src/controllers/integrationController.ts rename to backend/src/controllers/v1/integrationController.ts index 910c7e825..c05794959 100644 --- a/backend/src/controllers/integrationController.ts +++ b/backend/src/controllers/v1/integrationController.ts @@ -1,9 +1,9 @@ import { Request, Response } from 'express'; import { readFileSync } from 'fs'; import * as Sentry from '@sentry/node'; -import { Integration, Bot, BotKey } from '../models'; -import { EventService } from '../services'; -import { eventPushSecrets } from '../events'; +import { Integration, Bot, BotKey } from '../../models'; +import { EventService } from '../../services'; +import { eventPushSecrets } from '../../events'; interface Key { encryptedKey: string; diff --git a/backend/src/controllers/keyController.ts b/backend/src/controllers/v1/keyController.ts similarity index 82% rename from backend/src/controllers/keyController.ts rename to backend/src/controllers/v1/keyController.ts index 778d44b60..332215894 100644 --- a/backend/src/controllers/keyController.ts +++ b/backend/src/controllers/v1/keyController.ts @@ -1,9 +1,8 @@ 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'; +import { Key } from '../../models'; +import { findMembership } from '../../helpers/membership'; +import { GRANTED } from '../../variables'; /** * Add (encrypted) copy of workspace key for workspace with id [workspaceId] for user with @@ -84,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/membershipController.ts b/backend/src/controllers/v1/membershipController.ts similarity index 95% rename from backend/src/controllers/membershipController.ts rename to backend/src/controllers/v1/membershipController.ts index f2ed3db6e..187e8127c 100644 --- a/backend/src/controllers/membershipController.ts +++ b/backend/src/controllers/v1/membershipController.ts @@ -1,13 +1,13 @@ import { Request, Response } from 'express'; import * as Sentry from '@sentry/node'; -import { Membership, MembershipOrg, User, Key } from '../models'; +import { Membership, MembershipOrg, User, Key } from '../../models'; import { findMembership, deleteMembership as deleteMember -} from '../helpers/membership'; -import { sendMail } from '../helpers/nodemailer'; -import { SITE_URL } from '../config'; -import { ADMIN, MEMBER, GRANTED, ACCEPTED } from '../variables'; +} from '../../helpers/membership'; +import { sendMail } from '../../helpers/nodemailer'; +import { SITE_URL } from '../../config'; +import { ADMIN, MEMBER, GRANTED, ACCEPTED } from '../../variables'; /** * Check that user is a member of workspace with id [workspaceId] diff --git a/backend/src/controllers/membershipOrgController.ts b/backend/src/controllers/v1/membershipOrgController.ts similarity index 89% rename from backend/src/controllers/membershipOrgController.ts rename to backend/src/controllers/v1/membershipOrgController.ts index a2159bcd0..400147a16 100644 --- a/backend/src/controllers/membershipOrgController.ts +++ b/backend/src/controllers/v1/membershipOrgController.ts @@ -1,14 +1,14 @@ import { Request, Response } from 'express'; import * as Sentry from '@sentry/node'; import crypto from 'crypto'; -import { SITE_URL, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../config'; -import { MembershipOrg, Organization, User, Token } from '../models'; -import { deleteMembershipOrg as deleteMemberFromOrg } from '../helpers/membershipOrg'; -import { checkEmailVerification } from '../helpers/signup'; -import { createToken } from '../helpers/auth'; -import { updateSubscriptionOrgQuantity } from '../helpers/organization'; -import { sendMail } from '../helpers/nodemailer'; -import { OWNER, ADMIN, MEMBER, ACCEPTED, INVITED } from '../variables'; +import { SITE_URL, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../../config'; +import { MembershipOrg, Organization, User, Token } from '../../models'; +import { deleteMembershipOrg as deleteMemberFromOrg } from '../../helpers/membershipOrg'; +import { checkEmailVerification } from '../../helpers/signup'; +import { createToken } from '../../helpers/auth'; +import { updateSubscriptionOrgQuantity } from '../../helpers/organization'; +import { sendMail } from '../../helpers/nodemailer'; +import { OWNER, ADMIN, MEMBER, ACCEPTED, INVITED } from '../../variables'; /** * Delete organization membership with id [membershipOrgId] from organization @@ -80,14 +80,14 @@ export const changeMembershipOrgRole = async (req: Request, res: Response) => { // TODO let membershipToChangeRole; - try { - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to change organization membership role' - }); - } + // try { + // } catch (err) { + // Sentry.setUser({ email: req.user.email }); + // Sentry.captureException(err); + // return res.status(400).send({ + // message: 'Failed to change organization membership role' + // }); + // } return res.status(200).send({ membershipOrg: membershipToChangeRole diff --git a/backend/src/controllers/organizationController.ts b/backend/src/controllers/v1/organizationController.ts similarity index 97% rename from backend/src/controllers/organizationController.ts rename to backend/src/controllers/v1/organizationController.ts index 056990acc..44e140b4e 100644 --- a/backend/src/controllers/organizationController.ts +++ b/backend/src/controllers/v1/organizationController.ts @@ -6,7 +6,7 @@ import { STRIPE_PRODUCT_STARTER, STRIPE_PRODUCT_PRO, STRIPE_PRODUCT_CARD_AUTH -} from '../config'; +} from '../../config'; import Stripe from 'stripe'; const stripe = new Stripe(STRIPE_SECRET_KEY, { @@ -18,10 +18,10 @@ import { Organization, Workspace, IncidentContactOrg -} from '../models'; -import { createOrganization as create } from '../helpers/organization'; -import { addMembershipsOrg } from '../helpers/membershipOrg'; -import { OWNER, ACCEPTED } from '../variables'; +} from '../../models'; +import { createOrganization as create } from '../../helpers/organization'; +import { addMembershipsOrg } from '../../helpers/membershipOrg'; +import { OWNER, ACCEPTED } from '../../variables'; const productToPriceMap = { starter: STRIPE_PRODUCT_STARTER, diff --git a/backend/src/controllers/passwordController.ts b/backend/src/controllers/v1/passwordController.ts similarity index 96% rename from backend/src/controllers/passwordController.ts rename to backend/src/controllers/v1/passwordController.ts index b029bc0be..27d712a6b 100644 --- a/backend/src/controllers/passwordController.ts +++ b/backend/src/controllers/v1/passwordController.ts @@ -1,13 +1,14 @@ import { Request, Response } from 'express'; import * as Sentry from '@sentry/node'; import crypto from 'crypto'; +// eslint-disable-next-line @typescript-eslint/no-var-requires const jsrp = require('jsrp'); import * as bigintConversion from 'bigint-conversion'; -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'; +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 = {}; diff --git a/backend/src/controllers/secretController.ts b/backend/src/controllers/v1/secretController.ts similarity index 91% rename from backend/src/controllers/secretController.ts rename to backend/src/controllers/v1/secretController.ts index bfd9aee1f..238b38ced 100644 --- a/backend/src/controllers/secretController.ts +++ b/backend/src/controllers/v1/secretController.ts @@ -1,16 +1,16 @@ import { Request, Response } from 'express'; import * as Sentry from '@sentry/node'; -import { Key } from '../models'; +import { Key, Secret } from '../../models'; import { - pushSecrets as push, + v1PushSecrets as push, pullSecrets as pull, 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'; +} 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 { ciphertextKey: string; @@ -21,6 +21,10 @@ interface PushSecret { ivValue: string; tagValue: string; hashValue: string; + ciphertextComment: string; + ivComment: string; + tagComment: string; + hashComment: string; type: 'shared' | 'personal'; } @@ -169,9 +173,6 @@ export const pullSecrets = async (req: Request, res: Response) => { * @returns */ export const pullSecretsServiceToken = async (req: Request, res: Response) => { - // get (encrypted) secrets from workspace with id [workspaceId] - // service token route - let secrets; let key; try { @@ -225,4 +226,4 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => { secrets: reformatPullSecrets({ secrets }), key }); -}; +}; \ No newline at end of file diff --git a/backend/src/controllers/serviceTokenController.ts b/backend/src/controllers/v1/serviceTokenController.ts similarity index 83% rename from backend/src/controllers/serviceTokenController.ts rename to backend/src/controllers/v1/serviceTokenController.ts index ecc3ca0ca..e21cd7695 100644 --- a/backend/src/controllers/serviceTokenController.ts +++ b/backend/src/controllers/v1/serviceTokenController.ts @@ -1,8 +1,8 @@ import { Request, Response } from 'express'; -import { ServiceToken } from '../models'; -import { createToken } from '../helpers/auth'; -import { ENV_SET } from '../variables'; -import { JWT_SERVICE_SECRET } from '../config'; +import { ServiceToken } from '../../models'; +import { createToken } from '../../helpers/auth'; +import { ENV_SET } from '../../variables'; +import { JWT_SERVICE_SECRET } from '../../config'; /** * Return service token on request @@ -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/controllers/signupController.ts b/backend/src/controllers/v1/signupController.ts similarity index 89% rename from backend/src/controllers/signupController.ts rename to backend/src/controllers/v1/signupController.ts index 90fad4744..62e5a62a3 100644 --- a/backend/src/controllers/signupController.ts +++ b/backend/src/controllers/v1/signupController.ts @@ -1,15 +1,16 @@ import { Request, Response } from 'express'; import * as Sentry from '@sentry/node'; -import { NODE_ENV, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../config'; -import { User, MembershipOrg } from '../models'; -import { completeAccount } from '../helpers/user'; +import { NODE_ENV, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../../config'; +import { User, MembershipOrg } from '../../models'; +import { completeAccount } from '../../helpers/user'; import { sendEmailVerification, checkEmailVerification, initializeDefaultOrg -} from '../helpers/signup'; -import { issueTokens, createToken } from '../helpers/auth'; -import { INVITED, ACCEPTED } from '../variables'; +} from '../../helpers/signup'; +import { issueTokens, createToken } from '../../helpers/auth'; +import { INVITED, ACCEPTED } from '../../variables'; +import axios from 'axios'; /** * Signup step 1: Initialize account for user under email [email] and send a verification code @@ -179,6 +180,21 @@ export const completeAccountSignup = async (req: Request, res: Response) => { token = tokens.token; refreshToken = tokens.refreshToken; + + // sending a welcome email to new users + if (process.env.LOOPS_API_KEY) { + await axios.post("https://app.loops.so/api/v1/events/send", { + "email": email, + "eventName": "Sign Up", + "firstName": firstName, + "lastName": lastName + }, { + headers: { + "Accept": "application/json", + "Authorization": "Bearer " + process.env.LOOPS_API_KEY + }, + }); + } } catch (err) { Sentry.setUser(null); Sentry.captureException(err); diff --git a/backend/src/ee/controllers/stripeController.ts b/backend/src/controllers/v1/stripeController.ts similarity index 100% rename from backend/src/ee/controllers/stripeController.ts rename to backend/src/controllers/v1/stripeController.ts diff --git a/backend/src/controllers/userActionController.ts b/backend/src/controllers/v1/userActionController.ts similarity index 97% rename from backend/src/controllers/userActionController.ts rename to backend/src/controllers/v1/userActionController.ts index 8203aa427..c7a3c2337 100644 --- a/backend/src/controllers/userActionController.ts +++ b/backend/src/controllers/v1/userActionController.ts @@ -1,6 +1,6 @@ import { Request, Response } from 'express'; import * as Sentry from '@sentry/node'; -import { UserAction } from '../models'; +import { UserAction } from '../../models'; /** * Add user action [action] diff --git a/backend/src/controllers/userController.ts b/backend/src/controllers/v1/userController.ts similarity index 100% rename from backend/src/controllers/userController.ts rename to backend/src/controllers/v1/userController.ts diff --git a/backend/src/controllers/workspaceController.ts b/backend/src/controllers/v1/workspaceController.ts similarity index 97% rename from backend/src/controllers/workspaceController.ts rename to backend/src/controllers/v1/workspaceController.ts index a402834d7..bd2ecb15a 100644 --- a/backend/src/controllers/workspaceController.ts +++ b/backend/src/controllers/v1/workspaceController.ts @@ -7,14 +7,14 @@ import { Integration, IntegrationAuth, IUser, - ServiceToken -} from '../models'; + ServiceToken, +} from '../../models'; import { createWorkspace as create, deleteWorkspace as deleteWork -} from '../helpers/workspace'; -import { addMemberships } from '../helpers/membership'; -import { ADMIN, COMPLETED, GRANTED } from '../variables'; +} from '../../helpers/workspace'; +import { addMemberships } from '../../helpers/membership'; +import { ADMIN, COMPLETED, GRANTED } from '../../variables'; /** * Return public keys of members of workspace with id [workspaceId] diff --git a/backend/src/controllers/v2/index.ts b/backend/src/controllers/v2/index.ts new file mode 100644 index 000000000..dc6977c91 --- /dev/null +++ b/backend/src/controllers/v2/index.ts @@ -0,0 +1,5 @@ +import * as workspaceController from './workspaceController'; + +export { + workspaceController +} diff --git a/backend/src/controllers/v2/secretController.ts b/backend/src/controllers/v2/secretController.ts new file mode 100644 index 000000000..e69de29bb diff --git a/backend/src/controllers/v2/workspaceController.ts b/backend/src/controllers/v2/workspaceController.ts new file mode 100644 index 000000000..86693b6c4 --- /dev/null +++ b/backend/src/controllers/v2/workspaceController.ts @@ -0,0 +1,565 @@ +import { Request, Response } from 'express'; +import * as Sentry from '@sentry/node'; +import { + Workspace, + Membership, + MembershipOrg, + Integration, + IntegrationAuth, + Key, + IUser, + ServiceToken, +} from '../../models'; +import { + createWorkspace as create, + deleteWorkspace as deleteWork +} from '../../helpers/workspace'; +import { + v2PushSecrets as push, + pullSecrets as pull, + reformatPullSecrets +} from '../../helpers/secret'; +import { pushKeys } from '../../helpers/key'; +import { addMemberships } from '../../helpers/membership'; +import { postHogClient, EventService } from '../../services'; +import { eventPushSecrets } from '../../events'; +import { ADMIN, COMPLETED, GRANTED, ENV_SET } from '../../variables'; +interface V2PushSecret { + type: string; // personal or shared + secretKeyCiphertext: string; + secretKeyIV: string; + secretKeyTag: string; + secretKeyHash: string; + secretValueCiphertext: string; + secretValueIV: string; + secretValueTag: string; + secretValueHash: string; + secretCommentCiphertext?: string; + secretCommentIV?: string; + secretCommentTag?: string; + secretCommentHash?: string; +} + +/** + * Return public keys of members of workspace with id [workspaceId] + * @param req + * @param res + * @returns + */ +export const getWorkspacePublicKeys = async (req: Request, res: Response) => { + let publicKeys; + try { + const { workspaceId } = req.params; + + publicKeys = ( + await Membership.find({ + workspace: workspaceId + }).populate<{ user: IUser }>('user', 'publicKey') + ) + .filter((m) => m.status === COMPLETED || m.status === GRANTED) + .map((member) => { + return { + publicKey: member.user.publicKey, + userId: member.user._id + }; + }); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to get workspace member public keys' + }); + } + + return res.status(200).send({ + publicKeys + }); +}; + +/** + * Return memberships for workspace with id [workspaceId] + * @param req + * @param res + * @returns + */ +export const getWorkspaceMemberships = async (req: Request, res: Response) => { + let users; + try { + const { workspaceId } = req.params; + + users = await Membership.find({ + workspace: workspaceId + }).populate('user', '+publicKey'); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to get workspace members' + }); + } + + return res.status(200).send({ + users + }); +}; + +/** + * Return workspaces that user is part of + * @param req + * @param res + * @returns + */ +export const getWorkspaces = async (req: Request, res: Response) => { + let workspaces; + try { + workspaces = ( + await Membership.find({ + user: req.user._id + }).populate('workspace') + ).map((m) => m.workspace); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to get workspaces' + }); + } + + return res.status(200).send({ + workspaces + }); +}; + +/** + * Return workspace with id [workspaceId] + * @param req + * @param res + * @returns + */ +export const getWorkspace = async (req: Request, res: Response) => { + let workspace; + try { + const { workspaceId } = req.params; + + workspace = await Workspace.findOne({ + _id: workspaceId + }); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to get workspace' + }); + } + + return res.status(200).send({ + workspace + }); +}; + +/** + * Create new workspace named [workspaceName] under organization with id + * [organizationId] and add user as admin + * @param req + * @param res + * @returns + */ +export const createWorkspace = async (req: Request, res: Response) => { + let workspace; + try { + const { workspaceName, organizationId } = req.body; + + // validate organization membership + const membershipOrg = await MembershipOrg.findOne({ + user: req.user._id, + organization: organizationId + }); + + if (!membershipOrg) { + throw new Error('Failed to validate organization membership'); + } + + if (workspaceName.length < 1) { + throw new Error('Workspace names must be at least 1-character long'); + } + + // create workspace and add user as member + workspace = await create({ + name: workspaceName, + organizationId + }); + + await addMemberships({ + userIds: [req.user._id], + workspaceId: workspace._id.toString(), + roles: [ADMIN], + statuses: [GRANTED] + }); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to create workspace' + }); + } + + return res.status(200).send({ + workspace + }); +}; + +/** + * Delete workspace with id [workspaceId] + * @param req + * @param res + * @returns + */ +export const deleteWorkspace = async (req: Request, res: Response) => { + try { + const { workspaceId } = req.params; + + // delete workspace + await deleteWork({ + id: workspaceId + }); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to delete workspace' + }); + } + + return res.status(200).send({ + message: 'Successfully deleted workspace' + }); +}; + +/** + * Change name of workspace with id [workspaceId] to [name] + * @param req + * @param res + * @returns + */ +export const changeWorkspaceName = async (req: Request, res: Response) => { + let workspace; + try { + const { workspaceId } = req.params; + const { name } = req.body; + + workspace = await Workspace.findOneAndUpdate( + { + _id: workspaceId + }, + { + name + }, + { + new: true + } + ); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to change workspace name' + }); + } + + return res.status(200).send({ + message: 'Successfully changed workspace name', + workspace + }); +}; + +/** + * Return integrations for workspace with id [workspaceId] + * @param req + * @param res + * @returns + */ +export const getWorkspaceIntegrations = async (req: Request, res: Response) => { + let integrations; + try { + const { workspaceId } = req.params; + + integrations = await Integration.find({ + workspace: workspaceId + }); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to get workspace integrations' + }); + } + + return res.status(200).send({ + integrations + }); +}; + +/** + * Return (integration) authorizations for workspace with id [workspaceId] + * @param req + * @param res + * @returns + */ +export const getWorkspaceIntegrationAuthorizations = async ( + req: Request, + res: Response +) => { + let authorizations; + try { + const { workspaceId } = req.params; + + authorizations = await IntegrationAuth.find({ + workspace: workspaceId + }); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to get workspace integration authorizations' + }); + } + + return res.status(200).send({ + authorizations + }); +}; + +/** + * Return service service tokens for workspace [workspaceId] belonging to user + * @param req + * @param res + * @returns + */ +export const getWorkspaceServiceTokens = async ( + req: Request, + res: Response +) => { + let serviceTokens; + try { + const { workspaceId } = req.params; + + serviceTokens = await ServiceToken.find({ + user: req.user._id, + workspace: workspaceId + }); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to get workspace service tokens' + }); + } + + return res.status(200).send({ + serviceTokens + }); +} + +/** + * Upload (encrypted) secrets to workspace with id [workspaceId] + * for environment [environment] + * @param req + * @param res + * @returns + */ +export const pushWorkspaceSecrets = async (req: Request, res: Response) => { + // upload (encrypted) secrets to workspace with id [workspaceId] + + try { + let { secrets }: { secrets: V2PushSecret[] } = req.body; + const { keys, environment, channel } = req.body; + const { workspaceId } = req.params; + + // validate environment + if (!ENV_SET.has(environment)) { + throw new Error('Failed to validate environment'); + } + + // sanitize secrets + secrets = secrets.filter( + (s: V2PushSecret) => s.secretKeyCiphertext !== '' && s.secretValueCiphertext !== '' + ); + + await push({ + userId: req.user._id, + workspaceId, + environment, + secrets + }); + + await pushKeys({ + userId: req.user._id, + workspaceId, + keys + }); + + + if (postHogClient) { + postHogClient.capture({ + event: 'secrets pushed', + distinctId: req.user.email, + properties: { + numberOfSecrets: secrets.length, + environment, + workspaceId, + channel: channel ? channel : 'cli' + } + }); + } + + // trigger event - push secrets + EventService.handleEvent({ + event: eventPushSecrets({ + workspaceId + }) + }); + + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to upload workspace secrets' + }); + } + + return res.status(200).send({ + message: 'Successfully uploaded workspace secrets' + }); +}; + +/** + * Return (encrypted) secrets for workspace with id [workspaceId] + * for environment [environment] and (encrypted) workspace key + * @param req + * @param res + * @returns + */ +export const pullSecrets = async (req: Request, res: Response) => { + // TODO: only return secrets, do not return workspace key + + let secrets; + let key; + try { + const environment: string = req.query.environment as string; + const channel: string = req.query.channel as string; + const { workspaceId } = req.params; + + // validate environment + if (!ENV_SET.has(environment)) { + throw new Error('Failed to validate environment'); + } + + secrets = await pull({ + userId: req.user._id.toString(), + workspaceId, + environment + }); + + key = await Key.findOne({ + workspace: workspaceId, + receiver: req.user._id + }) + .sort({ createdAt: -1 }) + .populate('sender', '+publicKey'); + + if (channel !== 'cli') { + secrets = reformatPullSecrets({ secrets }); + } + + if (postHogClient) { + // capture secrets pushed event in production + postHogClient.capture({ + distinctId: req.user.email, + event: 'secrets pulled', + properties: { + numberOfSecrets: secrets.length, + environment, + workspaceId, + channel: channel ? channel : 'cli' + } + }); + } + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to pull workspace secrets' + }); + } + + return res.status(200).send({ + secrets, + key + }); +}; + +// TODO: modify based on upcoming serviceTokenData changes + +/** + * Return (encrypted) secrets for workspace with id [workspaceId] + * for environment [environment] and (encrypted) workspace key + * via service token + * @param req + * @param res + * @returns + */ + export const pullSecretsServiceToken = async (req: Request, res: Response) => { + let secrets; + let key; + try { + const environment: string = req.query.environment as string; + const channel: string = req.query.channel as string; + const { workspaceId } = req.params; + + // validate environment + if (!ENV_SET.has(environment)) { + throw new Error('Failed to validate environment'); + } + + secrets = await pull({ + userId: req.serviceToken.user._id.toString(), + workspaceId, + environment + }); + + key = { + encryptedKey: req.serviceToken.encryptedKey, + nonce: req.serviceToken.nonce, + sender: { + publicKey: req.serviceToken.publicKey + }, + receiver: req.serviceToken.user, + workspace: req.serviceToken.workspace + }; + + if (postHogClient) { + // capture secrets pulled event in production + postHogClient.capture({ + distinctId: req.serviceToken.user.email, + event: 'secrets pulled', + properties: { + numberOfSecrets: secrets.length, + environment, + workspaceId, + channel: channel ? channel : 'cli' + } + }); + } + } catch (err) { + Sentry.setUser({ email: req.serviceToken.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to pull workspace secrets' + }); + } + + return res.status(200).send({ + secrets: reformatPullSecrets({ secrets }), + key + }); +}; \ No newline at end of file diff --git a/backend/src/ee/controllers/index.ts b/backend/src/ee/controllers/index.ts deleted file mode 100644 index e4fb89a8e..000000000 --- a/backend/src/ee/controllers/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import * as stripeController from './stripeController'; - -export { - stripeController -} \ No newline at end of file diff --git a/backend/src/ee/controllers/v1/index.ts b/backend/src/ee/controllers/v1/index.ts new file mode 100644 index 000000000..23880070d --- /dev/null +++ b/backend/src/ee/controllers/v1/index.ts @@ -0,0 +1,9 @@ +import * as stripeController from './stripeController'; +import * as secretController from './secretController'; +import * as workspaceController from './workspaceController'; + +export { + stripeController, + secretController, + workspaceController +} \ No newline at end of file diff --git a/backend/src/ee/controllers/v1/secretController.ts b/backend/src/ee/controllers/v1/secretController.ts new file mode 100644 index 000000000..b2d66ab33 --- /dev/null +++ b/backend/src/ee/controllers/v1/secretController.ts @@ -0,0 +1,35 @@ +import { Request, Response } from 'express'; +import * as Sentry from '@sentry/node'; +import { SecretVersion } from '../../models'; + +/** + * Return secret versions for secret with id [secretId] + * @param req + * @param res + */ + export const getSecretVersions = async (req: Request, res: Response) => { + let secretVersions; + try { + const { secretId } = req.params; + + const offset: number = parseInt(req.query.offset as string); + const limit: number = parseInt(req.query.limit as string); + + secretVersions = await SecretVersion.find({ + secret: secretId + }) + .skip(offset) + .limit(limit); + + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to get secret versions' + }); + } + + return res.status(200).send({ + secretVersions + }); +} \ No newline at end of file diff --git a/backend/src/controllers/stripeController.ts b/backend/src/ee/controllers/v1/stripeController.ts similarity index 91% rename from backend/src/controllers/stripeController.ts rename to backend/src/ee/controllers/v1/stripeController.ts index 99a85ee2b..faef14cea 100644 --- a/backend/src/controllers/stripeController.ts +++ b/backend/src/ee/controllers/v1/stripeController.ts @@ -1,7 +1,7 @@ import { Request, Response } from 'express'; import * as Sentry from '@sentry/node'; import Stripe from 'stripe'; -import { STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET } from '../config'; +import { STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET } from '../../../config'; const stripe = new Stripe(STRIPE_SECRET_KEY, { apiVersion: '2022-08-01' }); diff --git a/backend/src/ee/controllers/v1/workspaceController.ts b/backend/src/ee/controllers/v1/workspaceController.ts new file mode 100644 index 000000000..8b7ba422e --- /dev/null +++ b/backend/src/ee/controllers/v1/workspaceController.ts @@ -0,0 +1,35 @@ +import { Request, Response } from 'express'; +import * as Sentry from '@sentry/node'; +import { SecretSnapshot } from '../../models'; + +/** + * Return secret snapshots for workspace with id [workspaceId] + * @param req + * @param res + */ + export const getWorkspaceSecretSnapshots = async (req: Request, res: Response) => { + let secretSnapshots; + try { + const { workspaceId } = req.params; + + const offset: number = parseInt(req.query.offset as string); + const limit: number = parseInt(req.query.limit as string); + + secretSnapshots = await SecretSnapshot.find({ + workspace: workspaceId + }) + .skip(offset) + .limit(limit); + + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to get secret snapshots' + }); + } + + return res.status(200).send({ + secretSnapshots + }); +} \ No newline at end of file diff --git a/backend/src/ee/helpers/license.ts b/backend/src/ee/helpers/license.ts deleted file mode 100644 index 256bdc23a..000000000 --- a/backend/src/ee/helpers/license.ts +++ /dev/null @@ -1,21 +0,0 @@ - -/** - * @param {Object} obj - * @param {Object} obj.licenseKey - Infisical license key - */ -const checkLicenseKey = ({ - licenseKey -}: { - licenseKey: string -}) => { - try { - // TODO - - } catch (err) { - - } -} - -export { - checkLicenseKey -} \ No newline at end of file diff --git a/backend/src/ee/helpers/secret.ts b/backend/src/ee/helpers/secret.ts new file mode 100644 index 000000000..a688a108f --- /dev/null +++ b/backend/src/ee/helpers/secret.ts @@ -0,0 +1,74 @@ +import * as Sentry from '@sentry/node'; +import { + Secret +} from '../../models'; +import { + SecretSnapshot, + SecretVersion, + ISecretVersion +} from '../models'; + +/** + * Save a copy of the current state of secrets in workspace with id + * [workspaceId] under a new snapshot with incremented version under the + * secretsnapshots collection. + * @param {Object} obj + * @param {String} obj.workspaceId + */ + const takeSecretSnapshotHelper = async ({ + workspaceId +}: { + workspaceId: string; +}) => { + try { + const secrets = await Secret.find({ + workspace: workspaceId + }); + + const latestSecretSnapshot = await SecretSnapshot.findOne({ + workspace: workspaceId + }).sort({ version: -1 }); + + if (!latestSecretSnapshot) { + // case: no snapshots exist for workspace -> create first snapshot + await new SecretSnapshot({ + workspace: workspaceId, + version: 1, + secrets + }).save(); + + return; + } + + // case: snapshots exist for workspace + await new SecretSnapshot({ + workspace: workspaceId, + version: latestSecretSnapshot.version + 1, + secrets + }).save(); + + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to take a secret snapshot'); + } +} + +const addSecretVersionsHelper = async ({ + secretVersions +}: { + secretVersions: ISecretVersion[] +}) => { + try { + await SecretVersion.insertMany(secretVersions); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to add secret versions'); + } +} + +export { + takeSecretSnapshotHelper, + addSecretVersionsHelper +} \ No newline at end of file diff --git a/backend/src/ee/models/index.ts b/backend/src/ee/models/index.ts new file mode 100644 index 000000000..35d41c19a --- /dev/null +++ b/backend/src/ee/models/index.ts @@ -0,0 +1,9 @@ +import SecretSnapshot, { ISecretSnapshot } from "./secretSnapshot"; +import SecretVersion, { ISecretVersion } from "./secretVersion"; + +export { + SecretSnapshot, + ISecretSnapshot, + SecretVersion, + ISecretVersion +} \ No newline at end of file diff --git a/backend/src/ee/models/secretSnapshot.ts b/backend/src/ee/models/secretSnapshot.ts new file mode 100644 index 000000000..69633a92e --- /dev/null +++ b/backend/src/ee/models/secretSnapshot.ts @@ -0,0 +1,109 @@ +import { Schema, model, Types } from 'mongoose'; +import { + SECRET_SHARED, + SECRET_PERSONAL, + ENV_DEV, + ENV_TESTING, + ENV_STAGING, + ENV_PROD +} from '../../variables'; + +export interface ISecretSnapshot { + workspace: Types.ObjectId; + version: number; + secrets: { + version: number; + workspace: Types.ObjectId; + type: string; + user: Types.ObjectId; + environment: string; + secretKeyCiphertext: string; + secretKeyIV: string; + secretKeyTag: string; + secretKeyHash: string; + secretValueCiphertext: string; + secretValueIV: string; + secretValueTag: string; + secretValueHash: string; + }[] +} + +const secretSnapshotSchema = new Schema( + { + workspace: { + type: Schema.Types.ObjectId, + ref: 'Workspace', + required: true + }, + version: { + type: Number, + required: true + }, + secrets: [{ + version: { + type: Number, + default: 1, + required: true + }, + workspace: { + type: Schema.Types.ObjectId, + ref: 'Workspace', + required: true + }, + type: { + type: String, + enum: [SECRET_SHARED, SECRET_PERSONAL], + required: true + }, + user: { + // user associated with the personal secret + type: Schema.Types.ObjectId, + ref: 'User' + }, + environment: { + type: String, + enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD], + required: true + }, + secretKeyCiphertext: { + type: String, + required: true + }, + secretKeyIV: { + type: String, // symmetric + required: true + }, + secretKeyTag: { + type: String, // symmetric + required: true + }, + secretKeyHash: { + type: String, + required: true + }, + secretValueCiphertext: { + type: String, + required: true + }, + secretValueIV: { + type: String, // symmetric + required: true + }, + secretValueTag: { + type: String, // symmetric + required: true + }, + secretValueHash: { + type: String, + required: true + } + }] + }, + { + timestamps: true + } +); + +const SecretSnapshot = model('SecretSnapshot', secretSnapshotSchema); + +export default SecretSnapshot; \ No newline at end of file diff --git a/backend/src/ee/models/secretVersion.ts b/backend/src/ee/models/secretVersion.ts new file mode 100644 index 000000000..a93a037f6 --- /dev/null +++ b/backend/src/ee/models/secretVersion.ts @@ -0,0 +1,75 @@ +import { Schema, model, Types } from 'mongoose'; + +export interface ISecretVersion { + _id?: Types.ObjectId; + secret: Types.ObjectId; + version: number; + isDeleted: boolean; + secretKeyCiphertext: string; + secretKeyIV: string; + secretKeyTag: string; + secretKeyHash: string; + secretValueCiphertext: string; + secretValueIV: string; + secretValueTag: string; + secretValueHash: string; +} + +const secretVersionSchema = new Schema( + { + secret: { // could be deleted + type: Schema.Types.ObjectId, + ref: 'Secret', + required: true + }, + version: { + type: Number, + default: 1, + required: true + }, + isDeleted: { + type: Boolean, + default: false, + required: true + }, + secretKeyCiphertext: { + type: String, + required: true + }, + secretKeyIV: { + type: String, // symmetric + required: true + }, + secretKeyTag: { + type: String, // symmetric + required: true + }, + secretKeyHash: { + type: String, + required: true + }, + secretValueCiphertext: { + type: String, + required: true + }, + secretValueIV: { + type: String, // symmetric + required: true + }, + secretValueTag: { + type: String, // symmetric + required: true + }, + secretValueHash: { + type: String, + required: true + } + }, + { + timestamps: true + } +); + +const SecretVersion = model('SecretVersion', secretVersionSchema); + +export default SecretVersion; \ No newline at end of file diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts new file mode 100644 index 000000000..960665f4a --- /dev/null +++ b/backend/src/ee/routes/v1/index.ts @@ -0,0 +1,7 @@ +import secret from './secret'; +import workspace from './workspace'; + +export { + secret, + workspace +} \ No newline at end of file diff --git a/backend/src/ee/routes/v1/secret.ts b/backend/src/ee/routes/v1/secret.ts new file mode 100644 index 000000000..a866a6320 --- /dev/null +++ b/backend/src/ee/routes/v1/secret.ts @@ -0,0 +1,26 @@ +import express from 'express'; +const router = express.Router(); +import { + requireAuth, + requireWorkspaceAuth, + validateRequest +} from '../../../middleware'; +import { body, query, param } from 'express-validator'; +import { secretController } from '../../controllers/v1'; +import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../../variables'; + +router.get( + '/:secretId/secret-versions', + requireAuth, + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [COMPLETED, GRANTED] + }), + param('secretId').exists().trim(), + query('offset').exists().isInt(), + query('limit').exists().isInt(), + validateRequest, + secretController.getSecretVersions +); + +export default router; \ No newline at end of file diff --git a/backend/src/ee/routes/stripe.ts b/backend/src/ee/routes/v1/stripe.ts similarity index 71% rename from backend/src/ee/routes/stripe.ts rename to backend/src/ee/routes/v1/stripe.ts index 6f89f5655..02d68c4ea 100644 --- a/backend/src/ee/routes/stripe.ts +++ b/backend/src/ee/routes/v1/stripe.ts @@ -1,6 +1,6 @@ import express from 'express'; const router = express.Router(); -import { stripeController } from '../controllers'; +import { stripeController } from '../../controllers/v1'; router.post('/webhook', stripeController.handleWebhook); diff --git a/backend/src/ee/routes/v1/workspace.ts b/backend/src/ee/routes/v1/workspace.ts new file mode 100644 index 000000000..6756269e5 --- /dev/null +++ b/backend/src/ee/routes/v1/workspace.ts @@ -0,0 +1,27 @@ +import express from 'express'; +const router = express.Router(); +import { + requireAuth, + requireWorkspaceAuth, + validateRequest +} from '../../../middleware'; +import { param, query } from 'express-validator'; +import { ADMIN, MEMBER, GRANTED } from '../../../variables'; +import { workspaceController } from '../../controllers/v1'; + +router.get( + '/:workspaceId/secret-snapshots', + requireAuth, + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [GRANTED] + }), + param('workspaceId').exists().trim(), + query('offset').exists().isInt(), + query('limit').exists().isInt(), + validateRequest, + workspaceController.getWorkspaceSecretSnapshots +); + + +export default router; \ No newline at end of file diff --git a/backend/src/ee/services/EELicenseService.ts b/backend/src/ee/services/EELicenseService.ts new file mode 100644 index 000000000..f31482dde --- /dev/null +++ b/backend/src/ee/services/EELicenseService.ts @@ -0,0 +1,19 @@ +import { LICENSE_KEY } from '../../config'; + +/** + * Class to handle Enterprise Edition license actions + */ +class EELicenseService { + + private readonly _isLicenseValid: boolean; + + constructor(licenseKey: string) { + this._isLicenseValid = true; + } + + public get isLicenseValid(): boolean { + return this._isLicenseValid; + } +} + +export default new EELicenseService(LICENSE_KEY); \ No newline at end of file diff --git a/backend/src/ee/services/EESecretService.ts b/backend/src/ee/services/EESecretService.ts new file mode 100644 index 000000000..643f763f1 --- /dev/null +++ b/backend/src/ee/services/EESecretService.ts @@ -0,0 +1,47 @@ +import { ISecretVersion } from '../models'; +import { + takeSecretSnapshotHelper, + addSecretVersionsHelper +} from '../helpers/secret'; +import EELicenseService from './EELicenseService'; + +/** + * Class to handle Enterprise Edition secret actions + */ +class EESecretService { + + /** + * Save a copy of the current state of secrets in workspace with id + * [workspaceId] under a new snapshot with incremented version under the + * SecretSnapshot collection. + * Requires a valid license key [licenseKey] + * @param {Object} obj + * @param {String} obj.workspaceId + */ + static async takeSecretSnapshot({ + workspaceId + }: { + workspaceId: string; + }) { + if (!EELicenseService.isLicenseValid) return; + await takeSecretSnapshotHelper({ workspaceId }); + } + + /** + * Adds secret versions [secretVersions] to the SecretVersion collection. + * @param {Object} obj + * @param {SecretVersion} obj.secretVersions + */ + static async addSecretVersions({ + secretVersions + }: { + secretVersions: ISecretVersion[]; + }) { + if (!EELicenseService.isLicenseValid) return; + await addSecretVersionsHelper({ + secretVersions + }); + } +} + +export default EESecretService; \ No newline at end of file diff --git a/backend/src/ee/services/index.ts b/backend/src/ee/services/index.ts new file mode 100644 index 000000000..3cec256bb --- /dev/null +++ b/backend/src/ee/services/index.ts @@ -0,0 +1,7 @@ +import EELicenseService from "./EELicenseService"; +import EESecretService from "./EESecretService"; + +export { + EELicenseService, + EESecretService +} \ No newline at end of file diff --git a/backend/src/helpers/bot.ts b/backend/src/helpers/bot.ts index abaf73af4..b3f276b53 100644 --- a/backend/src/helpers/bot.ts +++ b/backend/src/helpers/bot.ts @@ -12,7 +12,6 @@ import { decryptSymmetric, decryptAsymmetric } from '../utils/crypto'; -import { decryptSecrets } from '../helpers/secret'; import { ENCRYPTION_KEY } from '../config'; import { SECRET_SHARED } from '../variables'; diff --git a/backend/src/helpers/integration.ts b/backend/src/helpers/integration.ts index ccfd72a53..d92156ece 100644 --- a/backend/src/helpers/integration.ts +++ b/backend/src/helpers/integration.ts @@ -2,18 +2,18 @@ import * as Sentry from '@sentry/node'; import { Bot, Integration, - IIntegration, IntegrationAuth, - IIntegrationAuth } from '../models'; import { exchangeCode, exchangeRefresh, syncSecrets } from '../integrations'; -import { BotService, IntegrationService } from '../services'; +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; @@ -176,12 +176,13 @@ const syncIntegrationsHelper = async ({ */ const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: string }) => { let refreshToken; + try { const integrationAuth = await IntegrationAuth .findById(integrationAuthId) .select('+refreshCiphertext +refreshIV +refreshTag'); - if (!integrationAuth) throw new Error('Failed to find integration auth'); + if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'}); refreshToken = await BotService.decryptSymmetric({ workspaceId: integrationAuth.workspace.toString(), @@ -193,7 +194,10 @@ const syncIntegrationsHelper = async ({ } catch (err) { Sentry.setUser(null); Sentry.captureException(err); - throw new Error('Failed to get integration refresh token'); + if(err instanceof RequestError) + throw err + else + throw new Error('Failed to get integration refresh token'); } return refreshToken; @@ -209,12 +213,13 @@ const syncIntegrationsHelper = async ({ */ 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 new Error('Failed to find integration auth'); + if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'}); accessToken = await BotService.decryptSymmetric({ workspaceId: integrationAuth.workspace.toString(), @@ -240,7 +245,10 @@ const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrati } catch (err) { Sentry.setUser(null); Sentry.captureException(err); - throw new Error('Failed to get integration access token'); + if(err instanceof RequestError) + throw err + else + throw new Error('Failed to get integration access token'); } return accessToken; diff --git a/backend/src/helpers/membership.ts b/backend/src/helpers/membership.ts index 1ff542f2a..b06460cde 100644 --- a/backend/src/helpers/membership.ts +++ b/backend/src/helpers/membership.ts @@ -21,6 +21,7 @@ const validateMembership = async ({ }) => { 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, diff --git a/backend/src/helpers/nodemailer.ts b/backend/src/helpers/nodemailer.ts index 1c92af93f..958342aae 100644 --- a/backend/src/helpers/nodemailer.ts +++ b/backend/src/helpers/nodemailer.ts @@ -2,40 +2,10 @@ import fs from 'fs'; import path from 'path'; import handlebars from 'handlebars'; import nodemailer from 'nodemailer'; -import { - SMTP_HOST, - SMTP_PORT, - SMTP_NAME, - SMTP_USERNAME, - SMTP_PASSWORD -} from '../config'; -import SMTPConnection from 'nodemailer/lib/smtp-connection'; +import { SMTP_FROM_NAME, SMTP_FROM_ADDRESS } from '../config'; 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 - }; -} -// create nodemailer transporter -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}` - ); - }); +let smtpTransporter: nodemailer.Transporter; /** * @param {Object} obj @@ -63,8 +33,8 @@ const sendMail = async ({ const temp = handlebars.compile(html); const htmlToSend = temp(substitutions); - await transporter.sendMail({ - from: `"${SMTP_NAME}" <${SMTP_USERNAME}>`, + await smtpTransporter.sendMail({ + from: `"${SMTP_FROM_NAME}" <${SMTP_FROM_ADDRESS}>`, to: recipients.join(', '), subject: subjectLine, html: htmlToSend @@ -75,4 +45,8 @@ const sendMail = async ({ } }; -export { sendMail }; +const setTransporter = (transporter: nodemailer.Transporter) => { + smtpTransporter = transporter; +}; + +export { sendMail, setTransporter }; diff --git a/backend/src/helpers/secret.ts b/backend/src/helpers/secret.ts index 042aba4fa..f055971ae 100644 --- a/backend/src/helpers/secret.ts +++ b/backend/src/helpers/secret.ts @@ -1,12 +1,21 @@ import * as Sentry from '@sentry/node'; import { Secret, - ISecret + ISecret, } from '../models'; +import { + EESecretService +} from '../ee/services'; +import { + SecretVersion +} from '../ee/models'; +import { + takeSecretSnapshotHelper +} from '../ee/helpers/secret'; import { decryptSymmetric } from '../utils/crypto'; import { SECRET_SHARED, SECRET_PERSONAL } from '../variables'; -interface PushSecret { +interface V1PushSecret { ciphertextKey: string; ivKey: string; tagKey: string; @@ -15,11 +24,31 @@ interface PushSecret { ivValue: string; tagValue: string; hashValue: string; + ciphertextComment: string; + ivComment: string; + tagComment: string; + hashComment: string; type: 'shared' | 'personal'; } +interface V2PushSecret { + type: string; // personal or shared + secretKeyCiphertext: string; + secretKeyIV: string; + secretKeyTag: string; + secretKeyHash: string; + secretValueCiphertext: string; + secretValueIV: string; + secretValueTag: string; + secretValueHash: string; + secretCommentCiphertext?: string; + secretCommentIV?: string; + secretCommentTag?: string; + secretCommentHash?: string; +} + interface Update { - [index: string]: string; + [index: string]: any; } type DecryptSecretType = 'text' | 'object' | 'expanded'; @@ -35,7 +64,7 @@ type DecryptSecretType = 'text' | 'object' | 'expanded'; * @param {String} obj.environment - environment for secrets * @param {Object[]} obj.secrets - secrets to push */ -const pushSecrets = async ({ +const v1PushSecrets = async ({ userId, workspaceId, environment, @@ -44,8 +73,9 @@ const pushSecrets = async ({ userId: string; workspaceId: string; environment: string; - secrets: PushSecret[]; + secrets: V1PushSecret[]; }): Promise => { + // TODO: clean up function and fix up types try { // construct useful data structures const oldSecrets = await pullSecrets({ @@ -53,74 +83,133 @@ const pushSecrets = async ({ workspaceId, environment }); - const oldSecretsObj: any = oldSecrets.reduce((accumulator, s: any) => { - return { ...accumulator, [s.secretKeyHash]: s }; - }, {}); - const newSecretsObj = secrets.reduce((accumulator, s) => { - return { ...accumulator, [s.hashKey]: s }; - }, {}); + + const oldSecretsObj: any = oldSecrets.reduce((accumulator, s: any) => + ({ ...accumulator, [`${s.type}-${s.secretKeyHash}`]: s }) + , {}); + const newSecretsObj: any = secrets.reduce((accumulator, s) => + ({ ...accumulator, [`${s.type}-${s.hashKey}`]: s }) + , {}); // handle deleting secrets - const toDelete = oldSecrets.filter( - (s: ISecret) => !(s.secretKeyHash in newSecretsObj) - ); + const toDelete = oldSecrets + .filter( + (s: ISecret) => !(`${s.type}-${s.secretKeyHash}` in newSecretsObj) + ) + .map((s) => s._id); if (toDelete.length > 0) { await Secret.deleteMany({ - _id: { $in: toDelete.map((s) => s._id) } + _id: { $in: toDelete } + }); + + await SecretVersion.updateMany({ + secret: { $in: toDelete } + }, { + isDeleted: true }); } - - // handle modifying secrets where type or value changed - const operations = secrets + + const toUpdate = oldSecrets .filter((s) => { - if (s.hashKey in oldSecretsObj) { - if (s.hashValue !== oldSecretsObj[s.hashKey].secretValueHash) { - // case: filter secrets where value changed + if (`${s.type}-${s.secretKeyHash}` in newSecretsObj) { + if (s.secretValueHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].hashValue + || s.secretCommentHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].hashComment) { + // case: filter secrets where value or comment changed return true; } - if (s.type !== oldSecretsObj[s.hashKey].type) { - // case: filter secrets where type changed + if (!s.version) { + // case: filter (legacy) secrets that were not versioned return true; } } - + return false; - }) + }); + + const operations = toUpdate .map((s) => { + const { + ciphertextValue, + ivValue, + tagValue, + hashValue, + ciphertextComment, + ivComment, + tagComment, + hashComment + } = newSecretsObj[`${s.type}-${s.secretKeyHash}`]; + const update: Update = { - type: s.type, - secretValueCiphertext: s.ciphertextValue, - secretValueIV: s.ivValue, - secretValueTag: s.tagValue, - secretValueHash: s.hashValue - }; + secretValueCiphertext: ciphertextValue, + secretValueIV: ivValue, + secretValueTag: tagValue, + secretValueHash: hashValue, + secretCommentCiphertext: ciphertextComment, + secretCommentIV: ivComment, + secretCommentTag: tagComment, + secretCommentHash: hashComment, + } + + if (!s.version) { + // case: (legacy) secret was not versioned + update.version = 1; + } else { + update['$inc'] = { + version: 1 + } + } if (s.type === SECRET_PERSONAL) { - // attach user assocaited with the personal secret + // attach user associated with the personal secret update['user'] = userId; } return { updateOne: { filter: { - workspace: workspaceId, - _id: oldSecretsObj[s.hashKey]._id + _id: oldSecretsObj[`${s.type}-${s.secretKeyHash}`]._id }, update } }; }); - const a = await Secret.bulkWrite(operations as any); + await Secret.bulkWrite(operations as any); + + // (EE) add secret versions for updated secrets + await EESecretService.addSecretVersions({ + secretVersions: toUpdate.map(({ + _id, + version, + type, + secretKeyHash, + }) => { + const newSecret = newSecretsObj[`${type}-${secretKeyHash}`]; + return ({ + secret: _id, + version: version ? version + 1 : 1, + isDeleted: false, + secretKeyCiphertext: newSecret.ciphertextKey, + secretKeyIV: newSecret.ivKey, + secretKeyTag: newSecret.tagKey, + secretKeyHash: newSecret.hashKey, + secretValueCiphertext: newSecret.ciphertextValue, + secretValueIV: newSecret.ivValue, + secretValueTag: newSecret.tagValue, + secretValueHash: newSecret.hashValue + }) + }) + }); // handle adding new secrets - const toAdd = secrets.filter((s) => !(s.hashKey in oldSecretsObj)); + const toAdd = secrets.filter((s) => !(`${s.type}-${s.hashKey}` in oldSecretsObj)); if (toAdd.length > 0) { // add secrets - await Secret.insertMany( + const newSecrets = await Secret.insertMany( toAdd.map((s, idx) => { - let obj: any = { + const obj: any = { + version: 1, workspace: workspaceId, type: toAdd[idx].type, environment, @@ -131,7 +220,11 @@ const pushSecrets = async ({ secretValueCiphertext: s.ciphertextValue, secretValueIV: s.ivValue, secretValueTag: s.tagValue, - secretValueHash: s.hashValue + secretValueHash: s.hashValue, + secretCommentCiphertext: s.ciphertextComment, + secretCommentIV: s.ivComment, + secretCommentTag: s.tagComment, + secretCommentHash: s.hashComment }; if (toAdd[idx].type === 'personal') { @@ -141,7 +234,282 @@ const pushSecrets = async ({ return obj; }) ); + + // (EE) add secret versions for new secrets + EESecretService.addSecretVersions({ + secretVersions: newSecrets.map(({ + _id, + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretKeyHash, + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretValueHash + }) => ({ + secret: _id, + version: 1, + isDeleted: false, + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretKeyHash, + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretValueHash + })) + }); } + + // (EE) take a secret snapshot + await EESecretService.takeSecretSnapshot({ + workspaceId + }) + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to push shared and personal secrets'); + } +}; + +/** + * Push secrets for user with id [userId] to workspace + * with id [workspaceId] with environment [environment]. Follow steps: + * 1. Handle shared secrets (insert, delete) + * 2. handle personal secrets (insert, delete) + * @param {Object} obj + * @param {String} obj.userId - id of user to push secrets for + * @param {String} obj.workspaceId - id of workspace to push to + * @param {String} obj.environment - environment for secrets + * @param {Object[]} obj.secrets - secrets to push + */ + const v2PushSecrets = async ({ + userId, + workspaceId, + environment, + secrets +}: { + userId: string; + workspaceId: string; + environment: string; + secrets: V2PushSecret[]; +}): Promise => { + // TODO: clean up function and fix up types + try { + // construct useful data structures + const oldSecrets = await pullSecrets({ + userId, + workspaceId, + environment + }); + + const oldSecretsObj: any = oldSecrets.reduce((accumulator, s: any) => + ({ ...accumulator, [`${s.type}-${s.secretKeyHash}`]: s }) + , {}); + const newSecretsObj: any = secrets.reduce((accumulator, s) => + ({ ...accumulator, [`${s.type}-${s.secretKeyHash}`]: s }) + , {}); + + // handle deleting secrets + const toDelete = oldSecrets + .filter( + (s: ISecret) => !(`${s.type}-${s.secretKeyHash}` in newSecretsObj) + ) + .map((s) => s._id); + if (toDelete.length > 0) { + await Secret.deleteMany({ + _id: { $in: toDelete } + }); + + await SecretVersion.updateMany({ + secret: { $in: toDelete } + }, { + isDeleted: true + }); + } + + const toUpdate = oldSecrets + .filter((s) => { + if (`${s.type}-${s.secretKeyHash}` in newSecretsObj) { + if (s.secretValueHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].secretValueHash + || s.secretCommentHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].secretCommentHash) { + // case: filter secrets where value or comment changed + return true; + } + + if (!s.version) { + // case: filter (legacy) secrets that were not versioned + return true; + } + } + + return false; + }); + + const operations = toUpdate + .map((s) => { + const { + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretValueHash, + secretCommentCiphertext, + secretCommentIV, + secretCommentTag, + secretCommentHash, + } = newSecretsObj[`${s.type}-${s.secretKeyHash}`]; + + const update: Update = { + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretValueHash, + secretCommentCiphertext, + secretCommentIV, + secretCommentTag, + secretCommentHash, + } + + if (!s.version) { + // case: (legacy) secret was not versioned + update.version = 1; + } else { + update['$inc'] = { + version: 1 + } + } + + if (s.type === SECRET_PERSONAL) { + // attach user associated with the personal secret + update['user'] = userId; + } + + return { + updateOne: { + filter: { + _id: oldSecretsObj[`${s.type}-${s.secretKeyHash}`]._id + }, + update + } + }; + }); + await Secret.bulkWrite(operations as any); + + // (EE) add secret versions for updated secrets + await EESecretService.addSecretVersions({ + secretVersions: toUpdate.map((s) => { + const { + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretKeyHash, + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretValueHash, + secretCommentCiphertext, + secretCommentIV, + secretCommentTag, + secretCommentHash, + } = newSecretsObj[`${s.type}-${s.secretKeyHash}`]; + + return ({ + secret: s._id, + version: s.version ? s.version + 1 : 1, + isDeleted: false, + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretKeyHash, + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretValueHash + }) + }) + }); + + // handle adding new secrets + const toAdd = secrets.filter((s) => !(`${s.type}-${s.secretKeyHash}` in oldSecretsObj)); + + if (toAdd.length > 0) { + // add secrets + const newSecrets = await Secret.insertMany( + toAdd.map(({ + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretKeyHash, + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretValueHash, + secretCommentCiphertext, + secretCommentIV, + secretCommentTag, + secretCommentHash, + }, idx) => { + const obj: any = { + version: 1, + workspace: workspaceId, + type: toAdd[idx].type, + environment, + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretKeyHash, + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretValueHash, + secretCommentCiphertext, + secretCommentIV, + secretCommentTag, + secretCommentHash + }; + + if (toAdd[idx].type === 'personal') { + obj['user' as keyof typeof obj] = userId; + } + + return obj; + }) + ); + + // (EE) add secret versions for new secrets + EESecretService.addSecretVersions({ + secretVersions: newSecrets.map(({ + _id, + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretKeyHash, + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretValueHash + }) => ({ + secret: _id, + version: 1, + isDeleted: false, + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretKeyHash, + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretValueHash + })) + }); + } + + // (EE) take a secret snapshot + await EESecretService.takeSecretSnapshot({ + workspaceId + }) } catch (err) { Sentry.setUser(null); Sentry.captureException(err); @@ -222,6 +590,13 @@ const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => { iv: s.secretValueIV, tag: s.secretValueTag, hash: s.secretValueHash + }, + secretComment: { + workspace: s.workspace, + ciphertext: s.secretCommentCiphertext, + iv: s.secretCommentIV, + tag: s.secretCommentTag, + hash: s.secretCommentHash } })); } catch (err) { @@ -233,71 +608,9 @@ const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => { return reformatedSecrets; }; -/** - * Return decrypted secrets in format [format] - * @param {Object} obj - * @param {Object[]} obj.secrets - array of (encrypted) secret key-value pair objects - * @param {String} obj.key - symmetric key to decrypt secret key-value pairs - * @param {String} obj.format - desired return format that is either "text," "object," or "expanded" - * @return {String|Object} (decrypted) secrets also called the content - */ -const decryptSecrets = ({ - secrets, - key, - format -}: { - secrets: PushSecret[]; - key: string; - format: DecryptSecretType; -}) => { - // init content - let content: any = format === 'text' ? '' : {}; - - // decrypt secrets - secrets.forEach((s, idx) => { - const secretKey = decryptSymmetric({ - ciphertext: s.ciphertextKey, - iv: s.ivKey, - tag: s.tagKey, - key - }); - - const secretValue = decryptSymmetric({ - ciphertext: s.ciphertextValue, - iv: s.ivValue, - tag: s.tagValue, - key - }); - - switch (format) { - case 'text': - content += secretKey; - content += '='; - content += secretValue; - - if (idx < secrets.length) { - content += '\n'; - } - break; - case 'object': - content[secretKey] = secretValue; - break; - case 'expanded': - content[secretKey] = { - ...s, - plaintextKey: secretKey, - plaintextValue: secretValue - }; - break; - } - }); - - return content; -}; - export { - pushSecrets, + v1PushSecrets, + v2PushSecrets, pullSecrets, - reformatPullSecrets, - decryptSecrets + reformatPullSecrets }; diff --git a/backend/src/index.ts b/backend/src/index.ts index 7e1d1411f..d182c2655 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -1,131 +1,25 @@ -/* eslint-disable no-console */ -import http from 'http'; -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 } from './config'; -import { apiLimiter } from './helpers/rateLimiter'; -import { createTerminus } from '@godaddy/terminus'; +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, - 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, - log as logRouter -} 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); - }); - return mongoose.connection; -}; - -const dbConnection = 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()); +if (NODE_ENV !== 'test') { + Sentry.init({ + dsn: SENTRY_DSN, + tracesSampleRate: 1.0, + debug: NODE_ENV === 'production' ? false : true, + environment: NODE_ENV + }); } - -app.use(express.json()); - -// 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); -app.use('/api/v1/log', logRouter); - -const server = http.createServer(app); - -const onSignal = () => { - console.log('Server is starting clean-up'); - return Promise.all([ - () => { - dbConnection.close(() => { - console.info('Database connection closed'); - }); - } - ]); -}; - -const healthCheck = () => { - // `state.isShuttingDown` (boolean) shows whether the server is shutting down or not - return Promise - .resolve - // optionally include a resolve value to be included as - // info in the health check response - (); -}; - -createTerminus(server, { - healthChecks: { - '/healthcheck': healthCheck, - onSignal - } -}); - -server.listen(PORT, () => { - console.log('Listening on PORT ' + PORT); -}); diff --git a/backend/src/integrations/apps.ts b/backend/src/integrations/apps.ts index 70680ef7d..e3b78c481 100644 --- a/backend/src/integrations/apps.ts +++ b/backend/src/integrations/apps.ts @@ -1,17 +1,22 @@ import axios from 'axios'; import * as Sentry from '@sentry/node'; +import { Octokit } from '@octokit/rest'; +import { IIntegrationAuth } from '../models'; import { - IIntegrationAuth -} from '../models'; -import { - INTEGRATION_HEROKU, - INTEGRATION_VERCEL, - INTEGRATION_NETLIFY, - INTEGRATION_HEROKU_API_URL, - INTEGRATION_VERCEL_API_URL, - INTEGRATION_NETLIFY_API_URL + 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 @@ -21,47 +26,51 @@ import { * @returns {String} apps.name - name of integration app */ const getApps = async ({ - integrationAuth, - accessToken + integrationAuth, + accessToken }: { - integrationAuth: IIntegrationAuth; - accessToken: string; + integrationAuth: IIntegrationAuth; + accessToken: string; }) => { - - interface App { - name: string; - siteId?: 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; - } - - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to get integration apps'); + 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; } - - return apps; -} + } 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 @@ -70,31 +79,29 @@ const getApps = async ({ * @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; -} +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 @@ -103,30 +110,28 @@ const getAppsHeroku = async ({ * @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; -} +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 @@ -136,34 +141,73 @@ const getAppsVercel = async ({ * @returns {String} apps.name - name of Netlify site */ const getAppsNetlify = async ({ - integrationAuth, - accessToken + integrationAuth, + accessToken }: { - integrationAuth: IIntegrationAuth; - accessToken: string; + 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; -} + let apps; + try { + const res = ( + await axios.get(`${INTEGRATION_NETLIFY_API_URL}/api/v1/sites`, { + headers: { + Authorization: `Bearer ${accessToken}` + } + }) + ).data; -export { - getApps -} \ No newline at end of file + 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 index 265036ee0..3e7076118 100644 --- a/backend/src/integrations/exchange.ts +++ b/backend/src/integrations/exchange.ts @@ -1,46 +1,56 @@ import axios from 'axios'; import * as Sentry from '@sentry/node'; import { - INTEGRATION_HEROKU, - INTEGRATION_VERCEL, - INTEGRATION_NETLIFY, - INTEGRATION_HEROKU_TOKEN_URL, - INTEGRATION_VERCEL_TOKEN_URL, - INTEGRATION_NETLIFY_TOKEN_URL, - ACTION_PUSH_TO_HEROKU + 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_SECRET_HEROKU, - CLIENT_ID_VERCEL, - CLIENT_ID_NETLIFY, - CLIENT_SECRET_VERCEL, - CLIENT_SECRET_NETLIFY +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'; interface ExchangeCodeHerokuResponse { - token_type: string; - access_token: string; - expires_in: number; - refresh_token: string; - user_id: string; - session_nonce?: string; + 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; + 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; + access_token: string; + token_type: string; + refresh_token: string; + scope: string; + created_at: number; +} + +interface ExchangeCodeGithubResponse { + access_token: string; + scope: string; + token_type: string; } /** @@ -56,40 +66,45 @@ interface ExchangeCodeNetlifyResponse { * @returns {String} obj.action - integration action for bot sequence */ const exchangeCode = async ({ - integration, - code -}: { - integration: string; - code: string; + 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; - } - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed OAuth2 code-token exchange'); + 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; } - - return obj; -} + } 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 @@ -144,35 +159,33 @@ const exchangeCodeHeroku = async ({ * @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 - }); -} +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 @@ -184,58 +197,89 @@ const exchangeCodeVercel = async ({ * @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 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 + const res2 = await axios.get('https://api.netlify.com/api/v1/sites', { + headers: { + Authorization: `Bearer ${res.access_token}` + } }); -} -export { - exchangeCode -} \ No newline at end of file + 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/refresh.ts b/backend/src/integrations/refresh.ts index 16870944d..ea232f1e5 100644 --- a/backend/src/integrations/refresh.ts +++ b/backend/src/integrations/refresh.ts @@ -13,46 +13,47 @@ import { * 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 + * @param {String} obj.refreshToken - refresh token to use to get new access token for Heroku */ const exchangeRefresh = async ({ - integration, - refreshToken + integration, + refreshToken }: { - integration: string; - refreshToken: string; + 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'); + let accessToken; + try { + switch (integration) { + case INTEGRATION_HEROKU: + accessToken = await exchangeRefreshHeroku({ + refreshToken + }); + break; } - - return accessToken; -} + } 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 + * @returns */ const exchangeRefreshHeroku = async ({ - refreshToken + refreshToken }: { - refreshToken: string; + 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, @@ -63,16 +64,14 @@ const exchangeRefreshHeroku = async ({ } 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; -} + 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'); + } -export { - exchangeRefresh -} \ No newline at end of file + return accessToken; +}; + +export { exchangeRefresh }; diff --git a/backend/src/integrations/revoke.ts b/backend/src/integrations/revoke.ts index 43a1582b7..483486343 100644 --- a/backend/src/integrations/revoke.ts +++ b/backend/src/integrations/revoke.ts @@ -1,50 +1,47 @@ import axios from 'axios'; import * as Sentry from '@sentry/node'; +import { IIntegrationAuth, IntegrationAuth, Integration } from '../models'; import { - IIntegrationAuth, - IntegrationAuth, - Integration -} from '../models'; -import { - INTEGRATION_HEROKU, - INTEGRATION_VERCEL, - INTEGRATION_NETLIFY + INTEGRATION_HEROKU, + INTEGRATION_VERCEL, + INTEGRATION_NETLIFY, + INTEGRATION_GITHUB } from '../variables'; const revokeAccess = async ({ - integrationAuth, - accessToken + integrationAuth, + accessToken }: { - integrationAuth: IIntegrationAuth, - accessToken: string + 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; - } - - 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'); + 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; } -} -export { - revokeAccess -} \ No newline at end of file + 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 index b26bdee74..30628fb9a 100644 --- a/backend/src/integrations/sync.ts +++ b/backend/src/integrations/sync.ts @@ -1,16 +1,21 @@ 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 { - IIntegration, IIntegrationAuth -} from '../models'; -import { - INTEGRATION_HEROKU, - INTEGRATION_VERCEL, - INTEGRATION_NETLIFY, - INTEGRATION_HEROKU_API_URL, - INTEGRATION_VERCEL_API_URL, - INTEGRATION_NETLIFY_API_URL + 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) @@ -26,49 +31,54 @@ import { * @param {String} obj.accessToken - access token for integration */ const syncSecrets = async ({ - integration, - integrationAuth, - secrets, - accessToken, + integration, + integrationAuth, + secrets, + accessToken }: { - integration: IIntegration; - integrationAuth: IIntegrationAuth; - secrets: any; - accessToken: string; + 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; - } - - // TODO: set integration to inactive if it was not synced correctly (send alert?) - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to sync secrets to integration'); + 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] @@ -77,47 +87,49 @@ const syncSecrets = async ({ * @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, + secrets, + accessToken }: { - integration: IIntegration, - secrets: any; - accessToken: string; + 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; - } - }); + 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; - 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'); - } -} + 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] @@ -474,8 +486,120 @@ const syncSecretsNetlify = async ({ throw new Error('Failed to sync secrets to Heroku'); } } - -export { - syncSecrets -} \ No newline at end of file +/** + * 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/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 index 6c5a3820a..e39f0d1b5 100644 --- a/backend/src/middleware/requireBotAuth.ts +++ b/backend/src/middleware/requireBotAuth.ts @@ -1,7 +1,7 @@ -import * as Sentry from '@sentry/node'; 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'; @@ -15,30 +15,22 @@ const requireBotAuth = ({ location?: req; }) => { return async (req: Request, res: Response, next: NextFunction) => { - try { - const bot = await Bot.findOne({ _id: req[location].botId }); - - if (!bot) { - throw new Error('Failed to find bot'); - } - - await validateMembership({ - userId: req.user._id.toString(), - workspaceId: bot.workspace.toString(), - acceptedRoles, - acceptedStatuses - }); - - req.bot = bot; - - next(); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - return res.status(401).send({ - error: 'Failed bot authorization' - }); + 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(); } } diff --git a/backend/src/middleware/requireIntegrationAuth.ts b/backend/src/middleware/requireIntegrationAuth.ts index fe653dbc0..4389028ab 100644 --- a/backend/src/middleware/requireIntegrationAuth.ts +++ b/backend/src/middleware/requireIntegrationAuth.ts @@ -1,8 +1,8 @@ -import * as Sentry from '@sentry/node'; import { Request, Response, NextFunction } from 'express'; -import { Bot, Integration, IntegrationAuth, Membership } from '../models'; +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 @@ -21,48 +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'); - } - - 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) { - throw new Error('Failed to find integration authorization'); - } - - req.integration = integration; - req.accessToken = await IntegrationService.getIntegrationAuthAccess({ - integrationAuthId: integrationAuth._id.toString() - }); - - 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 ed44ffec5..278716e60 100644 --- a/backend/src/middleware/requireIntegrationAuthorizationAuth.ts +++ b/backend/src/middleware/requireIntegrationAuthorizationAuth.ts @@ -3,6 +3,7 @@ import { Request, Response, NextFunction } from 'express'; 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 @@ -22,41 +23,33 @@ const requireIntegrationAuthorizationAuth = ({ attachAccessToken?: boolean; }) => { return async (req: Request, res: Response, next: NextFunction) => { - try { - const { integrationAuthId } = req.params; + 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) { - throw new Error('Failed to find integration authorization'); - } - - await validateMembership({ - userId: req.user._id.toString(), - workspaceId: integrationAuth.workspace.toString(), - acceptedRoles, - acceptedStatuses - }); + 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 + }); - req.integrationAuth = integrationAuth; - if (attachAccessToken) { - req.accessToken = await IntegrationService.getIntegrationAuthAccess({ - integrationAuthId: integrationAuth._id.toString() - }); - } - - 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 a27611f2d..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 { validateMembership } from '../helpers/membership'; +import { UnauthorizedRequestError } from '../utils/errors'; type req = 'params' | 'body' | 'query'; @@ -36,11 +36,7 @@ const requireWorkspaceAuth = ({ 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/integration.ts b/backend/src/models/integration.ts index edbe0234e..6da699216 100644 --- a/backend/src/models/integration.ts +++ b/backend/src/models/integration.ts @@ -1,77 +1,83 @@ import { Schema, model, Types } from 'mongoose'; import { - ENV_DEV, - ENV_TESTING, - ENV_STAGING, - ENV_PROD, - INTEGRATION_HEROKU, - INTEGRATION_VERCEL, - 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; - target: string; - context: string; - siteId: string; - integration: 'heroku' | 'vercel' | '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 - }, - 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 - ], - 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 0da3eb0d8..231416588 100644 --- a/backend/src/models/integrationAuth.ts +++ b/backend/src/models/integrationAuth.ts @@ -1,83 +1,87 @@ import { Schema, model, Types } from 'mongoose'; -import { - INTEGRATION_HEROKU, - INTEGRATION_VERCEL, - INTEGRATION_NETLIFY +import { + INTEGRATION_HEROKU, + INTEGRATION_VERCEL, + INTEGRATION_NETLIFY, + INTEGRATION_GITHUB } from '../variables'; export interface IIntegrationAuth { - _id: Types.ObjectId; - workspace: Types.ObjectId; - integration: 'heroku' | 'vercel' | 'netlify'; - teamId: string; - accountId: string; - 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_VERCEL, - INTEGRATION_NETLIFY - ], - 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 - } + { + 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/secret.ts b/backend/src/models/secret.ts index b83ef728d..d36e32b91 100644 --- a/backend/src/models/secret.ts +++ b/backend/src/models/secret.ts @@ -10,6 +10,7 @@ import { export interface ISecret { _id: Types.ObjectId; + version: number; workspace: Types.ObjectId; type: string; user: Types.ObjectId; @@ -22,10 +23,18 @@ export interface ISecret { secretValueIV: string; secretValueTag: string; secretValueHash: string; + secretCommentCiphertext?: string; + secretCommentIV?: string; + secretCommentTag?: string; + secretCommentHash?: string; } const secretSchema = new Schema( { + version: { + type: Number, + required: true + }, workspace: { type: Schema.Types.ObjectId, ref: 'Workspace', @@ -77,6 +86,22 @@ const secretSchema = new Schema( secretValueHash: { type: String, required: true + }, + secretCommentCiphertext: { + type: String, + required: false + }, + secretCommentIV: { + type: String, // symmetric + required: false + }, + secretCommentTag: { + type: String, // symmetric + required: false + }, + secretCommentHash: { + type: String, + required: false } }, { diff --git a/backend/src/routes/auth.ts b/backend/src/routes/v1/auth.ts similarity index 79% rename from backend/src/routes/auth.ts rename to backend/src/routes/v1/auth.ts index 6be9c09f7..ace15421e 100644 --- a/backend/src/routes/auth.ts +++ b/backend/src/routes/v1/auth.ts @@ -1,9 +1,9 @@ import express from 'express'; const router = express.Router(); import { body } from 'express-validator'; -import { requireAuth, validateRequest } from '../middleware'; -import { authController } from '../controllers'; -import { loginLimiter } from '../helpers/rateLimiter'; +import { requireAuth, validateRequest } from '../../middleware'; +import { authController } from '../../controllers/v1'; +import { loginLimiter } from '../../helpers/rateLimiter'; router.post('/token', validateRequest, authController.getNewToken); diff --git a/backend/src/routes/bot.ts b/backend/src/routes/v1/bot.ts similarity index 83% rename from backend/src/routes/bot.ts rename to backend/src/routes/v1/bot.ts index 3189bec44..90a0aa285 100644 --- a/backend/src/routes/bot.ts +++ b/backend/src/routes/v1/bot.ts @@ -6,9 +6,9 @@ import { requireBotAuth, requireWorkspaceAuth, validateRequest -} from '../middleware'; -import { botController } from '../controllers'; -import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../variables'; +} from '../../middleware'; +import { botController } from '../../controllers/v1'; +import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables'; router.get( '/:workspaceId', diff --git a/backend/src/routes/index.ts b/backend/src/routes/v1/index.ts similarity index 100% rename from backend/src/routes/index.ts rename to backend/src/routes/v1/index.ts diff --git a/backend/src/routes/integration.ts b/backend/src/routes/v1/integration.ts similarity index 85% rename from backend/src/routes/integration.ts rename to backend/src/routes/v1/integration.ts index e6738a803..f16aad23b 100644 --- a/backend/src/routes/integration.ts +++ b/backend/src/routes/v1/integration.ts @@ -4,10 +4,10 @@ import { requireAuth, requireIntegrationAuth, validateRequest -} from '../middleware'; -import { ADMIN, MEMBER, GRANTED } from '../variables'; +} from '../../middleware'; +import { ADMIN, MEMBER, GRANTED } from '../../variables'; import { body, param } from 'express-validator'; -import { integrationController } from '../controllers'; +import { integrationController } from '../../controllers/v1'; router.patch( '/:integrationId', diff --git a/backend/src/routes/integrationAuth.ts b/backend/src/routes/v1/integrationAuth.ts similarity index 89% rename from backend/src/routes/integrationAuth.ts rename to backend/src/routes/v1/integrationAuth.ts index ef80a2dcc..004706786 100644 --- a/backend/src/routes/integrationAuth.ts +++ b/backend/src/routes/v1/integrationAuth.ts @@ -6,9 +6,9 @@ import { requireWorkspaceAuth, requireIntegrationAuthorizationAuth, validateRequest -} from '../middleware'; -import { ADMIN, MEMBER, GRANTED } from '../variables'; -import { integrationAuthController } from '../controllers'; +} from '../../middleware'; +import { ADMIN, MEMBER, GRANTED } from '../../variables'; +import { integrationAuthController } from '../../controllers/v1'; router.get( '/integration-options', diff --git a/backend/src/routes/inviteOrg.ts b/backend/src/routes/v1/inviteOrg.ts similarity index 80% rename from backend/src/routes/inviteOrg.ts rename to backend/src/routes/v1/inviteOrg.ts index 16ab64f85..5c89b022f 100644 --- a/backend/src/routes/inviteOrg.ts +++ b/backend/src/routes/v1/inviteOrg.ts @@ -1,8 +1,8 @@ import express from 'express'; const router = express.Router(); import { body } from 'express-validator'; -import { requireAuth, validateRequest } from '../middleware'; -import { membershipOrgController } from '../controllers'; +import { requireAuth, validateRequest } from '../../middleware'; +import { membershipOrgController } from '../../controllers/v1'; router.post( '/signup', diff --git a/backend/src/routes/key.ts b/backend/src/routes/v1/key.ts similarity index 76% rename from backend/src/routes/key.ts rename to backend/src/routes/v1/key.ts index 9541ca123..094d18e72 100644 --- a/backend/src/routes/key.ts +++ b/backend/src/routes/v1/key.ts @@ -4,10 +4,10 @@ import { requireAuth, requireWorkspaceAuth, validateRequest -} from '../middleware'; +} from '../../middleware'; import { body, param } from 'express-validator'; -import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../variables'; -import { keyController } from '../controllers'; +import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables'; +import { keyController } from '../../controllers/v1'; router.post( '/:workspaceId', @@ -34,6 +34,4 @@ router.get( keyController.getLatestKey ); -router.get('/publicKey/infisical', keyController.getPublicKeyInfisical); - export default router; diff --git a/backend/src/routes/membership.ts b/backend/src/routes/v1/membership.ts similarity index 83% rename from backend/src/routes/membership.ts rename to backend/src/routes/v1/membership.ts index 04d94f576..4d81aa6dc 100644 --- a/backend/src/routes/membership.ts +++ b/backend/src/routes/v1/membership.ts @@ -1,8 +1,8 @@ import express from 'express'; const router = express.Router(); import { body, param } from 'express-validator'; -import { requireAuth, validateRequest } from '../middleware'; -import { membershipController } from '../controllers'; +import { requireAuth, validateRequest } from '../../middleware'; +import { membershipController } from '../../controllers/v1'; router.get( // used for CLI (deprecate) '/:workspaceId/connect', diff --git a/backend/src/routes/membershipOrg.ts b/backend/src/routes/v1/membershipOrg.ts similarity index 78% rename from backend/src/routes/membershipOrg.ts rename to backend/src/routes/v1/membershipOrg.ts index b6cd6313f..c74d20449 100644 --- a/backend/src/routes/membershipOrg.ts +++ b/backend/src/routes/v1/membershipOrg.ts @@ -1,8 +1,8 @@ import express from 'express'; const router = express.Router(); import { param } from 'express-validator'; -import { requireAuth, validateRequest } from '../middleware'; -import { membershipOrgController } from '../controllers'; +import { requireAuth, validateRequest } from '../../middleware'; +import { membershipOrgController } from '../../controllers/v1'; router.post( // TODO diff --git a/backend/src/routes/organization.ts b/backend/src/routes/v1/organization.ts similarity index 95% rename from backend/src/routes/organization.ts rename to backend/src/routes/v1/organization.ts index 276e26095..1fff9b597 100644 --- a/backend/src/routes/organization.ts +++ b/backend/src/routes/v1/organization.ts @@ -5,9 +5,9 @@ import { requireAuth, requireOrganizationAuth, validateRequest -} from '../middleware'; -import { OWNER, ADMIN, MEMBER, ACCEPTED } from '../variables'; -import { organizationController } from '../controllers'; +} from '../../middleware'; +import { OWNER, ADMIN, MEMBER, ACCEPTED } from '../../variables'; +import { organizationController } from '../../controllers/v1'; router.get( '/', diff --git a/backend/src/routes/password.ts b/backend/src/routes/v1/password.ts similarity index 94% rename from backend/src/routes/password.ts rename to backend/src/routes/v1/password.ts index 8032cba83..2cb811de5 100644 --- a/backend/src/routes/password.ts +++ b/backend/src/routes/v1/password.ts @@ -1,9 +1,9 @@ import express from 'express'; const router = express.Router(); import { body } from 'express-validator'; -import { requireAuth, requireSignupAuth, validateRequest } from '../middleware'; -import { passwordController } from '../controllers'; -import { passwordLimiter } from '../helpers/rateLimiter'; +import { requireAuth, requireSignupAuth, validateRequest } from '../../middleware'; +import { passwordController } from '../../controllers/v1'; +import { passwordLimiter } from '../../helpers/rateLimiter'; router.post( '/srp1', diff --git a/backend/src/routes/secret.ts b/backend/src/routes/v1/secret.ts similarity index 87% rename from backend/src/routes/secret.ts rename to backend/src/routes/v1/secret.ts index 98b3009de..e2dcb3368 100644 --- a/backend/src/routes/secret.ts +++ b/backend/src/routes/v1/secret.ts @@ -5,10 +5,10 @@ import { requireWorkspaceAuth, requireServiceTokenAuth, validateRequest -} from '../middleware'; +} from '../../middleware'; import { body, query, param } from 'express-validator'; -import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../variables'; -import { secretController } from '../controllers'; +import { secretController } from '../../controllers/v1'; +import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables'; router.post( '/:workspaceId', diff --git a/backend/src/routes/serviceToken.ts b/backend/src/routes/v1/serviceToken.ts similarity index 85% rename from backend/src/routes/serviceToken.ts rename to backend/src/routes/v1/serviceToken.ts index 00195edee..57031f3e0 100644 --- a/backend/src/routes/serviceToken.ts +++ b/backend/src/routes/v1/serviceToken.ts @@ -5,10 +5,10 @@ import { requireWorkspaceAuth, requireServiceTokenAuth, validateRequest -} from '../middleware'; +} from '../../middleware'; import { body } from 'express-validator'; -import { ADMIN, MEMBER, GRANTED } from '../variables'; -import { serviceTokenController } from '../controllers'; +import { ADMIN, MEMBER, GRANTED } from '../../variables'; +import { serviceTokenController } from '../../controllers/v1'; // TODO: revoke service token diff --git a/backend/src/routes/signup.ts b/backend/src/routes/v1/signup.ts similarity index 89% rename from backend/src/routes/signup.ts rename to backend/src/routes/v1/signup.ts index 40b5929b0..3ea3b2738 100644 --- a/backend/src/routes/signup.ts +++ b/backend/src/routes/v1/signup.ts @@ -1,9 +1,9 @@ import express from 'express'; const router = express.Router(); import { body } from 'express-validator'; -import { requireSignupAuth, validateRequest } from '../middleware'; -import { signupController } from '../controllers'; -import { signupLimiter } from '../helpers/rateLimiter'; +import { requireSignupAuth, validateRequest } from '../../middleware'; +import { signupController } from '../../controllers/v1'; +import { signupLimiter } from '../../helpers/rateLimiter'; router.post( '/email/signup', diff --git a/backend/src/routes/stripe.ts b/backend/src/routes/v1/stripe.ts similarity index 71% rename from backend/src/routes/stripe.ts rename to backend/src/routes/v1/stripe.ts index ba5706562..cfcca77cb 100644 --- a/backend/src/routes/stripe.ts +++ b/backend/src/routes/v1/stripe.ts @@ -1,6 +1,6 @@ import express from 'express'; const router = express.Router(); -import { stripeController } from '../controllers'; +import { stripeController } from '../../controllers/v1'; router.post('/webhook', stripeController.handleWebhook); diff --git a/backend/src/routes/user.ts b/backend/src/routes/v1/user.ts similarity index 58% rename from backend/src/routes/user.ts rename to backend/src/routes/v1/user.ts index 4a25c48fb..393978922 100644 --- a/backend/src/routes/user.ts +++ b/backend/src/routes/v1/user.ts @@ -1,7 +1,7 @@ import express from 'express'; const router = express.Router(); -import { requireAuth } from '../middleware'; -import { userController } from '../controllers'; +import { requireAuth } from '../../middleware'; +import { userController } from '../../controllers/v1'; router.get('/', requireAuth, userController.getUser); diff --git a/backend/src/routes/userAction.ts b/backend/src/routes/v1/userAction.ts similarity index 73% rename from backend/src/routes/userAction.ts rename to backend/src/routes/v1/userAction.ts index 5ea454156..544b948e8 100644 --- a/backend/src/routes/userAction.ts +++ b/backend/src/routes/v1/userAction.ts @@ -1,8 +1,8 @@ import express from 'express'; const router = express.Router(); -import { requireAuth, validateRequest } from '../middleware'; +import { requireAuth, validateRequest } from '../../middleware'; import { body, query } from 'express-validator'; -import { userActionController } from '../controllers'; +import { userActionController } from '../../controllers/v1'; router.post( '/', diff --git a/backend/src/routes/workspace.ts b/backend/src/routes/v1/workspace.ts similarity index 92% rename from backend/src/routes/workspace.ts rename to backend/src/routes/v1/workspace.ts index 1d20c102a..6a1ae9034 100644 --- a/backend/src/routes/workspace.ts +++ b/backend/src/routes/v1/workspace.ts @@ -1,13 +1,13 @@ import express from 'express'; const router = express.Router(); -import { body, param } from 'express-validator'; +import { body, param, query } from 'express-validator'; import { requireAuth, requireWorkspaceAuth, validateRequest -} from '../middleware'; -import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../variables'; -import { workspaceController, membershipController } from '../controllers'; +} from '../../middleware'; +import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables'; +import { workspaceController, membershipController } from '../../controllers/v1'; router.get( '/:workspaceId/keys', diff --git a/backend/src/routes/v2/index.ts b/backend/src/routes/v2/index.ts new file mode 100644 index 000000000..6e6758753 --- /dev/null +++ b/backend/src/routes/v2/index.ts @@ -0,0 +1,7 @@ +import secret from './secret'; +import workspace from './workspace'; + +export { + secret, + workspace +} diff --git a/backend/src/routes/v2/secret.ts b/backend/src/routes/v2/secret.ts new file mode 100644 index 000000000..17a91d39c --- /dev/null +++ b/backend/src/routes/v2/secret.ts @@ -0,0 +1,4 @@ +import express from 'express'; +const router = express.Router(); + +export default router; diff --git a/backend/src/routes/v2/workspace.ts b/backend/src/routes/v2/workspace.ts new file mode 100644 index 000000000..98eacd9ea --- /dev/null +++ b/backend/src/routes/v2/workspace.ts @@ -0,0 +1,176 @@ +import express from 'express'; +const router = express.Router(); +import { body, param, query } from 'express-validator'; +import { + requireAuth, + requireWorkspaceAuth, + requireServiceTokenAuth, + validateRequest +} from '../../middleware'; +import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables'; +import { membershipController } from '../../controllers/v1'; +import { workspaceController } from '../../controllers/v2'; + +router.get( + '/:workspaceId/keys', + requireAuth, + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [COMPLETED, GRANTED] + }), + param('workspaceId').exists().trim(), + validateRequest, + workspaceController.getWorkspacePublicKeys +); + +router.get( + '/:workspaceId/users', + requireAuth, + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [COMPLETED, GRANTED] + }), + param('workspaceId').exists().trim(), + validateRequest, + workspaceController.getWorkspaceMemberships +); + +router.get('/', requireAuth, workspaceController.getWorkspaces); + +router.get( + '/:workspaceId', + requireAuth, + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [COMPLETED, GRANTED] + }), + param('workspaceId').exists().trim(), + validateRequest, + workspaceController.getWorkspace +); + +router.post( + '/', + requireAuth, + body('workspaceName').exists().trim().notEmpty(), + body('organizationId').exists().trim().notEmpty(), + validateRequest, + workspaceController.createWorkspace +); + +router.delete( + '/:workspaceId', + requireAuth, + requireWorkspaceAuth({ + acceptedRoles: [ADMIN], + acceptedStatuses: [GRANTED] + }), + param('workspaceId').exists().trim(), + validateRequest, + workspaceController.deleteWorkspace +); + +router.post( + '/:workspaceId/name', + requireAuth, + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [COMPLETED, GRANTED] + }), + param('workspaceId').exists().trim(), + body('name').exists().trim().notEmpty(), + validateRequest, + workspaceController.changeWorkspaceName +); + +router.post( + '/:workspaceId/invite-signup', + requireAuth, + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [GRANTED] + }), + param('workspaceId').exists().trim(), + body('email').exists().trim().notEmpty(), + validateRequest, + membershipController.inviteUserToWorkspace +); + +router.get( + '/:workspaceId/integrations', + requireAuth, + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [GRANTED] + }), + param('workspaceId').exists().trim(), + validateRequest, + workspaceController.getWorkspaceIntegrations +); + +router.get( + '/:workspaceId/authorizations', + requireAuth, + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [GRANTED] + }), + param('workspaceId').exists().trim(), + validateRequest, + workspaceController.getWorkspaceIntegrationAuthorizations +); + +router.get( // TODO: modify + '/:workspaceId/service-tokens', + requireAuth, + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [GRANTED] + }), + param('workspaceId').exists().trim(), + validateRequest, + workspaceController.getWorkspaceServiceTokens +); + +router.post( + '/:workspaceId/secrets', + requireAuth, + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [COMPLETED, GRANTED] + }), + body('secrets').exists(), + body('keys').exists(), + body('environment').exists().trim().notEmpty(), + body('channel'), + param('workspaceId').exists().trim(), + validateRequest, + workspaceController.pushWorkspaceSecrets +); + +router.get( + '/:workspaceId/secrets', + requireAuth, + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [COMPLETED, GRANTED] + }), + query('environment').exists().trim(), + query('channel'), + param('workspaceId').exists().trim(), + validateRequest, + workspaceController.pullSecrets +); + +router.get( // TODO: modify based on upcoming serviceTokenData changes + '/:workspaceId/secrets-service-token', + requireServiceTokenAuth, + query('environment').exists().trim(), + query('channel'), + param('workspaceId').exists().trim(), + validateRequest, + workspaceController.pullSecretsServiceToken +); + + +export default router; diff --git a/backend/src/services/PostHogClient.ts b/backend/src/services/PostHogClient.ts index 4096ac8ec..4ce0117f0 100644 --- a/backend/src/services/PostHogClient.ts +++ b/backend/src/services/PostHogClient.ts @@ -5,8 +5,16 @@ import { POSTHOG_PROJECT_API_KEY, TELEMETRY_ENABLED } from '../config'; +import { getLogger } from '../utils/logger'; -console.log('TELEMETRY_ENABLED: ', TELEMETRY_ENABLED); +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')) +} let postHogClient: any; if (NODE_ENV === 'production' && TELEMETRY_ENABLED) { 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/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/types/express/index.d.ts b/backend/src/types/express/index.d.ts index 9a9e81449..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 { 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..24fe007f9 --- /dev/null +++ b/backend/src/utils/patchAsyncRoutes.js @@ -0,0 +1,69 @@ +/* +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); +} + +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; + }, +}); + +module.exports = { + patchRouterParam +}; 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/action.ts b/backend/src/variables/action.ts deleted file mode 100644 index 1f913bbe9..000000000 --- a/backend/src/variables/action.ts +++ /dev/null @@ -1,5 +0,0 @@ -const ACTION_PUSH_TO_HEROKU = 'pushToHeroku'; - -export { - ACTION_PUSH_TO_HEROKU -} \ No newline at end of file diff --git a/backend/src/variables/index.ts b/backend/src/variables/index.ts index b21324423..e284d6d5c 100644 --- a/backend/src/variables/index.ts +++ b/backend/src/variables/index.ts @@ -1,79 +1,75 @@ import { - ENV_DEV, - ENV_TESTING, - ENV_STAGING, - ENV_PROD, - ENV_SET + ENV_DEV, + ENV_TESTING, + ENV_STAGING, + ENV_PROD, + ENV_SET } from './environment'; import { - INTEGRATION_HEROKU, - INTEGRATION_VERCEL, - INTEGRATION_NETLIFY, - INTEGRATION_SET, - INTEGRATION_OAUTH2, - INTEGRATION_HEROKU_TOKEN_URL, - INTEGRATION_VERCEL_TOKEN_URL, - INTEGRATION_NETLIFY_TOKEN_URL, - INTEGRATION_HEROKU_API_URL, - INTEGRATION_VERCEL_API_URL, - INTEGRATION_NETLIFY_API_URL, - INTEGRATION_OPTIONS + 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 + OWNER, + ADMIN, + MEMBER, + INVITED, + ACCEPTED, + COMPLETED, + GRANTED } from './organization'; -import { - SECRET_SHARED, - SECRET_PERSONAL -} from './secret'; -import { - PLAN_STARTER, - PLAN_PRO -} from './stripe'; -import { - EVENT_PUSH_SECRETS, - EVENT_PULL_SECRETS -} from './event'; -import { - ACTION_PUSH_TO_HEROKU -} from './action'; +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, - PLAN_STARTER, - PLAN_PRO, - SECRET_SHARED, - SECRET_PERSONAL, - ENV_DEV, - ENV_TESTING, - ENV_STAGING, - ENV_PROD, - ENV_SET, - INTEGRATION_HEROKU, - INTEGRATION_VERCEL, - INTEGRATION_NETLIFY, - INTEGRATION_SET, - INTEGRATION_OAUTH2, - INTEGRATION_HEROKU_TOKEN_URL, - INTEGRATION_VERCEL_TOKEN_URL, - INTEGRATION_NETLIFY_TOKEN_URL, - INTEGRATION_HEROKU_API_URL, - INTEGRATION_VERCEL_API_URL, - INTEGRATION_NETLIFY_API_URL, - EVENT_PUSH_SECRETS, - EVENT_PULL_SECRETS, - ACTION_PUSH_TO_HEROKU, - INTEGRATION_OPTIONS -}; \ No newline at end of file + 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 index 55ae271a1..00e817c57 100644 --- a/backend/src/variables/integration.ts +++ b/backend/src/variables/integration.ts @@ -1,6 +1,7 @@ import { CLIENT_ID_HEROKU, CLIENT_ID_NETLIFY, + CLIENT_ID_GITHUB, CLIENT_SLUG_VERCEL } from '../config'; @@ -8,10 +9,12 @@ import { 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_HEROKU, + INTEGRATION_VERCEL, + INTEGRATION_NETLIFY, + INTEGRATION_GITHUB ]); // integration types @@ -19,13 +22,17 @@ 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_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 = [ { @@ -56,6 +63,16 @@ const INTEGRATION_OPTIONS = [ 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', @@ -104,16 +121,19 @@ const INTEGRATION_OPTIONS = [ ] export { - INTEGRATION_HEROKU, - INTEGRATION_VERCEL, - INTEGRATION_NETLIFY, - INTEGRATION_SET, - INTEGRATION_OAUTH2, - INTEGRATION_HEROKU_TOKEN_URL, - INTEGRATION_VERCEL_TOKEN_URL, - INTEGRATION_NETLIFY_TOKEN_URL, - INTEGRATION_HEROKU_API_URL, - INTEGRATION_VERCEL_API_URL, - INTEGRATION_NETLIFY_API_URL, - INTEGRATION_OPTIONS -} \ No newline at end of file + 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/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/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/login.go b/cli/packages/cmd/login.go index 29e2c4935..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 } diff --git a/cli/packages/cmd/root.go b/cli/packages/cmd/root.go index 7317b3eea..173df36df 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.11", + Version: "0.1.16", } // 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 7518fe98d..6e0133c8e 100644 --- a/cli/packages/cmd/run.go +++ b/cli/packages/cmd/run.go @@ -60,6 +60,13 @@ var runCmd = &cobra.Command{ return } + secretOverriding, err := cmd.Flags().GetBool("secret-overriding") + if err != nil { + log.Errorln("Unable to parse the secret-overriding flag") + log.Debugln(err) + return + } + shouldExpandSecrets, err := cmd.Flags().GetBool("expand") if err != nil { log.Errorln("Unable to parse the substitute flag") @@ -84,6 +91,10 @@ var runCmd = &cobra.Command{ secrets = util.SubstituteSecrets(secrets) } + if secretOverriding { + secrets = util.OverrideWithPersonalSecrets(secrets) + } + if cmd.Flags().Changed("command") { command := cmd.Flag("command").Value.String() err = executeMultipleCommandWithEnvs(command, secrets) @@ -108,6 +119,7 @@ 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().Bool("secret-overriding", true, "Prioritizes personal secrets with the same name over shared secrets") runCmd.Flags().StringP("command", "c", "", "chained commands to execute (e.g. \"npm install && npm run dev; echo ...\")") } 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..8ba1c4627 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,12 +10,14 @@ 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 { Key string `json:"key"` Value string `json:"value"` + Type string `json:"type"` } type WorkspaceConfigFile struct { 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 0ee04b013..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) @@ -152,3 +159,62 @@ func GetAllWorkSpaceConfigsStartingFromCurrentPath() (workspaces []models.Worksp 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/secrets.go b/cli/packages/util/secrets.go index c127111b1..f88aed96b 100644 --- a/cli/packages/util/secrets.go +++ b/cli/packages/util/secrets.go @@ -14,6 +14,9 @@ import ( "golang.org/x/crypto/nacl/box" ) +const PERSONAL_SECRET_TYPE_NAME = "personal" +const SHARED_SECRET_TYPE_NAME = "shared" + 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. @@ -78,6 +81,7 @@ func getSecretsByWorkspaceIdAndEnvName(httpClient resty.Client, envName string, env := models.SingleEnvironmentVariable{ Key: string(plainTextKey), Value: string(plainTextValue), + Type: string(secret.Type), } listOfEnv = append(listOfEnv, env) @@ -187,6 +191,7 @@ func GetSecretsFromAPIUsingInfisicalToken(infisicalToken string, envName string, env := models.SingleEnvironmentVariable{ Key: string(plainTextKey), Value: string(plainTextValue), + Type: string(secret.Type), } listOfEnv = append(listOfEnv, env) @@ -335,9 +340,48 @@ func SubstituteSecrets(secrets []models.SingleEnvironmentVariable) []models.Sing expandedSecrets = append(expandedSecrets, models.SingleEnvironmentVariable{ Key: secret.Key, Value: expandedVariable, + Type: secret.Type, }) } return expandedSecrets } + +// if two secrets with the same name are found, the one that has type `personal` will be in the returned list +func OverrideWithPersonalSecrets(secrets []models.SingleEnvironmentVariable) []models.SingleEnvironmentVariable { + personalSecret := make(map[string]models.SingleEnvironmentVariable) + sharedSecret := make(map[string]models.SingleEnvironmentVariable) + secretsToReturn := []models.SingleEnvironmentVariable{} + + for _, secret := range secrets { + if secret.Type == PERSONAL_SECRET_TYPE_NAME { + personalSecret[secret.Key] = models.SingleEnvironmentVariable{ + Key: secret.Key, + Value: secret.Value, + Type: secret.Type, + } + } + + if secret.Type == SHARED_SECRET_TYPE_NAME { + sharedSecret[secret.Key] = models.SingleEnvironmentVariable{ + Key: secret.Key, + Value: secret.Value, + Type: secret.Type, + } + } + } + + for _, secret := range secrets { + personalValue, personalExists := personalSecret[secret.Key] + sharedValue, sharedExists := sharedSecret[secret.Key] + + if personalExists && sharedExists || personalExists && !sharedExists { + secretsToReturn = append(secretsToReturn, personalValue) + } else { + secretsToReturn = append(secretsToReturn, sharedValue) + } + } + + return secretsToReturn +} 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/docs/cli/commands/commands.mdx b/docs/cli/commands/commands.mdx index acefdfa7e..7c1deeb1b 100644 --- a/docs/cli/commands/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/login.mdx b/docs/cli/commands/login.mdx index 32c1bedba..de004c97f 100644 --- a/docs/cli/commands/login.mdx +++ b/docs/cli/commands/login.mdx @@ -7,7 +7,5 @@ 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. +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/commands/run.mdx b/docs/cli/commands/run.mdx index 2c65ef53e..4afd7586d 100644 --- a/docs/cli/commands/run.mdx +++ b/docs/cli/commands/run.mdx @@ -34,3 +34,4 @@ Inject environment variables from the platform into an application process. | `--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 | +| `--secret-overriding`| Prioritizes personal secrets with the same name over shared secrets | `true` | 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 144259023..0c998feb4 100644 --- a/docs/cli/overview.mdx +++ b/docs/cli/overview.mdx @@ -88,6 +88,14 @@ The Infisical CLI provides a way to inject environment variables from the platfo sudo apt-get update && sudo apt-get install -y infisical ``` + + + Use the `yay` package manager to install from the [Arch User Repository](https://aur.archlinux.org/packages/infisical-bin) + + ```bash + yay -S infisical-bin + ``` + diff --git a/docs/cli/usage.mdx b/docs/cli/usage.mdx index 4237f8154..fe08b60b3 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 @@ -21,13 +38,7 @@ infisical init infisical run -- [your application start command] ``` -Options you can specify: - -| 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` | -| `--expand` | Parse shell parameter expansions in your secrets (e.g., `${DOMAIN}`) | `true` | +View all available options for `run` command [here](./commands/run) ## Examples: diff --git a/docs/contributing/FAQ.mdx b/docs/contributing/FAQ.mdx deleted file mode 100644 index 9f4f7aee9..000000000 --- a/docs/contributing/FAQ.mdx +++ /dev/null @@ -1,18 +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 - -If you opt for actual SMTP server (not the local MailHog), you have to have the right environment variables set. - -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`. diff --git a/docs/contributing/developing.mdx b/docs/contributing/developing.mdx index 25a1f72ee..423b83da2 100644 --- a/docs/contributing/developing.mdx +++ b/docs/contributing/developing.mdx @@ -16,59 +16,44 @@ cd infisical ## Set up environment variables -Before running the docker-compose we have to generate the .env file with the environment variables, you can create your own file or start with the -`.env.example` as an example guide. +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. -Mandatory variables in the `.env` file: + + 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. + -1. Keys and JWT variables +Refer to the [environment variable list](https://infisical.com/docs/self-hosting/configuration/envars) for guidance on each envar. -![image](https://user-images.githubusercontent.com/8071263/208800914-f468c2ad-c6a8-4da7-8ffd-eece5d7f08d2.png) +### Helpful tips for developing with Infisical: -The `.env.example` has these variables empty, you can self generate the `JWT and ENCRYPTION_KEY` with this [32-byte random hex strings generator](https://www.browserling.com/tools/random-hex). + +Use the `ENCRYPTION_KEY`, JWT-secret envars, `MONGO_URL`, `MONGO_USERNAME`, `MONGO_PASSWORD` provided in the `.env.example` file. -For the `PRIVATE_KEY and PUBLIC_KEY` you can use the ones shown in the screenshot: +If setting your own values: -``` -PRIVATE_KEY='oGVv5rThrpZ7WLgQW27chY1cXngr4wLQIZnGfSKgHPk=' -PUBLIC_KEY='ldr6JaC7AY+tun3omGLdE4SWpkJbtVBOI54KfUP53Xc=' -``` +- `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`. + -2. Mongo variables and site URL + +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). -![image](https://user-images.githubusercontent.com/118568289/206792171-3376e3c6-c3ac-4d5d-8776-d78ee089b520.png) - -These variables are used to connect the MongoDB and set the URL for the localhost. - -For development, you can use `root` for the `MONGO_USERNAME` and `example` for the `MONGO_PASSWORD` as shown in the screenshot. - -Take into account that if you use your own `MONGO_USERNAME` and `MONGO_PASSWORD`, you also have to change the `MONGO_URL` with the form of `MONGO_USERNAME:MONGO_PASSWORD` after the `//` part of the URL. - -3. Mail SMTP service variables - -![image](https://user-images.githubusercontent.com/118568289/206792653-ba3211d1-1071-43f2-93a7-8b408bbd9e0e.png) - -If you want to receive actual emails (e.g. you want to test how the email message will look like), take note of the following. - -For the `SMTP_USERNAME` variable, you will need an email with 2-steps-verification. - -For the `SMTP_PASSWORD` variable, you will need to [generate an app password](https://support.google.com/mail/answer/185833?hl=en) with the email you used in the `SMTP_USERNAME` variable. - -Otherwise, a local SMTP server (MailHog) is available for testing purposes. Set the following values to use this: +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. + ``` SMTP_HOST=smtp-server SMTP_PORT=1025 -SMTP_NAME= +SMTP_FROM_ADDRESS=team@infisical.com +SMTP_FROM_NAME=Infisical SMTP_USERNAME=team@infisical.com SMTP_PASSWORD= ``` -Make sure to leave the `SMTP_PASSWORD` blank so the backend will be able to connect to MailHog - -You can browse `http://localhost:8025/` to browse email messages sent by the backend. - -With these environment variables, you will be ready to run the docker-compose. + + If using Mailhog, make sure to leave the `SMTP_PASSWORD` blank so the backend can connect to MailHog. + ## Docker for development @@ -84,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/token.mdx b/docs/getting-started/dashboard/token.mdx index 9b3ddf79f..455a57929 100644 --- a/docs/getting-started/dashboard/token.mdx +++ b/docs/getting-started/dashboard/token.mdx @@ -11,6 +11,12 @@ To generate the the token, head over to your project settings as shown below. ![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. Once the token is expired, the CLI using it will no longer be able to make 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.png b/docs/images/integrations.png index 88b0c7aec..7359aa198 100644 Binary files a/docs/images/integrations.png and b/docs/images/integrations.png differ 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/overview.mdx b/docs/integrations/overview.mdx index fb8a5d631..0ccb9da26 100644 --- a/docs/integrations/overview.mdx +++ b/docs/integrations/overview.mdx @@ -13,7 +13,8 @@ Missing an integration? Throw in a [request](https://github.com/Infisical/infisi | [Kubernetes](/integrations/platforms/kubernetes) | Platform | Available | | [Heroku](/integrations/cloud/heroku) | Cloud | Available | | [Vercel](/integrations/cloud/vercel) | Cloud | Available | -| [Netlify](/integrations/cloud/netlify) | 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 | diff --git a/docs/mint.json b/docs/mint.json index ff00ca83a..cbac56d5a 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -94,26 +94,31 @@ "cli/commands/login", "cli/commands/init", "cli/commands/run", - "cli/commands/export" + "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" ] }, { @@ -140,7 +145,10 @@ }, { "group": "CI/CD", - "pages": ["integrations/cicd/circleci"] + "pages": [ + "integrations/cicd/githubactions", + "integrations/cicd/circleci" + ] }, { "group": "Frameworks", @@ -174,8 +182,7 @@ "pages": [ "contributing/overview", "contributing/code-of-conduct", - "contributing/developing", - "contributing/FAQ" + "contributing/developing" ] } ], 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 9c6697df5..f598336b6 100644 --- a/docs/self-hosting/configuration/envars.mdx +++ b/docs/self-hosting/configuration/envars.mdx @@ -3,18 +3,15 @@ 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` | +| `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` | @@ -23,14 +20,22 @@ Configuring Infisical requires setting some environment variables. There is a fi | `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` | -| `SMTP_HOST` | Hostname to connect to for establishing SMTP connections | `smtp.gmail.com` | -| `SMTP_NAME` | Name label to be used in From field (e.g. `Team`) | `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` | -| `CLIENT_ID_VERCEL` | OAuth client id for Vercel integration | `None` | -| `CLIENT_ID_NETLIFY` | OAuth client id for Netlify integration | `None` | -| `CLIENT_SECRET_HEROKU` | OAuth client secret for Heroku integration | `None` | -| `CLIENT_SECRET_VERCEL` | OAuth client secret for Vercel integration | `None` | -| `CLIENT_SECRET_NETLIFY` | OAuth client secret for Netlify integration | `None` | +| `LICENSE_KEY` | License key if using Infisical Enterprise Edition | `true` | +| `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/components/basic/InputField.tsx b/frontend/components/basic/InputField.tsx index 46b42c15c..241410959 100644 --- a/frontend/components/basic/InputField.tsx +++ b/frontend/components/basic/InputField.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import { memo,useState } from 'react'; import { faCircle, faEye, faEyeSlash } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; @@ -96,6 +96,7 @@ const InputField = ( /> {props.label?.includes('Password') && ( -
+
{textLine1}
-
{textLine2}
+
{textLine2}
{/*eslint-disable-next-line react/jsx-no-target-blank */} void; position: number;}) => { + return
+

Comments & notes

+