diff --git a/backend/package-lock.json b/backend/package-lock.json index 1b8e1af72..c743979e6 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -15,11 +15,12 @@ "@godaddy/terminus": "^4.12.0", "@node-saml/passport-saml": "^4.0.4", "@octokit/rest": "^19.0.5", - "@sentry/node": "^7.49.0", + "@sentry/node": "^7.77.0", "@sentry/tracing": "^7.48.0", "@types/crypto-js": "^4.1.1", "@types/libsodium-wrappers": "^0.7.10", "@ucast/mongo2js": "^1.3.4", + "ajv": "^8.12.0", "argon2": "^0.30.3", "aws-sdk": "^2.1364.0", "axios": "^1.3.5", @@ -38,12 +39,14 @@ "helmet": "^5.1.1", "infisical-node": "^1.2.1", "ioredis": "^5.3.2", + "jmespath": "^0.16.0", "js-yaml": "^4.1.0", "jsonwebtoken": "^9.0.0", "jsrp": "^0.2.4", "libsodium-wrappers": "^0.7.10", "lodash": "^4.17.21", "mongoose": "^7.4.1", + "mysql2": "^3.6.2", "nanoid": "^3.3.6", "node-cache": "^5.1.2", "nodemailer": "^6.8.0", @@ -51,6 +54,9 @@ "passport-github": "^1.1.0", "passport-gitlab2": "^5.0.0", "passport-google-oauth20": "^2.0.0", + "pg": "^8.11.3", + "pino": "^8.16.1", + "pino-http": "^8.5.1", "posthog-node": "^2.6.0", "probot": "^12.3.1", "query-string": "^7.1.3", @@ -61,8 +67,6 @@ "tweetnacl-util": "^0.15.1", "typescript": "^4.9.3", "utility-types": "^3.10.0", - "winston": "^3.8.2", - "winston-loki": "^6.0.6", "zod": "^3.22.3" }, "devDependencies": { @@ -75,12 +79,15 @@ "@types/cors": "^2.8.12", "@types/express": "^4.17.14", "@types/jest": "^29.5.0", + "@types/jmespath": "^0.15.1", "@types/jsonwebtoken": "^8.5.9", "@types/lodash": "^4.14.191", "@types/node": "^18.11.3", "@types/nodemailer": "^6.4.6", "@types/passport": "^1.0.12", + "@types/pg": "^8.10.7", "@types/picomatch": "^2.3.0", + "@types/pino": "^7.0.5", "@types/supertest": "^2.0.12", "@types/swagger-jsdoc": "^6.0.1", "@types/swagger-ui-express": "^4.1.3", @@ -94,6 +101,7 @@ "jest-junit": "^15.0.0", "nodemon": "^2.0.19", "npm": "^8.19.3", + "pino-pretty": "^10.2.3", "smee-client": "^1.2.3", "supertest": "^6.3.3", "swagger-autogen": "^2.23.5", @@ -3436,14 +3444,6 @@ "mongoose": "^6.0.13 || ^7.0.0" } }, - "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", @@ -3466,16 +3466,6 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, - "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-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -3523,6 +3513,28 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, "node_modules/@eslint/js": { "version": "8.44.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.44.0.tgz", @@ -4904,65 +4916,54 @@ "node": ">=6" } }, + "node_modules/@probot/pino/node_modules/colorette": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", + "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==" + }, + "node_modules/@probot/pino/node_modules/jmespath": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", + "integrity": "sha512-+kHj8HXArPfpPEKGLZ+kB5ONRTCiGQXo8RQYL0hH8t6pWXUBBK5KkkQmTNOwKK4LEsd0yTsgtjJVm4UBSZea4w==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/@probot/pino/node_modules/pino-pretty": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-6.0.0.tgz", + "integrity": "sha512-jyeR2fXXWc68st1DTTM5NhkHlx8p+1fKZMfm84Jwq+jSw08IwAjNaZBZR6ts69hhPOfOjg/NiE1HYW7vBRPL3A==", + "dependencies": { + "@hapi/bourne": "^2.0.0", + "args": "^5.0.1", + "colorette": "^1.3.0", + "dateformat": "^4.5.1", + "fast-safe-stringify": "^2.0.7", + "jmespath": "^0.15.0", + "joycon": "^3.0.0", + "pump": "^3.0.0", + "readable-stream": "^3.6.0", + "rfdc": "^1.3.0", + "split2": "^3.1.1", + "strip-json-comments": "^3.1.1" + }, + "bin": { + "pino-pretty": "bin.js" + } + }, + "node_modules/@probot/pino/node_modules/pino-pretty/node_modules/split2": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", + "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", + "dependencies": { + "readable-stream": "^3.0.0" + } + }, "node_modules/@probot/pino/node_modules/tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, - "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-internal/tracing": { "version": "7.59.3", "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.59.3.tgz", @@ -5055,18 +5056,59 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, "node_modules/@sentry/node": { - "version": "7.59.3", - "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.59.3.tgz", - "integrity": "sha512-5dG90YzmKjuy5TK04qDuc9LBxnGfqsZw8Oh9giwOBfQVCaFp7R14AXyQ1F8k3iUF+4sGeTvoqi9I/GKAItVmlA==", + "version": "7.77.0", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.77.0.tgz", + "integrity": "sha512-Ob5tgaJOj0OYMwnocc6G/CDLWC7hXfVvKX/ofkF98+BbN/tQa5poL+OwgFn9BA8ud8xKzyGPxGU6LdZ8Oh3z/g==", "dependencies": { - "@sentry-internal/tracing": "7.59.3", - "@sentry/core": "7.59.3", - "@sentry/types": "7.59.3", - "@sentry/utils": "7.59.3", - "cookie": "^0.4.1", - "https-proxy-agent": "^5.0.0", - "lru_map": "^0.3.3", - "tslib": "^2.4.1 || ^1.9.3" + "@sentry-internal/tracing": "7.77.0", + "@sentry/core": "7.77.0", + "@sentry/types": "7.77.0", + "@sentry/utils": "7.77.0", + "https-proxy-agent": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/node/node_modules/@sentry-internal/tracing": { + "version": "7.77.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.77.0.tgz", + "integrity": "sha512-8HRF1rdqWwtINqGEdx8Iqs9UOP/n8E0vXUu3Nmbqj4p5sQPA7vvCfq+4Y4rTqZFc7sNdFpDsRION5iQEh8zfZw==", + "dependencies": { + "@sentry/core": "7.77.0", + "@sentry/types": "7.77.0", + "@sentry/utils": "7.77.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/node/node_modules/@sentry/core": { + "version": "7.77.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.77.0.tgz", + "integrity": "sha512-Tj8oTYFZ/ZD+xW8IGIsU6gcFXD/gfE+FUxUaeSosd9KHwBQNOLhZSsYo/tTVf/rnQI/dQnsd4onPZLiL+27aTg==", + "dependencies": { + "@sentry/types": "7.77.0", + "@sentry/utils": "7.77.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/node/node_modules/@sentry/types": { + "version": "7.77.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.77.0.tgz", + "integrity": "sha512-nfb00XRJVi0QpDHg+JkqrmEBHsqBnxJu191Ded+Cs1OJ5oPXEW6F59LVcBScGvMqe+WEk1a73eH8XezwfgrTsA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/node/node_modules/@sentry/utils": { + "version": "7.77.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.77.0.tgz", + "integrity": "sha512-NmM2kDOqVchrey3N5WSzdQoCsyDkQkiRxExPaNI2oKQ/jMWHs9yt0tSy7otPBcXs0AP59ihl75Bvm1tDRcsp5g==", + "dependencies": { + "@sentry/types": "7.77.0" }, "engines": { "node": ">=8" @@ -5835,6 +5877,12 @@ "pretty-format": "^29.0.0" } }, + "node_modules/@types/jmespath": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@types/jmespath/-/jmespath-0.15.1.tgz", + "integrity": "sha512-RWN1HQ71Hjl2ixw4a8s7/Bcz6S9uaBTaoCQ5cJB7OsjgHBFi3GaWMy0vRgZBPSYXdsMKFNxGLUUEh9uRf00Spw==", + "dev": true + }, "node_modules/@types/js-yaml": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.5.tgz", @@ -5907,6 +5955,74 @@ "@types/passport": "*" } }, + "node_modules/@types/pg": { + "version": "8.10.7", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.10.7.tgz", + "integrity": "sha512-ksJqHipwYaSEHz9e1fr6H6erjoEdNNaOxwyJgPx9bNeaqOW3iWBQgVHfpwiSAoqGzchfc+ZyRLwEfeCcyYD3uQ==", + "dev": true, + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^4.0.1" + } + }, + "node_modules/@types/pg/node_modules/pg-types": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.1.tgz", + "integrity": "sha512-hRCSDuLII9/LE3smys1hRHcu5QGcLs9ggT7I/TCs0IE+2Eesxi9+9RWAAwZ0yaGjxoWICF/YHLOEjydGujoJ+g==", + "dev": true, + "dependencies": { + "pg-int8": "1.0.1", + "pg-numeric": "1.0.2", + "postgres-array": "~3.0.1", + "postgres-bytea": "~3.0.0", + "postgres-date": "~2.0.1", + "postgres-interval": "^3.0.0", + "postgres-range": "^1.1.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@types/pg/node_modules/postgres-array": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.2.tgz", + "integrity": "sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@types/pg/node_modules/postgres-bytea": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz", + "integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==", + "dev": true, + "dependencies": { + "obuf": "~1.1.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@types/pg/node_modules/postgres-date": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.0.1.tgz", + "integrity": "sha512-YtMKdsDt5Ojv1wQRvUhnyDJNSr2dGIC96mQVKz7xufp07nfuFONzdaowrMHjlAzY6GDLd4f+LUHHAAM1h4MdUw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@types/pg/node_modules/postgres-interval": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz", + "integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/@types/picomatch": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-2.3.0.tgz", @@ -5914,14 +6030,13 @@ "dev": true }, "node_modules/@types/pino": { - "version": "6.3.12", - "resolved": "https://registry.npmjs.org/@types/pino/-/pino-6.3.12.tgz", - "integrity": "sha512-dsLRTq8/4UtVSpJgl9aeqHvbh6pzdmjYD3C092SYgLD2TyoCqHpTJk6vp8DvCTGGc7iowZ2MoiYiVUUCcu7muw==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/@types/pino/-/pino-7.0.5.tgz", + "integrity": "sha512-wKoab31pknvILkxAF8ss+v9iNyhw5Iu/0jLtRkUD74cNfOOLJNnqfFKAv0r7wVaTQxRZtWrMpGfShwwBjOcgcg==", + "deprecated": "This is a stub types definition. pino provides its own type definitions, so you do not need this installed.", + "dev": true, "dependencies": { - "@types/node": "*", - "@types/pino-pretty": "*", - "@types/pino-std-serializers": "*", - "sonic-boom": "^2.1.0" + "pino": "*" } }, "node_modules/@types/pino-http": { @@ -5932,6 +6047,17 @@ "@types/pino": "6.3" } }, + "node_modules/@types/pino-http/node_modules/@types/pino": { + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/@types/pino/-/pino-6.3.12.tgz", + "integrity": "sha512-dsLRTq8/4UtVSpJgl9aeqHvbh6pzdmjYD3C092SYgLD2TyoCqHpTJk6vp8DvCTGGc7iowZ2MoiYiVUUCcu7muw==", + "dependencies": { + "@types/node": "*", + "@types/pino-pretty": "*", + "@types/pino-std-serializers": "*", + "sonic-boom": "^2.1.0" + } + }, "node_modules/@types/pino-pretty": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/pino-pretty/-/pino-pretty-5.0.0.tgz", @@ -6032,11 +6158,6 @@ "@types/serve-static": "*" } }, - "node_modules/@types/triple-beam": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.2.tgz", - "integrity": "sha512-txGIh+0eDFzKGC25zORnswy+br1Ha7hj5cMVwKIU7+s0U2AxxJru/jZSMU6OC9MJWP6+pc/hc6ZjyZShpsyY2g==" - }, "node_modules/@types/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -6323,6 +6444,17 @@ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -6389,14 +6521,13 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", "dependencies": { "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", "uri-js": "^4.2.2" }, "funding": { @@ -6632,19 +6763,6 @@ "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", @@ -7020,17 +7138,6 @@ "node": ">=14.20.1" } }, - "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/btoa-lite": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz", @@ -7057,6 +7164,14 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "node_modules/buffer-writer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", + "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==", + "engines": { + "node": ">=4" + } + }, "node_modules/bull": { "version": "4.10.4", "resolved": "https://registry.npmjs.org/bull/-/bull-4.10.4.tgz", @@ -7311,15 +7426,6 @@ "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", "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", @@ -7335,16 +7441,8 @@ "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==" - }, - "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" - } + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "node_modules/color-support": { "version": "1.1.3", @@ -7354,32 +7452,10 @@ "color-support": "bin.js" } }, - "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/colorette": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", - "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==" - }, - "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" - } + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==" }, "node_modules/combined-stream": { "version": "1.0.8", @@ -7786,11 +7862,6 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, - "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", @@ -7950,6 +8021,22 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/eslint/node_modules/eslint-scope": { "version": "7.2.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.1.tgz", @@ -7975,6 +8062,12 @@ "node": ">=4.0" } }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -8072,6 +8165,14 @@ "node": ">= 0.6" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, "node_modules/events": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", @@ -8289,11 +8390,15 @@ "node >=0.6.0" ] }, + "node_modules/fast-copy": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.1.tgz", + "integrity": "sha512-Knr7NOtK3HWRYGtHoJrjkaWepqT8thIVGAwt0p0aUs1zqkAzXZV4vo9fFNwyb5fcqK1GKYFYxldQdIDVKhUAfA==" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-glob": { "version": "3.3.0", @@ -8395,11 +8500,6 @@ "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", @@ -8502,11 +8602,6 @@ "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", @@ -8648,6 +8743,14 @@ "node": ">=10" } }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "dependencies": { + "is-property": "^1.0.2" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -8661,7 +8764,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -8915,6 +9017,52 @@ "node": ">=12.0.0" } }, + "node_modules/help-me": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-4.2.0.tgz", + "integrity": "sha512-TAOnTB8Tz5Dw8penUuzHVrKNKlCIbwwbHnXraNJxPwf8LRtE2HlM84RYuezMFcwOJmoYOCWVDyJ8TQGxn9PgxA==", + "dependencies": { + "glob": "^8.0.0", + "readable-stream": "^3.6.0" + } + }, + "node_modules/help-me/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/help-me/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/help-me/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/hexoid": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", @@ -9278,6 +9426,11 @@ "node": ">=0.10.0" } }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==" + }, "node_modules/is-retry-allowed": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz", @@ -9293,6 +9446,7 @@ "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" }, @@ -10027,10 +10181,9 @@ "dev": true }, "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -10126,11 +10279,6 @@ "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", @@ -10262,19 +10410,6 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, - "node_modules/logform": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/logform/-/logform-2.5.1.tgz", - "integrity": "sha512-9FyqAm9o9NKKfiAKfZoYo9bGXXuwMkxQiQttkT4YjjVtQVIQtK6LmVtlxmCaFswo6N4AfEkHqZTV0taDtPotNg==", - "dependencies": { - "@colors/colors": "1.5.0", - "@types/triple-beam": "^1.3.2", - "fecha": "^4.2.0", - "ms": "^2.1.1", - "safe-stable-stringify": "^2.3.1", - "triple-beam": "^1.3.0" - } - }, "node_modules/long": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", @@ -10727,6 +10862,78 @@ "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.2" } }, + "node_modules/mysql2": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.6.2.tgz", + "integrity": "sha512-m5erE6bMoWfPXW1D5UrVwlT8PowAoSX69KcZzPuARQ3wY1RJ52NW9PdvdPo076XiSIkQ5IBTis7hxdlrQTlyug==", + "dependencies": { + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.6.3", + "long": "^5.2.1", + "lru-cache": "^8.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/mysql2/node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/mysql2/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mysql2/node_modules/lru-cache": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz", + "integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==", + "engines": { + "node": ">=16.14" + } + }, + "node_modules/mysql2/node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", + "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", + "dependencies": { + "lru-cache": "^7.14.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/named-placeholders/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "engines": { + "node": ">=12" + } + }, "node_modules/nanoid": { "version": "3.3.6", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", @@ -13555,6 +13762,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, "node_modules/octokit-auth-probot": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/octokit-auth-probot/-/octokit-auth-probot-1.2.9.tgz", @@ -13582,6 +13795,14 @@ "@octokit/openapi-types": "^14.0.0" } }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -13610,14 +13831,6 @@ "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", @@ -13696,6 +13909,11 @@ "node": ">=6" } }, + "node_modules/packet-reader": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", + "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -13861,6 +14079,98 @@ "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" }, + "node_modules/pg": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.3.tgz", + "integrity": "sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==", + "dependencies": { + "buffer-writer": "2.0.0", + "packet-reader": "1.0.0", + "pg-connection-string": "^2.6.2", + "pg-pool": "^3.6.1", + "pg-protocol": "^1.6.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.1.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz", + "integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-numeric": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pg-numeric/-/pg-numeric-1.0.2.tgz", + "integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/pg-pool": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.1.tgz", + "integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz", + "integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dependencies": { + "split2": "^4.1.0" + } + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -13888,73 +14198,206 @@ } }, "node_modules/pino": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-6.14.0.tgz", - "integrity": "sha512-iuhEDel3Z3hF9Jfe44DPXR8l07bhjuFY3GMHIXbjnY9XcafbyDDwl2sN2vw2GjMPf5Nkoe+OFao7ffn9SXaKDg==", + "version": "8.16.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-8.16.1.tgz", + "integrity": "sha512-3bKsVhBmgPjGV9pyn4fO/8RtoVDR8ssW1ev819FsRXlRNgW8gR/9Kx+gCK4UPWd4JjrRDLWpzd/pb1AyWm3MGA==", "dependencies": { - "fast-redact": "^3.0.0", - "fast-safe-stringify": "^2.0.8", - "flatstr": "^1.0.12", - "pino-std-serializers": "^3.1.0", - "process-warning": "^1.0.0", + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "v1.1.0", + "pino-std-serializers": "^6.0.0", + "process-warning": "^2.0.0", "quick-format-unescaped": "^4.0.3", - "sonic-boom": "^1.0.2" + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^3.7.0", + "thread-stream": "^2.0.0" }, "bin": { "pino": "bin.js" } }, - "node_modules/pino-http": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/pino-http/-/pino-http-5.8.0.tgz", - "integrity": "sha512-YwXiyRb9y0WCD1P9PcxuJuh3Dc5qmXde/paJE86UGYRdiFOi828hR9iUGmk5gaw6NBT9gLtKANOHFimvh19U5w==", + "node_modules/pino-abstract-transport": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.1.0.tgz", + "integrity": "sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA==", "dependencies": { - "fast-url-parser": "^1.1.3", - "pino": "^6.13.0", - "pino-std-serializers": "^4.0.0" + "readable-stream": "^4.0.0", + "split2": "^4.0.0" } }, - "node_modules/pino-http/node_modules/pino-std-serializers": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-4.0.0.tgz", - "integrity": "sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q==" + "node_modules/pino-abstract-transport/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/pino-abstract-transport/node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/pino-abstract-transport/node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/pino-abstract-transport/node_modules/readable-stream": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.4.2.tgz", + "integrity": "sha512-Lk/fICSyIhodxy1IDK2HazkeGjSmezAWX2egdtJnYhtzKEsBPJowlI6F6LPb5tqIQILrMbx22S5o3GuJavPusA==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/pino-http": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/pino-http/-/pino-http-8.5.1.tgz", + "integrity": "sha512-T/3d9YHKBYpv/QHjNy73P5BNYYkRrC2/D6CxKMecG4fKFLN+B2iC6LsKYzGRTRV+Ld3fjxFC1ca4TUGbPdzk+Q==", + "dependencies": { + "get-caller-file": "^2.0.5", + "pino": "^8.0.0", + "pino-std-serializers": "^6.0.0", + "process-warning": "^2.0.0" + } }, "node_modules/pino-pretty": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-6.0.0.tgz", - "integrity": "sha512-jyeR2fXXWc68st1DTTM5NhkHlx8p+1fKZMfm84Jwq+jSw08IwAjNaZBZR6ts69hhPOfOjg/NiE1HYW7vBRPL3A==", + "version": "10.2.3", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-10.2.3.tgz", + "integrity": "sha512-4jfIUc8TC1GPUfDyMSlW1STeORqkoxec71yhxIpLDQapUu8WOuoz2TTCoidrIssyz78LZC69whBMPIKCMbi3cw==", "dependencies": { - "@hapi/bourne": "^2.0.0", - "args": "^5.0.1", - "colorette": "^1.3.0", - "dateformat": "^4.5.1", - "fast-safe-stringify": "^2.0.7", - "jmespath": "^0.15.0", - "joycon": "^3.0.0", + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^3.0.0", + "fast-safe-stringify": "^2.1.1", + "help-me": "^4.0.1", + "joycon": "^3.1.1", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^1.0.0", "pump": "^3.0.0", - "readable-stream": "^3.6.0", - "rfdc": "^1.3.0", - "split2": "^3.1.1", + "readable-stream": "^4.0.0", + "secure-json-parse": "^2.4.0", + "sonic-boom": "^3.0.0", "strip-json-comments": "^3.1.1" }, "bin": { "pino-pretty": "bin.js" } }, - "node_modules/pino-pretty/node_modules/jmespath": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", - "integrity": "sha512-+kHj8HXArPfpPEKGLZ+kB5ONRTCiGQXo8RQYL0hH8t6pWXUBBK5KkkQmTNOwKK4LEsd0yTsgtjJVm4UBSZea4w==", - "engines": { - "node": ">= 0.6.0" + "node_modules/pino-pretty/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" } }, - "node_modules/pino-pretty/node_modules/split2": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", - "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", + "node_modules/pino-pretty/node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/pino-pretty/node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/pino-pretty/node_modules/readable-stream": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.4.2.tgz", + "integrity": "sha512-Lk/fICSyIhodxy1IDK2HazkeGjSmezAWX2egdtJnYhtzKEsBPJowlI6F6LPb5tqIQILrMbx22S5o3GuJavPusA==", "dependencies": { - "readable-stream": "^3.0.0" + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/pino-pretty/node_modules/sonic-boom": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.7.0.tgz", + "integrity": "sha512-IudtNvSqA/ObjN97tfgNmOKyDOs4dNcg4cUUsHDebqsgb8wGBBwb31LIgShNO8fye0dFI52X1+tFoKKI6Rq1Gg==", + "dependencies": { + "atomic-sleep": "^1.0.0" } }, "node_modules/pino-std-serializers": { @@ -13962,18 +14405,12 @@ "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz", "integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==" }, - "node_modules/pino/node_modules/pino-std-serializers": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-3.2.0.tgz", - "integrity": "sha512-EqX4pwDPrt3MuOAAUBMU0Tk5kR/YcCM5fNPEzgCO2zJ5HfX0vbiH9HbJglnyeQsN96Kznae6MWD47pZB5avTrg==" - }, "node_modules/pino/node_modules/sonic-boom": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-1.4.1.tgz", - "integrity": "sha512-LRHh/A8tpW7ru89lrlkU4AszXt1dbwSjVWguGrmlxE7tawVmDBlI1PILMkXAxJTwqhgsEeTHzj36D5CmHgQmNg==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.7.0.tgz", + "integrity": "sha512-IudtNvSqA/ObjN97tfgNmOKyDOs4dNcg4cUUsHDebqsgb8wGBBwb31LIgShNO8fye0dFI52X1+tFoKKI6Rq1Gg==", "dependencies": { - "atomic-sleep": "^1.0.0", - "flatstr": "^1.0.12" + "atomic-sleep": "^1.0.0" } }, "node_modules/pirates": { @@ -14117,6 +14554,47 @@ "node": ">=8" } }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-range": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.3.tgz", + "integrity": "sha512-VdlZoocy5lCP0c/t66xAfclglEapXPCIVhqqJRncYpvbCgImF0w67aPKfbqUMr72tO2k5q0TdTZwCLjPTI6C9g==", + "dev": true + }, "node_modules/posthog-node": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-2.6.0.tgz", @@ -14448,6 +14926,25 @@ "@octokit/openapi-types": "^14.0.0" } }, + "node_modules/probot/node_modules/@types/pino": { + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/@types/pino/-/pino-6.3.12.tgz", + "integrity": "sha512-dsLRTq8/4UtVSpJgl9aeqHvbh6pzdmjYD3C092SYgLD2TyoCqHpTJk6vp8DvCTGGc7iowZ2MoiYiVUUCcu7muw==", + "dependencies": { + "@types/node": "*", + "@types/pino-pretty": "*", + "@types/pino-std-serializers": "*", + "sonic-boom": "^2.1.0" + } + }, + "node_modules/probot/node_modules/@types/pino/node_modules/sonic-boom": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-2.8.0.tgz", + "integrity": "sha512-kuonw1YOYYNOve5iHdSahXPOK49GqwA+LZhI6Wz/l0rP57iKyXXIHaRagOBHAPmGwJC6od2Z9zgvZ5loSgMlVg==", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, "node_modules/probot/node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -14512,15 +15009,74 @@ "node": ">=10" } }, + "node_modules/probot/node_modules/pino": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-6.14.0.tgz", + "integrity": "sha512-iuhEDel3Z3hF9Jfe44DPXR8l07bhjuFY3GMHIXbjnY9XcafbyDDwl2sN2vw2GjMPf5Nkoe+OFao7ffn9SXaKDg==", + "dependencies": { + "fast-redact": "^3.0.0", + "fast-safe-stringify": "^2.0.8", + "flatstr": "^1.0.12", + "pino-std-serializers": "^3.1.0", + "process-warning": "^1.0.0", + "quick-format-unescaped": "^4.0.3", + "sonic-boom": "^1.0.2" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/probot/node_modules/pino-http": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/pino-http/-/pino-http-5.8.0.tgz", + "integrity": "sha512-YwXiyRb9y0WCD1P9PcxuJuh3Dc5qmXde/paJE86UGYRdiFOi828hR9iUGmk5gaw6NBT9gLtKANOHFimvh19U5w==", + "dependencies": { + "fast-url-parser": "^1.1.3", + "pino": "^6.13.0", + "pino-std-serializers": "^4.0.0" + } + }, + "node_modules/probot/node_modules/pino-http/node_modules/pino-std-serializers": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-4.0.0.tgz", + "integrity": "sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q==" + }, + "node_modules/probot/node_modules/pino-std-serializers": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-3.2.0.tgz", + "integrity": "sha512-EqX4pwDPrt3MuOAAUBMU0Tk5kR/YcCM5fNPEzgCO2zJ5HfX0vbiH9HbJglnyeQsN96Kznae6MWD47pZB5avTrg==" + }, + "node_modules/probot/node_modules/process-warning": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-1.0.0.tgz", + "integrity": "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==" + }, + "node_modules/probot/node_modules/sonic-boom": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-1.4.1.tgz", + "integrity": "sha512-LRHh/A8tpW7ru89lrlkU4AszXt1dbwSjVWguGrmlxE7tawVmDBlI1PILMkXAxJTwqhgsEeTHzj36D5CmHgQmNg==", + "dependencies": { + "atomic-sleep": "^1.0.0", + "flatstr": "^1.0.12" + } + }, "node_modules/probot/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/process-warning": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-1.0.0.tgz", - "integrity": "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==" + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.3.0.tgz", + "integrity": "sha512-N6mp1+2jpQr3oCFMz6SeHRGbv6Slb20bRhj4v3xR99HqNToAcOe1MFOp4tytyzOfJn+QtN8Rf7U/h2KAn4kC6g==" }, "node_modules/prompts": { "version": "2.4.2", @@ -14535,29 +15091,6 @@ "node": ">= 6" } }, - "node_modules/protobufjs": { - "version": "7.2.4", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.4.tgz", - "integrity": "sha512-AT+RJgD2sH8phPmCf7OUZR8xGdcJRga4+1cOaXJ64hvcSkVhNcRHOwIxUatPH15+nj59WAGTDv3LSGZPEQbJaQ==", - "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/node": ">=13.7.0", - "long": "^5.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -14799,6 +15332,14 @@ "node": ">=8.10.0" } }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "engines": { + "node": ">= 12.13.0" + } + }, "node_modules/redis-commands": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz", @@ -14837,6 +15378,14 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.2", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", @@ -14990,6 +15539,11 @@ "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==" }, + "node_modules/secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==" + }, "node_modules/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -15061,6 +15615,11 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, "node_modules/serve-static": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", @@ -15141,19 +15700,6 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, - "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.1.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz", @@ -15328,14 +15874,6 @@ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" }, - "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", @@ -15647,17 +16185,20 @@ "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", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/thread-stream": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.4.1.tgz", + "integrity": "sha512-d/Ex2iWd1whipbT681JmTINKw0ZwOUBZm7+Gjs64DHuX34mmw8vJL2bFAaNacaW72zYiTJxSHi5abUuOi5nsfg==", + "dependencies": { + "real-require": "^0.2.0" + } + }, "node_modules/tiny-lru": { "version": "11.0.1", "resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-11.0.1.tgz", @@ -15748,14 +16289,6 @@ "node": ">=6" } }, - "node_modules/triple-beam": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", - "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", - "engines": { - "node": ">= 14.0.0" - } - }, "node_modules/ts-jest": { "version": "29.1.1", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.1.tgz", @@ -16040,7 +16573,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "dependencies": { "punycode": "^2.1.0" } @@ -16049,7 +16581,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", - "dev": true, "engines": { "node": ">=6" } @@ -16063,11 +16594,6 @@ "querystring": "0.2.0" } }, - "node_modules/url-polyfill": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/url-polyfill/-/url-polyfill-1.1.12.tgz", - "integrity": "sha512-mYFmBHCapZjtcNHW0MDq9967t+z4Dmg5CJ0KqysK3+ZbyoNOWQHksGCTWwDhxGXllkWlOc10Xfko6v4a3ucM6A==" - }, "node_modules/url/node_modules/punycode": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", @@ -16240,278 +16766,6 @@ "string-width": "^1.0.2 || 2 || 3 || 4" } }, - "node_modules/winston": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.10.0.tgz", - "integrity": "sha512-nT6SIDaE9B7ZRO0u3UvdrimG0HkB7dSTAgInQnNR2SOPJ4bvq5q79+pXLftKmP52lJGW15+H5MCK0nM9D3KB/g==", - "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.7", - "resolved": "https://registry.npmjs.org/winston-loki/-/winston-loki-6.0.7.tgz", - "integrity": "sha512-6psEy428ckTXP/ni+DrA4vas2Ldt/43TFm7beoVExKLlTyEwu7zu2c8GBGbnxrdzRQAxXUfom2sK7zr4QP8y5g==", - "dependencies": { - "async-exit-hook": "2.0.1", - "btoa": "^1.2.1", - "protobufjs": "^7.2.4", - "url-polyfill": "^1.1.12", - "winston-transport": "^4.3.0" - }, - "optionalDependencies": { - "snappy": "7.1.1" - } - }, - "node_modules/winston-loki/node_modules/@napi-rs/snappy-android-arm-eabi": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/snappy-android-arm-eabi/-/snappy-android-arm-eabi-7.1.1.tgz", - "integrity": "sha512-NKd/ztuVEgQaAaNVQ5zZaCB9VV+7+uBXBHqhaE5iSapQhLc41szTlT0s68FCee75OoT3vhqdA6Jp5TrzZ2WOaw==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/winston-loki/node_modules/@napi-rs/snappy-android-arm64": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/snappy-android-arm64/-/snappy-android-arm64-7.1.1.tgz", - "integrity": "sha512-DktruMAO0K0toTnxNHg2GWNIAPJqdvIchCsdsRaKyuEnG101qBg0mYiRCAhxHgbT6RJlOGbUPKkbA9KKRhEJUg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/winston-loki/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/winston-loki/node_modules/@napi-rs/snappy-darwin-x64": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/snappy-darwin-x64/-/snappy-darwin-x64-7.1.1.tgz", - "integrity": "sha512-X1D2F67bQkPwr5iSR29/RnOrLwAkB55YO6t41toABzla3mflLDpzZcakz6FokIukykf7ey31/t73v/4pbgaBkg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/winston-loki/node_modules/@napi-rs/snappy-freebsd-x64": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/snappy-freebsd-x64/-/snappy-freebsd-x64-7.1.1.tgz", - "integrity": "sha512-vSeuf+An8jFVHPAn5IbWE9hTGU9PFAaZLj/X7rKTQQtZstnDsHgWe6u4g7FHLuOdwQ8TvhcxAEpNlYIXIk4AJg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/winston-loki/node_modules/@napi-rs/snappy-linux-arm-gnueabihf": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/snappy-linux-arm-gnueabihf/-/snappy-linux-arm-gnueabihf-7.1.1.tgz", - "integrity": "sha512-/yyN6QsnOs3D1+jI3SfRX+gtnD86rbixdfmgxv9g40+FrDaDTLAu/3VuZIqH02qqq/xiWbDnkO+42RGxXDzTCw==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/winston-loki/node_modules/@napi-rs/snappy-linux-arm64-gnu": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/snappy-linux-arm64-gnu/-/snappy-linux-arm64-gnu-7.1.1.tgz", - "integrity": "sha512-StEeUCSwUoajgrBtiCQPTkHu+0Q4QlYndghGZNdbN1zJ1ny70YzPpevaFBUyjI/eJ+FN9uICKtwTPtQNSILS5g==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/winston-loki/node_modules/@napi-rs/snappy-linux-arm64-musl": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/snappy-linux-arm64-musl/-/snappy-linux-arm64-musl-7.1.1.tgz", - "integrity": "sha512-jWEBRzj+lswZVYf0b5eY0fjMlBL9L9yqjmTuv2UIMjJNHPuR282LK/s3Fm9sYIXQtKkiCo5JyhmIcoghZ3v0Eg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/winston-loki/node_modules/@napi-rs/snappy-linux-x64-gnu": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/snappy-linux-x64-gnu/-/snappy-linux-x64-gnu-7.1.1.tgz", - "integrity": "sha512-41DPoAUFAU9VNrj/96qKfStH2Xq88ZYIsSz8BlITDm2ScoeDGOGbmaWguCXU7I+bC2uKWTmUVMXKqk6tVY6LEg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/winston-loki/node_modules/@napi-rs/snappy-linux-x64-musl": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/snappy-linux-x64-musl/-/snappy-linux-x64-musl-7.1.1.tgz", - "integrity": "sha512-xR4hzFQqVq6J8Zf6XyUVtFJBaRgDyAQYUoBsCr92tZ7gI/0RlWCV6Q6JMO/wP5CSsvyFJIAtSUXXqlzIpw0GPA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/winston-loki/node_modules/@napi-rs/snappy-win32-arm64-msvc": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/snappy-win32-arm64-msvc/-/snappy-win32-arm64-msvc-7.1.1.tgz", - "integrity": "sha512-2mHPadctsaYtrfSV5Na8ooTdI5rflPxP1pceY4us6vbjeWrfgB+KQCuEFOHsGXqFNfsi6L9nWH8nB9swnxnSyw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/winston-loki/node_modules/@napi-rs/snappy-win32-ia32-msvc": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/snappy-win32-ia32-msvc/-/snappy-win32-ia32-msvc-7.1.1.tgz", - "integrity": "sha512-FOMgs9W71hdgjyl3T9F7b/WEIuoryfgBqsyhtHjAaa/98R0BUHl0bOoHg8ka0b5GgnhLBHkX2Yd6VD+Si9Q2ww==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/winston-loki/node_modules/@napi-rs/snappy-win32-x64-msvc": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/snappy-win32-x64-msvc/-/snappy-win32-x64-msvc-7.1.1.tgz", - "integrity": "sha512-Mu3yELySvzhBcNTVCq+hYxVh+lH3/KjoQ5HIEb3DDPoX0AGRTm3XZa+usq8pFWjl91Cgp9nWK+9lVSkCCIRaKA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/winston-loki/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/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/wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", @@ -16635,6 +16889,14 @@ "node": ">=0.6.0" } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -19476,11 +19738,6 @@ "integrity": "sha512-pojgSWYKNIwFM6wWDNct1YD0+8nIxhe2jp5jBbK8JGU60dEs2o0Yw3mCo2y7nBwbvRC2oEots/BlLMVb1Wdo8A==", "requires": {} }, - "@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", @@ -19502,16 +19759,6 @@ } } }, - "@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-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -19542,6 +19789,26 @@ "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + } } }, "@eslint/js": { @@ -20578,6 +20845,45 @@ "tslib": "^1.9.3" } }, + "colorette": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", + "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==" + }, + "jmespath": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", + "integrity": "sha512-+kHj8HXArPfpPEKGLZ+kB5ONRTCiGQXo8RQYL0hH8t6pWXUBBK5KkkQmTNOwKK4LEsd0yTsgtjJVm4UBSZea4w==" + }, + "pino-pretty": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-6.0.0.tgz", + "integrity": "sha512-jyeR2fXXWc68st1DTTM5NhkHlx8p+1fKZMfm84Jwq+jSw08IwAjNaZBZR6ts69hhPOfOjg/NiE1HYW7vBRPL3A==", + "requires": { + "@hapi/bourne": "^2.0.0", + "args": "^5.0.1", + "colorette": "^1.3.0", + "dateformat": "^4.5.1", + "fast-safe-stringify": "^2.0.7", + "jmespath": "^0.15.0", + "joycon": "^3.0.0", + "pump": "^3.0.0", + "readable-stream": "^3.6.0", + "rfdc": "^1.3.0", + "split2": "^3.1.1", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "split2": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", + "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", + "requires": { + "readable-stream": "^3.0.0" + } + } + } + }, "tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", @@ -20585,60 +20891,6 @@ } } }, - "@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-internal/tracing": { "version": "7.59.3", "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.59.3.tgz", @@ -20714,18 +20966,49 @@ } }, "@sentry/node": { - "version": "7.59.3", - "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.59.3.tgz", - "integrity": "sha512-5dG90YzmKjuy5TK04qDuc9LBxnGfqsZw8Oh9giwOBfQVCaFp7R14AXyQ1F8k3iUF+4sGeTvoqi9I/GKAItVmlA==", + "version": "7.77.0", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.77.0.tgz", + "integrity": "sha512-Ob5tgaJOj0OYMwnocc6G/CDLWC7hXfVvKX/ofkF98+BbN/tQa5poL+OwgFn9BA8ud8xKzyGPxGU6LdZ8Oh3z/g==", "requires": { - "@sentry-internal/tracing": "7.59.3", - "@sentry/core": "7.59.3", - "@sentry/types": "7.59.3", - "@sentry/utils": "7.59.3", - "cookie": "^0.4.1", - "https-proxy-agent": "^5.0.0", - "lru_map": "^0.3.3", - "tslib": "^2.4.1 || ^1.9.3" + "@sentry-internal/tracing": "7.77.0", + "@sentry/core": "7.77.0", + "@sentry/types": "7.77.0", + "@sentry/utils": "7.77.0", + "https-proxy-agent": "^5.0.0" + }, + "dependencies": { + "@sentry-internal/tracing": { + "version": "7.77.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.77.0.tgz", + "integrity": "sha512-8HRF1rdqWwtINqGEdx8Iqs9UOP/n8E0vXUu3Nmbqj4p5sQPA7vvCfq+4Y4rTqZFc7sNdFpDsRION5iQEh8zfZw==", + "requires": { + "@sentry/core": "7.77.0", + "@sentry/types": "7.77.0", + "@sentry/utils": "7.77.0" + } + }, + "@sentry/core": { + "version": "7.77.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.77.0.tgz", + "integrity": "sha512-Tj8oTYFZ/ZD+xW8IGIsU6gcFXD/gfE+FUxUaeSosd9KHwBQNOLhZSsYo/tTVf/rnQI/dQnsd4onPZLiL+27aTg==", + "requires": { + "@sentry/types": "7.77.0", + "@sentry/utils": "7.77.0" + } + }, + "@sentry/types": { + "version": "7.77.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.77.0.tgz", + "integrity": "sha512-nfb00XRJVi0QpDHg+JkqrmEBHsqBnxJu191Ded+Cs1OJ5oPXEW6F59LVcBScGvMqe+WEk1a73eH8XezwfgrTsA==" + }, + "@sentry/utils": { + "version": "7.77.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.77.0.tgz", + "integrity": "sha512-NmM2kDOqVchrey3N5WSzdQoCsyDkQkiRxExPaNI2oKQ/jMWHs9yt0tSy7otPBcXs0AP59ihl75Bvm1tDRcsp5g==", + "requires": { + "@sentry/types": "7.77.0" + } + } } }, "@sentry/tracing": { @@ -21382,6 +21665,12 @@ "pretty-format": "^29.0.0" } }, + "@types/jmespath": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@types/jmespath/-/jmespath-0.15.1.tgz", + "integrity": "sha512-RWN1HQ71Hjl2ixw4a8s7/Bcz6S9uaBTaoCQ5cJB7OsjgHBFi3GaWMy0vRgZBPSYXdsMKFNxGLUUEh9uRf00Spw==", + "dev": true + }, "@types/js-yaml": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.5.tgz", @@ -21454,6 +21743,61 @@ "@types/passport": "*" } }, + "@types/pg": { + "version": "8.10.7", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.10.7.tgz", + "integrity": "sha512-ksJqHipwYaSEHz9e1fr6H6erjoEdNNaOxwyJgPx9bNeaqOW3iWBQgVHfpwiSAoqGzchfc+ZyRLwEfeCcyYD3uQ==", + "dev": true, + "requires": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^4.0.1" + }, + "dependencies": { + "pg-types": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.1.tgz", + "integrity": "sha512-hRCSDuLII9/LE3smys1hRHcu5QGcLs9ggT7I/TCs0IE+2Eesxi9+9RWAAwZ0yaGjxoWICF/YHLOEjydGujoJ+g==", + "dev": true, + "requires": { + "pg-int8": "1.0.1", + "pg-numeric": "1.0.2", + "postgres-array": "~3.0.1", + "postgres-bytea": "~3.0.0", + "postgres-date": "~2.0.1", + "postgres-interval": "^3.0.0", + "postgres-range": "^1.1.1" + } + }, + "postgres-array": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.2.tgz", + "integrity": "sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==", + "dev": true + }, + "postgres-bytea": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz", + "integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==", + "dev": true, + "requires": { + "obuf": "~1.1.2" + } + }, + "postgres-date": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.0.1.tgz", + "integrity": "sha512-YtMKdsDt5Ojv1wQRvUhnyDJNSr2dGIC96mQVKz7xufp07nfuFONzdaowrMHjlAzY6GDLd4f+LUHHAAM1h4MdUw==", + "dev": true + }, + "postgres-interval": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz", + "integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==", + "dev": true + } + } + }, "@types/picomatch": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-2.3.0.tgz", @@ -21461,14 +21805,12 @@ "dev": true }, "@types/pino": { - "version": "6.3.12", - "resolved": "https://registry.npmjs.org/@types/pino/-/pino-6.3.12.tgz", - "integrity": "sha512-dsLRTq8/4UtVSpJgl9aeqHvbh6pzdmjYD3C092SYgLD2TyoCqHpTJk6vp8DvCTGGc7iowZ2MoiYiVUUCcu7muw==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/@types/pino/-/pino-7.0.5.tgz", + "integrity": "sha512-wKoab31pknvILkxAF8ss+v9iNyhw5Iu/0jLtRkUD74cNfOOLJNnqfFKAv0r7wVaTQxRZtWrMpGfShwwBjOcgcg==", + "dev": true, "requires": { - "@types/node": "*", - "@types/pino-pretty": "*", - "@types/pino-std-serializers": "*", - "sonic-boom": "^2.1.0" + "pino": "*" } }, "@types/pino-http": { @@ -21477,6 +21819,19 @@ "integrity": "sha512-A9MW6VCnx5ii7s+Fs5aFIw+aSZcBCpsZ/atpxamu8tTsvWFacxSf2Hrn1Ohn1jkVRB/LiPGOapRXcFawDBnDnA==", "requires": { "@types/pino": "6.3" + }, + "dependencies": { + "@types/pino": { + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/@types/pino/-/pino-6.3.12.tgz", + "integrity": "sha512-dsLRTq8/4UtVSpJgl9aeqHvbh6pzdmjYD3C092SYgLD2TyoCqHpTJk6vp8DvCTGGc7iowZ2MoiYiVUUCcu7muw==", + "requires": { + "@types/node": "*", + "@types/pino-pretty": "*", + "@types/pino-std-serializers": "*", + "sonic-boom": "^2.1.0" + } + } } }, "@types/pino-pretty": { @@ -21577,11 +21932,6 @@ "@types/serve-static": "*" } }, - "@types/triple-beam": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.2.tgz", - "integrity": "sha512-txGIh+0eDFzKGC25zORnswy+br1Ha7hj5cMVwKIU7+s0U2AxxJru/jZSMU6OC9MJWP6+pc/hc6ZjyZShpsyY2g==" - }, "@types/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -21776,6 +22126,14 @@ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, + "abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "requires": { + "event-target-shim": "^5.0.0" + } + }, "accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -21822,14 +22180,13 @@ } }, "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", "requires": { "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", "uri-js": "^4.2.2" } }, @@ -22004,16 +22361,6 @@ "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", @@ -22299,11 +22646,6 @@ "resolved": "https://registry.npmjs.org/bson/-/bson-5.5.1.tgz", "integrity": "sha512-ix0EwukN2EpC0SRWIj/7B5+A6uQMQy6KMREI9qQqvgpkV2frH63T0UDVd1SYedL6dNCmDBYB3QtXi4ISk9YT+g==" }, - "btoa": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", - "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==" - }, "btoa-lite": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz", @@ -22330,6 +22672,11 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "buffer-writer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", + "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==" + }, "bull": { "version": "4.10.4", "resolved": "https://registry.npmjs.org/bull/-/bull-4.10.4.tgz", @@ -22497,30 +22844,6 @@ "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", "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", @@ -22533,16 +22856,8 @@ "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==" - }, - "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" - } + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "color-support": { "version": "1.1.3", @@ -22550,18 +22865,9 @@ "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==" }, "colorette": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", - "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==" - }, - "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" - } + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==" }, "combined-stream": { "version": "1.0.8", @@ -22867,11 +23173,6 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, - "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", @@ -22955,6 +23256,18 @@ "text-table": "^0.2.0" }, "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, "eslint-scope": { "version": "7.2.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.1.tgz", @@ -22970,6 +23283,12 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true } } }, @@ -23071,6 +23390,11 @@ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" }, + "event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" + }, "events": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", @@ -23243,11 +23567,15 @@ "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", "dev": true }, + "fast-copy": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.1.tgz", + "integrity": "sha512-Knr7NOtK3HWRYGtHoJrjkaWepqT8thIVGAwt0p0aUs1zqkAzXZV4vo9fFNwyb5fcqK1GKYFYxldQdIDVKhUAfA==" + }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "fast-glob": { "version": "3.3.0", @@ -23329,11 +23657,6 @@ "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", @@ -23417,11 +23740,6 @@ "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", @@ -23523,6 +23841,14 @@ "wide-align": "^1.1.2" } }, + "generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "requires": { + "is-property": "^1.0.2" + } + }, "gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -23532,8 +23858,7 @@ "get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" }, "get-intrinsic": { "version": "1.2.1", @@ -23700,6 +24025,45 @@ "resolved": "https://registry.npmjs.org/helmet/-/helmet-5.1.1.tgz", "integrity": "sha512-/yX0oVZBggA9cLJh8aw3PPCfedBnbd7J2aowjzsaWwZh7/UFY0nccn/aHAggIgWUFfnykX8GKd3a1pSbrmlcVQ==" }, + "help-me": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-4.2.0.tgz", + "integrity": "sha512-TAOnTB8Tz5Dw8penUuzHVrKNKlCIbwwbHnXraNJxPwf8LRtE2HlM84RYuezMFcwOJmoYOCWVDyJ8TQGxn9PgxA==", + "requires": { + "glob": "^8.0.0", + "readable-stream": "^3.6.0" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "requires": { + "balanced-match": "^1.0.0" + } + }, + "glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + } + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, "hexoid": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", @@ -23961,6 +24325,11 @@ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" }, + "is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==" + }, "is-retry-allowed": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz", @@ -23969,7 +24338,8 @@ "is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true }, "is-typed-array": { "version": "1.1.12", @@ -24533,10 +24903,9 @@ "dev": true }, "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, "json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -24613,11 +24982,6 @@ "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", @@ -24727,19 +25091,6 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, - "logform": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/logform/-/logform-2.5.1.tgz", - "integrity": "sha512-9FyqAm9o9NKKfiAKfZoYo9bGXXuwMkxQiQttkT4YjjVtQVIQtK6LmVtlxmCaFswo6N4AfEkHqZTV0taDtPotNg==", - "requires": { - "@colors/colors": "1.5.0", - "@types/triple-beam": "^1.3.2", - "fecha": "^4.2.0", - "ms": "^2.1.1", - "safe-stable-stringify": "^2.3.1", - "triple-beam": "^1.3.0" - } - }, "long": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", @@ -25069,6 +25420,61 @@ "node-gyp-build-optional-packages": "5.0.7" } }, + "mysql2": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.6.2.tgz", + "integrity": "sha512-m5erE6bMoWfPXW1D5UrVwlT8PowAoSX69KcZzPuARQ3wY1RJ52NW9PdvdPo076XiSIkQ5IBTis7hxdlrQTlyug==", + "requires": { + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.6.3", + "long": "^5.2.1", + "lru-cache": "^8.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "dependencies": { + "denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==" + }, + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + }, + "lru-cache": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz", + "integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==" + }, + "sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==" + } + } + }, + "named-placeholders": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", + "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", + "requires": { + "lru-cache": "^7.14.1" + }, + "dependencies": { + "lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==" + } + } + }, "nanoid": { "version": "3.3.6", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", @@ -27050,6 +27456,12 @@ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==" }, + "obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, "octokit-auth-probot": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/octokit-auth-probot/-/octokit-auth-probot-1.2.9.tgz", @@ -27076,6 +27488,11 @@ } } }, + "on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==" + }, "on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -27098,14 +27515,6 @@ "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", @@ -27157,6 +27566,11 @@ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" }, + "packet-reader": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", + "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==" + }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -27272,6 +27686,74 @@ "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" }, + "pg": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.3.tgz", + "integrity": "sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==", + "requires": { + "buffer-writer": "2.0.0", + "packet-reader": "1.0.0", + "pg-cloudflare": "^1.1.1", + "pg-connection-string": "^2.6.2", + "pg-pool": "^3.6.1", + "pg-protocol": "^1.6.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + } + }, + "pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "optional": true + }, + "pg-connection-string": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz", + "integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==" + }, + "pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==" + }, + "pg-numeric": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pg-numeric/-/pg-numeric-1.0.2.tgz", + "integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==", + "dev": true + }, + "pg-pool": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.1.tgz", + "integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==", + "requires": {} + }, + "pg-protocol": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz", + "integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==" + }, + "pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "requires": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + } + }, + "pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "requires": { + "split2": "^4.1.0" + } + }, "picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -27290,82 +27772,144 @@ "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" }, "pino": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-6.14.0.tgz", - "integrity": "sha512-iuhEDel3Z3hF9Jfe44DPXR8l07bhjuFY3GMHIXbjnY9XcafbyDDwl2sN2vw2GjMPf5Nkoe+OFao7ffn9SXaKDg==", + "version": "8.16.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-8.16.1.tgz", + "integrity": "sha512-3bKsVhBmgPjGV9pyn4fO/8RtoVDR8ssW1ev819FsRXlRNgW8gR/9Kx+gCK4UPWd4JjrRDLWpzd/pb1AyWm3MGA==", "requires": { - "fast-redact": "^3.0.0", - "fast-safe-stringify": "^2.0.8", - "flatstr": "^1.0.12", - "pino-std-serializers": "^3.1.0", - "process-warning": "^1.0.0", + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "v1.1.0", + "pino-std-serializers": "^6.0.0", + "process-warning": "^2.0.0", "quick-format-unescaped": "^4.0.3", - "sonic-boom": "^1.0.2" + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^3.7.0", + "thread-stream": "^2.0.0" }, "dependencies": { - "pino-std-serializers": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-3.2.0.tgz", - "integrity": "sha512-EqX4pwDPrt3MuOAAUBMU0Tk5kR/YcCM5fNPEzgCO2zJ5HfX0vbiH9HbJglnyeQsN96Kznae6MWD47pZB5avTrg==" - }, "sonic-boom": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-1.4.1.tgz", - "integrity": "sha512-LRHh/A8tpW7ru89lrlkU4AszXt1dbwSjVWguGrmlxE7tawVmDBlI1PILMkXAxJTwqhgsEeTHzj36D5CmHgQmNg==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.7.0.tgz", + "integrity": "sha512-IudtNvSqA/ObjN97tfgNmOKyDOs4dNcg4cUUsHDebqsgb8wGBBwb31LIgShNO8fye0dFI52X1+tFoKKI6Rq1Gg==", "requires": { - "atomic-sleep": "^1.0.0", - "flatstr": "^1.0.12" + "atomic-sleep": "^1.0.0" + } + } + } + }, + "pino-abstract-transport": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.1.0.tgz", + "integrity": "sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA==", + "requires": { + "readable-stream": "^4.0.0", + "split2": "^4.0.0" + }, + "dependencies": { + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "readable-stream": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.4.2.tgz", + "integrity": "sha512-Lk/fICSyIhodxy1IDK2HazkeGjSmezAWX2egdtJnYhtzKEsBPJowlI6F6LPb5tqIQILrMbx22S5o3GuJavPusA==", + "requires": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" } } } }, "pino-http": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/pino-http/-/pino-http-5.8.0.tgz", - "integrity": "sha512-YwXiyRb9y0WCD1P9PcxuJuh3Dc5qmXde/paJE86UGYRdiFOi828hR9iUGmk5gaw6NBT9gLtKANOHFimvh19U5w==", + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/pino-http/-/pino-http-8.5.1.tgz", + "integrity": "sha512-T/3d9YHKBYpv/QHjNy73P5BNYYkRrC2/D6CxKMecG4fKFLN+B2iC6LsKYzGRTRV+Ld3fjxFC1ca4TUGbPdzk+Q==", "requires": { - "fast-url-parser": "^1.1.3", - "pino": "^6.13.0", - "pino-std-serializers": "^4.0.0" - }, - "dependencies": { - "pino-std-serializers": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-4.0.0.tgz", - "integrity": "sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q==" - } + "get-caller-file": "^2.0.5", + "pino": "^8.0.0", + "pino-std-serializers": "^6.0.0", + "process-warning": "^2.0.0" } }, "pino-pretty": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-6.0.0.tgz", - "integrity": "sha512-jyeR2fXXWc68st1DTTM5NhkHlx8p+1fKZMfm84Jwq+jSw08IwAjNaZBZR6ts69hhPOfOjg/NiE1HYW7vBRPL3A==", + "version": "10.2.3", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-10.2.3.tgz", + "integrity": "sha512-4jfIUc8TC1GPUfDyMSlW1STeORqkoxec71yhxIpLDQapUu8WOuoz2TTCoidrIssyz78LZC69whBMPIKCMbi3cw==", "requires": { - "@hapi/bourne": "^2.0.0", - "args": "^5.0.1", - "colorette": "^1.3.0", - "dateformat": "^4.5.1", - "fast-safe-stringify": "^2.0.7", - "jmespath": "^0.15.0", - "joycon": "^3.0.0", + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^3.0.0", + "fast-safe-stringify": "^2.1.1", + "help-me": "^4.0.1", + "joycon": "^3.1.1", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^1.0.0", "pump": "^3.0.0", - "readable-stream": "^3.6.0", - "rfdc": "^1.3.0", - "split2": "^3.1.1", + "readable-stream": "^4.0.0", + "secure-json-parse": "^2.4.0", + "sonic-boom": "^3.0.0", "strip-json-comments": "^3.1.1" }, "dependencies": { - "jmespath": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", - "integrity": "sha512-+kHj8HXArPfpPEKGLZ+kB5ONRTCiGQXo8RQYL0hH8t6pWXUBBK5KkkQmTNOwKK4LEsd0yTsgtjJVm4UBSZea4w==" - }, - "split2": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", - "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", "requires": { - "readable-stream": "^3.0.0" + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "readable-stream": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.4.2.tgz", + "integrity": "sha512-Lk/fICSyIhodxy1IDK2HazkeGjSmezAWX2egdtJnYhtzKEsBPJowlI6F6LPb5tqIQILrMbx22S5o3GuJavPusA==", + "requires": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + } + }, + "sonic-boom": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.7.0.tgz", + "integrity": "sha512-IudtNvSqA/ObjN97tfgNmOKyDOs4dNcg4cUUsHDebqsgb8wGBBwb31LIgShNO8fye0dFI52X1+tFoKKI6Rq1Gg==", + "requires": { + "atomic-sleep": "^1.0.0" } } } @@ -27478,6 +28022,35 @@ } } }, + "postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==" + }, + "postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==" + }, + "postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==" + }, + "postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "requires": { + "xtend": "^4.0.0" + } + }, + "postgres-range": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.3.tgz", + "integrity": "sha512-VdlZoocy5lCP0c/t66xAfclglEapXPCIVhqqJRncYpvbCgImF0w67aPKfbqUMr72tO2k5q0TdTZwCLjPTI6C9g==", + "dev": true + }, "posthog-node": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-2.6.0.tgz", @@ -27801,6 +28374,27 @@ "@octokit/openapi-types": "^14.0.0" } }, + "@types/pino": { + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/@types/pino/-/pino-6.3.12.tgz", + "integrity": "sha512-dsLRTq8/4UtVSpJgl9aeqHvbh6pzdmjYD3C092SYgLD2TyoCqHpTJk6vp8DvCTGGc7iowZ2MoiYiVUUCcu7muw==", + "requires": { + "@types/node": "*", + "@types/pino-pretty": "*", + "@types/pino-std-serializers": "*", + "sonic-boom": "^2.1.0" + }, + "dependencies": { + "sonic-boom": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-2.8.0.tgz", + "integrity": "sha512-kuonw1YOYYNOve5iHdSahXPOK49GqwA+LZhI6Wz/l0rP57iKyXXIHaRagOBHAPmGwJC6od2Z9zgvZ5loSgMlVg==", + "requires": { + "atomic-sleep": "^1.0.0" + } + } + } + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -27849,6 +28443,56 @@ "yallist": "^4.0.0" } }, + "pino": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-6.14.0.tgz", + "integrity": "sha512-iuhEDel3Z3hF9Jfe44DPXR8l07bhjuFY3GMHIXbjnY9XcafbyDDwl2sN2vw2GjMPf5Nkoe+OFao7ffn9SXaKDg==", + "requires": { + "fast-redact": "^3.0.0", + "fast-safe-stringify": "^2.0.8", + "flatstr": "^1.0.12", + "pino-std-serializers": "^3.1.0", + "process-warning": "^1.0.0", + "quick-format-unescaped": "^4.0.3", + "sonic-boom": "^1.0.2" + } + }, + "pino-http": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/pino-http/-/pino-http-5.8.0.tgz", + "integrity": "sha512-YwXiyRb9y0WCD1P9PcxuJuh3Dc5qmXde/paJE86UGYRdiFOi828hR9iUGmk5gaw6NBT9gLtKANOHFimvh19U5w==", + "requires": { + "fast-url-parser": "^1.1.3", + "pino": "^6.13.0", + "pino-std-serializers": "^4.0.0" + }, + "dependencies": { + "pino-std-serializers": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-4.0.0.tgz", + "integrity": "sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q==" + } + } + }, + "pino-std-serializers": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-3.2.0.tgz", + "integrity": "sha512-EqX4pwDPrt3MuOAAUBMU0Tk5kR/YcCM5fNPEzgCO2zJ5HfX0vbiH9HbJglnyeQsN96Kznae6MWD47pZB5avTrg==" + }, + "process-warning": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-1.0.0.tgz", + "integrity": "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==" + }, + "sonic-boom": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-1.4.1.tgz", + "integrity": "sha512-LRHh/A8tpW7ru89lrlkU4AszXt1dbwSjVWguGrmlxE7tawVmDBlI1PILMkXAxJTwqhgsEeTHzj36D5CmHgQmNg==", + "requires": { + "atomic-sleep": "^1.0.0", + "flatstr": "^1.0.12" + } + }, "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -27856,10 +28500,15 @@ } } }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==" + }, "process-warning": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-1.0.0.tgz", - "integrity": "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==" + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.3.0.tgz", + "integrity": "sha512-N6mp1+2jpQr3oCFMz6SeHRGbv6Slb20bRhj4v3xR99HqNToAcOe1MFOp4tytyzOfJn+QtN8Rf7U/h2KAn4kC6g==" }, "prompts": { "version": "2.4.2", @@ -27871,25 +28520,6 @@ "sisteransi": "^1.0.5" } }, - "protobufjs": { - "version": "7.2.4", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.4.tgz", - "integrity": "sha512-AT+RJgD2sH8phPmCf7OUZR8xGdcJRga4+1cOaXJ64hvcSkVhNcRHOwIxUatPH15+nj59WAGTDv3LSGZPEQbJaQ==", - "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/node": ">=13.7.0", - "long": "^5.0.0" - } - }, "proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -28043,6 +28673,11 @@ "picomatch": "^2.2.1" } }, + "real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==" + }, "redis-commands": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz", @@ -28072,6 +28707,11 @@ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" + }, "resolve": { "version": "1.22.2", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", @@ -28168,6 +28808,11 @@ "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==" }, + "secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==" + }, "semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -28233,6 +28878,11 @@ } } }, + "seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, "serve-static": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", @@ -28298,21 +28948,6 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, - "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.1.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz", @@ -28452,11 +29087,6 @@ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" }, - "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", @@ -28693,17 +29323,20 @@ "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", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "thread-stream": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.4.1.tgz", + "integrity": "sha512-d/Ex2iWd1whipbT681JmTINKw0ZwOUBZm7+Gjs64DHuX34mmw8vJL2bFAaNacaW72zYiTJxSHi5abUuOi5nsfg==", + "requires": { + "real-require": "^0.2.0" + } + }, "tiny-lru": { "version": "11.0.1", "resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-11.0.1.tgz", @@ -28771,11 +29404,6 @@ } } }, - "triple-beam": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", - "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==" - }, "ts-jest": { "version": "29.1.1", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.1.tgz", @@ -28956,7 +29584,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "requires": { "punycode": "^2.1.0" }, @@ -28964,8 +29591,7 @@ "punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", - "dev": true + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==" } } }, @@ -28985,11 +29611,6 @@ } } }, - "url-polyfill": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/url-polyfill/-/url-polyfill-1.1.12.tgz", - "integrity": "sha512-mYFmBHCapZjtcNHW0MDq9967t+z4Dmg5CJ0KqysK3+ZbyoNOWQHksGCTWwDhxGXllkWlOc10Xfko6v4a3ucM6A==" - }, "util": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", @@ -29120,148 +29741,6 @@ "string-width": "^1.0.2 || 2 || 3 || 4" } }, - "winston": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.10.0.tgz", - "integrity": "sha512-nT6SIDaE9B7ZRO0u3UvdrimG0HkB7dSTAgInQnNR2SOPJ4bvq5q79+pXLftKmP52lJGW15+H5MCK0nM9D3KB/g==", - "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.7", - "resolved": "https://registry.npmjs.org/winston-loki/-/winston-loki-6.0.7.tgz", - "integrity": "sha512-6psEy428ckTXP/ni+DrA4vas2Ldt/43TFm7beoVExKLlTyEwu7zu2c8GBGbnxrdzRQAxXUfom2sK7zr4QP8y5g==", - "requires": { - "async-exit-hook": "2.0.1", - "btoa": "^1.2.1", - "protobufjs": "^7.2.4", - "snappy": "7.1.1", - "url-polyfill": "^1.1.12", - "winston-transport": "^4.3.0" - }, - "dependencies": { - "@napi-rs/snappy-android-arm-eabi": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/snappy-android-arm-eabi/-/snappy-android-arm-eabi-7.1.1.tgz", - "integrity": "sha512-NKd/ztuVEgQaAaNVQ5zZaCB9VV+7+uBXBHqhaE5iSapQhLc41szTlT0s68FCee75OoT3vhqdA6Jp5TrzZ2WOaw==", - "optional": true - }, - "@napi-rs/snappy-android-arm64": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/snappy-android-arm64/-/snappy-android-arm64-7.1.1.tgz", - "integrity": "sha512-DktruMAO0K0toTnxNHg2GWNIAPJqdvIchCsdsRaKyuEnG101qBg0mYiRCAhxHgbT6RJlOGbUPKkbA9KKRhEJUg==", - "optional": true - }, - "@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 - }, - "@napi-rs/snappy-darwin-x64": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/snappy-darwin-x64/-/snappy-darwin-x64-7.1.1.tgz", - "integrity": "sha512-X1D2F67bQkPwr5iSR29/RnOrLwAkB55YO6t41toABzla3mflLDpzZcakz6FokIukykf7ey31/t73v/4pbgaBkg==", - "optional": true - }, - "@napi-rs/snappy-freebsd-x64": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/snappy-freebsd-x64/-/snappy-freebsd-x64-7.1.1.tgz", - "integrity": "sha512-vSeuf+An8jFVHPAn5IbWE9hTGU9PFAaZLj/X7rKTQQtZstnDsHgWe6u4g7FHLuOdwQ8TvhcxAEpNlYIXIk4AJg==", - "optional": true - }, - "@napi-rs/snappy-linux-arm-gnueabihf": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/snappy-linux-arm-gnueabihf/-/snappy-linux-arm-gnueabihf-7.1.1.tgz", - "integrity": "sha512-/yyN6QsnOs3D1+jI3SfRX+gtnD86rbixdfmgxv9g40+FrDaDTLAu/3VuZIqH02qqq/xiWbDnkO+42RGxXDzTCw==", - "optional": true - }, - "@napi-rs/snappy-linux-arm64-gnu": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/snappy-linux-arm64-gnu/-/snappy-linux-arm64-gnu-7.1.1.tgz", - "integrity": "sha512-StEeUCSwUoajgrBtiCQPTkHu+0Q4QlYndghGZNdbN1zJ1ny70YzPpevaFBUyjI/eJ+FN9uICKtwTPtQNSILS5g==", - "optional": true - }, - "@napi-rs/snappy-linux-arm64-musl": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/snappy-linux-arm64-musl/-/snappy-linux-arm64-musl-7.1.1.tgz", - "integrity": "sha512-jWEBRzj+lswZVYf0b5eY0fjMlBL9L9yqjmTuv2UIMjJNHPuR282LK/s3Fm9sYIXQtKkiCo5JyhmIcoghZ3v0Eg==", - "optional": true - }, - "@napi-rs/snappy-linux-x64-gnu": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/snappy-linux-x64-gnu/-/snappy-linux-x64-gnu-7.1.1.tgz", - "integrity": "sha512-41DPoAUFAU9VNrj/96qKfStH2Xq88ZYIsSz8BlITDm2ScoeDGOGbmaWguCXU7I+bC2uKWTmUVMXKqk6tVY6LEg==", - "optional": true - }, - "@napi-rs/snappy-linux-x64-musl": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/snappy-linux-x64-musl/-/snappy-linux-x64-musl-7.1.1.tgz", - "integrity": "sha512-xR4hzFQqVq6J8Zf6XyUVtFJBaRgDyAQYUoBsCr92tZ7gI/0RlWCV6Q6JMO/wP5CSsvyFJIAtSUXXqlzIpw0GPA==", - "optional": true - }, - "@napi-rs/snappy-win32-arm64-msvc": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/snappy-win32-arm64-msvc/-/snappy-win32-arm64-msvc-7.1.1.tgz", - "integrity": "sha512-2mHPadctsaYtrfSV5Na8ooTdI5rflPxP1pceY4us6vbjeWrfgB+KQCuEFOHsGXqFNfsi6L9nWH8nB9swnxnSyw==", - "optional": true - }, - "@napi-rs/snappy-win32-ia32-msvc": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/snappy-win32-ia32-msvc/-/snappy-win32-ia32-msvc-7.1.1.tgz", - "integrity": "sha512-FOMgs9W71hdgjyl3T9F7b/WEIuoryfgBqsyhtHjAaa/98R0BUHl0bOoHg8ka0b5GgnhLBHkX2Yd6VD+Si9Q2ww==", - "optional": true - }, - "@napi-rs/snappy-win32-x64-msvc": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/snappy-win32-x64-msvc/-/snappy-win32-x64-msvc-7.1.1.tgz", - "integrity": "sha512-Mu3yELySvzhBcNTVCq+hYxVh+lH3/KjoQ5HIEb3DDPoX0AGRTm3XZa+usq8pFWjl91Cgp9nWK+9lVSkCCIRaKA==", - "optional": true - }, - "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" - } - } - } - }, - "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" - } - }, "wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", @@ -29358,6 +29837,11 @@ "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.27.tgz", "integrity": "sha512-fg03WRxtkCV6ohClePNAECYsmpKKTv5L8y/X3Dn1hQrec3POx2jHZ/0P2qQ6HvsrU1BmeqXcof3NGGueG6LxwQ==" }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + }, "y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/backend/package.json b/backend/package.json index 82a640b95..9768762e8 100644 --- a/backend/package.json +++ b/backend/package.json @@ -6,11 +6,12 @@ "@godaddy/terminus": "^4.12.0", "@node-saml/passport-saml": "^4.0.4", "@octokit/rest": "^19.0.5", - "@sentry/node": "^7.49.0", + "@sentry/node": "^7.77.0", "@sentry/tracing": "^7.48.0", "@types/crypto-js": "^4.1.1", "@types/libsodium-wrappers": "^0.7.10", "@ucast/mongo2js": "^1.3.4", + "ajv": "^8.12.0", "argon2": "^0.30.3", "aws-sdk": "^2.1364.0", "axios": "^1.3.5", @@ -29,12 +30,14 @@ "helmet": "^5.1.1", "infisical-node": "^1.2.1", "ioredis": "^5.3.2", + "jmespath": "^0.16.0", "js-yaml": "^4.1.0", "jsonwebtoken": "^9.0.0", "jsrp": "^0.2.4", "libsodium-wrappers": "^0.7.10", "lodash": "^4.17.21", "mongoose": "^7.4.1", + "mysql2": "^3.6.2", "nanoid": "^3.3.6", "node-cache": "^5.1.2", "nodemailer": "^6.8.0", @@ -42,6 +45,9 @@ "passport-github": "^1.1.0", "passport-gitlab2": "^5.0.0", "passport-google-oauth20": "^2.0.0", + "pino": "^8.16.1", + "pino-http": "^8.5.1", + "pg": "^8.11.3", "posthog-node": "^2.6.0", "probot": "^12.3.1", "query-string": "^7.1.3", @@ -52,8 +58,6 @@ "tweetnacl-util": "^0.15.1", "typescript": "^4.9.3", "utility-types": "^3.10.0", - "winston": "^3.8.2", - "winston-loki": "^6.0.6", "zod": "^3.22.3" }, "overrides": { @@ -66,7 +70,7 @@ "main": "src/index.js", "scripts": { "start": "node build/index.js", - "dev": "nodemon", + "dev": "nodemon index.js | pino-pretty --colorize", "swagger-autogen": "node ./swagger/index.ts", "build": "rimraf ./build && tsc && cp -R ./src/templates ./build && cp -R ./src/data ./build", "lint": "eslint . --ext .ts", @@ -98,12 +102,15 @@ "@types/cors": "^2.8.12", "@types/express": "^4.17.14", "@types/jest": "^29.5.0", + "@types/jmespath": "^0.15.1", "@types/jsonwebtoken": "^8.5.9", "@types/lodash": "^4.14.191", "@types/node": "^18.11.3", "@types/nodemailer": "^6.4.6", "@types/passport": "^1.0.12", + "@types/pg": "^8.10.7", "@types/picomatch": "^2.3.0", + "@types/pino": "^7.0.5", "@types/supertest": "^2.0.12", "@types/swagger-jsdoc": "^6.0.1", "@types/swagger-ui-express": "^4.1.3", @@ -117,6 +124,7 @@ "jest-junit": "^15.0.0", "nodemon": "^2.0.19", "npm": "^8.19.3", + "pino-pretty": "^10.2.3", "smee-client": "^1.2.3", "supertest": "^6.3.3", "swagger-autogen": "^2.23.5", diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 402fd5874..c5cb52663 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -24,7 +24,6 @@ export const getJwtRefreshLifetime = async () => (await client.getSecret("JWT_RE export const getJwtServiceSecret = async () => (await client.getSecret("JWT_SERVICE_SECRET")).secretValue; // TODO: deprecate (related to ST V1) export const getJwtSignupLifetime = async () => (await client.getSecret("JWT_SIGNUP_LIFETIME")).secretValue || "15m"; export const getJwtProviderAuthLifetime = async () => (await client.getSecret("JWT_PROVIDER_AUTH_LIFETIME")).secretValue || "15m"; -export const getJwtServiceTokenSecret = async () => (await client.getSecret("JWT_SERVICE_TOKEN_SECRET")).secretValue; export const getMongoURL = async () => (await client.getSecret("MONGO_URL")).secretValue; export const getNodeEnv = async () => (await client.getSecret("NODE_ENV")).secretValue || "production"; export const getVerboseErrorOutput = async () => (await client.getSecret("VERBOSE_ERROR_OUTPUT")).secretValue === "true" && true; diff --git a/backend/src/controllers/v1/authController.ts b/backend/src/controllers/v1/authController.ts index e426eef5a..374c4652c 100644 --- a/backend/src/controllers/v1/authController.ts +++ b/backend/src/controllers/v1/authController.ts @@ -24,10 +24,18 @@ import { validateRequest } from "../../helpers/validation"; import * as reqValidator from "../../validation/auth"; declare module "jsonwebtoken" { + export interface AuthnJwtPayload extends jwt.JwtPayload { + authTokenType: AuthTokenType; + } export interface UserIDJwtPayload extends jwt.JwtPayload { userId: string; refreshVersion?: number; } + export interface ServiceRefreshTokenJwtPayload extends jwt.JwtPayload { + serviceTokenDataId: string; + authTokenType: string; + tokenVersion: number; + } } /** diff --git a/backend/src/controllers/v1/integrationAuthController.ts b/backend/src/controllers/v1/integrationAuthController.ts index 70410ce35..ec166df48 100644 --- a/backend/src/controllers/v1/integrationAuthController.ts +++ b/backend/src/controllers/v1/integrationAuthController.ts @@ -10,6 +10,7 @@ import { ALGORITHM_AES_256_GCM, ENCODING_SCHEME_UTF8, INTEGRATION_BITBUCKET_API_URL, + INTEGRATION_CHECKLY_API_URL, INTEGRATION_GCP_SECRET_MANAGER, INTEGRATION_NORTHFLANK_API_URL, INTEGRATION_QOVERY_API_URL, @@ -344,6 +345,59 @@ export const getIntegrationAuthVercelBranches = async (req: Request, res: Respon }); }; +/** + * Return list of Checkly groups for a specific user + * @param req + * @param res + */ +export const getIntegrationAuthChecklyGroups = async (req: Request, res: Response) => { + const { + params: { integrationAuthId }, + query: { accountId } + } = await validateRequest(reqValidator.GetIntegrationAuthChecklyGroupsV1, req); + + const { integrationAuth, accessToken } = await getIntegrationAuthAccessHelper({ + integrationAuthId: new Types.ObjectId(integrationAuthId) + }); + + const { permission } = await getUserProjectPermissions( + req.user._id, + integrationAuth.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.Integrations + ); + + interface ChecklyGroup { + id: number; + name: string; + } + + if (accountId && accountId !== "") { + const { data }: { data: ChecklyGroup[] } = ( + await standardRequest.get(`${INTEGRATION_CHECKLY_API_URL}/v1/check-groups`, { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json", + "X-Checkly-Account": accountId + } + }) + ); + + return res.status(200).send({ + groups: data.map((g: ChecklyGroup) => ({ + name: g.name, + groupId: g.id, + })) + }); + } + + return res.status(200).send({ + groups: [] + }); +} + /** * Return list of Qovery Orgs for a specific user * @param req diff --git a/backend/src/controllers/v3/secretsController.ts b/backend/src/controllers/v3/secretsController.ts index 2e0c8514a..dedb15b65 100644 --- a/backend/src/controllers/v3/secretsController.ts +++ b/backend/src/controllers/v3/secretsController.ts @@ -140,7 +140,7 @@ export const getSecretsRaw = async (req: Request, res: Response) => { query: { secretPath, environment, workspaceId } } = validatedData; const { - query: { folderId, include_imports: includeImports } + query: { include_imports: includeImports } } = validatedData; // if the service token has single scope, it will get all secrets for that scope by default @@ -156,13 +156,6 @@ export const getSecretsRaw = async (req: Request, res: Response) => { workspaceId = serviceTokenDetails.workspace.toString(); } - if (folderId && folderId !== "root") { - const folder = await Folder.findOne({ workspace: workspaceId, environment }); - if (!folder) throw BadRequestError({ message: "Folder not found" }); - - secretPath = getFolderWithPathFromId(folder.nodes, folderId).folderPath; - } - if (!environment || !workspaceId) throw BadRequestError({ message: "Missing environment or workspace id" }); @@ -177,7 +170,6 @@ export const getSecretsRaw = async (req: Request, res: Response) => { const secrets = await SecretService.getSecrets({ workspaceId: new Types.ObjectId(workspaceId), environment, - folderId, secretPath, authData: req.authData }); @@ -467,20 +459,13 @@ export const deleteSecretByNameRaw = async (req: Request, res: Response) => { export const getSecrets = async (req: Request, res: Response) => { const validatedData = await validateRequest(reqValidator.GetSecretsV3, req); const { - query: { environment, workspaceId, include_imports: includeImports, folderId } + query: { environment, workspaceId, include_imports: includeImports } } = validatedData; let { query: { secretPath } } = validatedData; - if (folderId && folderId !== "root") { - const folder = await Folder.findOne({ workspace: workspaceId, environment }); - if (!folder) return res.send({ secrets: [] }); - - secretPath = getFolderWithPathFromId(folder.nodes, folderId).folderPath; - } - const { authVerifier: permissionCheckFn } = await checkSecretsPermission({ authData: req.authData, workspaceId, @@ -492,7 +477,6 @@ export const getSecrets = async (req: Request, res: Response) => { const secrets = await SecretService.getSecrets({ workspaceId: new Types.ObjectId(workspaceId), environment, - folderId, secretPath, authData: req.authData }); @@ -875,6 +859,14 @@ export const createSecretByNameBatch = async (req: Request, res: Response) => { authData: req.authData }); + await EventService.handleEvent({ + event: eventPushSecrets({ + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath + }) + }); + return res.status(200).send({ secrets: createdSecrets }); @@ -919,6 +911,14 @@ export const updateSecretByNameBatch = async (req: Request, res: Response) => { authData: req.authData }); + await EventService.handleEvent({ + event: eventPushSecrets({ + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath + }) + }); + return res.status(200).send({ secrets: updatedSecrets }); diff --git a/backend/src/ee/controllers/v1/index.ts b/backend/src/ee/controllers/v1/index.ts index b023599e7..dc161c271 100644 --- a/backend/src/ee/controllers/v1/index.ts +++ b/backend/src/ee/controllers/v1/index.ts @@ -10,6 +10,8 @@ import * as cloudProductsController from "./cloudProductsController"; import * as roleController from "./roleController"; import * as secretApprovalPolicyController from "./secretApprovalPolicyController"; import * as secretApprovalRequestController from "./secretApprovalRequestsController"; +import * as secretRotationProviderController from "./secretRotationProviderController"; +import * as secretRotationController from "./secretRotationController"; export { secretController, @@ -23,5 +25,7 @@ export { cloudProductsController, roleController, secretApprovalPolicyController, - secretApprovalRequestController + secretApprovalRequestController, + secretRotationProviderController, + secretRotationController }; diff --git a/backend/src/ee/controllers/v1/roleController.ts b/backend/src/ee/controllers/v1/roleController.ts index d80ef2817..edc31273a 100644 --- a/backend/src/ee/controllers/v1/roleController.ts +++ b/backend/src/ee/controllers/v1/roleController.ts @@ -212,12 +212,13 @@ export const getUserPermissions = async (req: Request, res: Response) => { const { params: { orgId } } = await validateRequest(GetUserPermission, req); - - const { permission } = await getUserOrgPermissions(req.user._id, orgId); + + const { permission, membership } = await getUserOrgPermissions(req.user._id, orgId); res.status(200).json({ data: { - permissions: packRules(permission.rules) + permissions: packRules(permission.rules), + membership } }); }; @@ -226,11 +227,12 @@ export const getUserWorkspacePermissions = async (req: Request, res: Response) = const { params: { workspaceId } } = await validateRequest(GetUserProjectPermission, req); - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const { permission, membership } = await getUserProjectPermissions(req.user._id, workspaceId); res.status(200).json({ data: { - permissions: packRules(permission.rules) + permissions: packRules(permission.rules), + membership } }); }; diff --git a/backend/src/ee/controllers/v1/secretRotationController.ts b/backend/src/ee/controllers/v1/secretRotationController.ts new file mode 100644 index 000000000..a8a2fd2dd --- /dev/null +++ b/backend/src/ee/controllers/v1/secretRotationController.ts @@ -0,0 +1,91 @@ +import { Request, Response } from "express"; +import { validateRequest } from "../../../helpers/validation"; +import * as reqValidator from "../../validation/secretRotation"; +import * as secretRotationService from "../../secretRotation/service"; +import { + getUserProjectPermissions, + ProjectPermissionActions, + ProjectPermissionSub +} from "../../services/ProjectRoleService"; +import { ForbiddenError } from "@casl/ability"; + +export const createSecretRotation = async (req: Request, res: Response) => { + const { + body: { + provider, + customProvider, + interval, + outputs, + secretPath, + environment, + workspaceId, + inputs + } + } = await validateRequest(reqValidator.createSecretRotationV1, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + ProjectPermissionSub.SecretRotation + ); + + const secretRotation = await secretRotationService.createSecretRotation({ + workspaceId, + inputs, + environment, + secretPath, + outputs, + interval, + customProvider, + provider + }); + + return res.send({ secretRotation }); +}; + +export const restartSecretRotations = async (req: Request, res: Response) => { + const { + body: { id } + } = await validateRequest(reqValidator.restartSecretRotationV1, req); + + const doc = await secretRotationService.getSecretRotationById({ id }); + const { permission } = await getUserProjectPermissions(req.user._id, doc.workspace.toString()); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + ProjectPermissionSub.SecretRotation + ); + + const secretRotation = await secretRotationService.restartSecretRotation({ id }); + return res.send({ secretRotation }); +}; + +export const deleteSecretRotations = async (req: Request, res: Response) => { + const { + params: { id } + } = await validateRequest(reqValidator.removeSecretRotationV1, req); + + const doc = await secretRotationService.getSecretRotationById({ id }); + const { permission } = await getUserProjectPermissions(req.user._id, doc.workspace.toString()); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + ProjectPermissionSub.SecretRotation + ); + + const secretRotations = await secretRotationService.deleteSecretRotation({ id }); + return res.send({ secretRotations }); +}; + +export const getSecretRotations = async (req: Request, res: Response) => { + const { + query: { workspaceId } + } = await validateRequest(reqValidator.getSecretRotationV1, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.SecretRotation + ); + + const secretRotations = await secretRotationService.getSecretRotationOfWorkspace(workspaceId); + return res.send({ secretRotations }); +}; diff --git a/backend/src/ee/controllers/v1/secretRotationProviderController.ts b/backend/src/ee/controllers/v1/secretRotationProviderController.ts new file mode 100644 index 000000000..7d62ed5f5 --- /dev/null +++ b/backend/src/ee/controllers/v1/secretRotationProviderController.ts @@ -0,0 +1,28 @@ +import { Request, Response } from "express"; +import { validateRequest } from "../../../helpers/validation"; +import * as reqValidator from "../../validation/secretRotationProvider"; +import * as secretRotationProviderService from "../../secretRotation/service"; +import { + getUserProjectPermissions, + ProjectPermissionActions, + ProjectPermissionSub +} from "../../services/ProjectRoleService"; +import { ForbiddenError } from "@casl/ability"; + +export const getProviderTemplates = async (req: Request, res: Response) => { + const { + params: { workspaceId } + } = await validateRequest(reqValidator.getSecretRotationProvidersV1, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.SecretRotation + ); + + const rotationProviderList = await secretRotationProviderService.getProviderTemplate({ + workspaceId + }); + + return res.send(rotationProviderList); +}; diff --git a/backend/src/ee/controllers/v3/serviceTokenDataController.ts b/backend/src/ee/controllers/v3/serviceTokenDataController.ts index d233cb0d9..4590f3ab8 100644 --- a/backend/src/ee/controllers/v3/serviceTokenDataController.ts +++ b/backend/src/ee/controllers/v3/serviceTokenDataController.ts @@ -1,3 +1,4 @@ +import jwt from "jsonwebtoken"; import { Request, Response } from "express"; import { Types } from "mongoose"; import { @@ -24,10 +25,11 @@ import { getUserProjectPermissions } from "../../services/ProjectRoleService"; import { ForbiddenError } from "@casl/ability"; -import { BadRequestError, ResourceNotFoundError } from "../../../utils/errors"; +import { BadRequestError, ResourceNotFoundError, UnauthorizedRequestError } from "../../../utils/errors"; import { extractIPDetails, isValidIpOrCidr } from "../../../utils/ip"; import { EEAuditLogService, EELicenseService } from "../../services"; -import { getJwtServiceTokenSecret } from "../../../config"; +import { getAuthSecret } from "../../../config"; +import { AuthTokenType } from "../../../variables"; /** * Return project key for service token V3 @@ -56,6 +58,99 @@ export const getServiceTokenDataKey = async (req: Request, res: Response) => { }); } +/** + * Return access and refresh token as per refresh operation + * @param req + * @param res + */ + export const refreshToken = async (req: Request, res: Response) => { + const { + body: { + refresh_token + } + } = await validateRequest(reqValidator.RefreshTokenV3, req); + + const decodedToken = ( + jwt.verify(refresh_token, await getAuthSecret()) + ); + + if (decodedToken.authTokenType !== AuthTokenType.SERVICE_REFRESH_TOKEN) throw UnauthorizedRequestError(); + + let serviceTokenData = await ServiceTokenDataV3.findOne({ + _id: new Types.ObjectId(decodedToken.serviceTokenDataId), + isActive: true + }); + + if (!serviceTokenData) throw UnauthorizedRequestError(); + + if (decodedToken.tokenVersion !== serviceTokenData.tokenVersion) { + // raise alarm + throw UnauthorizedRequestError(); + } + + const response: { + refresh_token?: string; + access_token: string; + expires_in: number; + token_type: string; + } = { + refresh_token, + access_token: "", + expires_in: 0, + token_type: "Bearer" + }; + + if (serviceTokenData.isRefreshTokenRotationEnabled) { + serviceTokenData = await ServiceTokenDataV3.findByIdAndUpdate( + serviceTokenData._id, + { + $inc: { + tokenVersion: 1 + } + }, + { + new: true + } + ); + + if (!serviceTokenData) throw BadRequestError(); + + response.refresh_token = createToken({ + payload: { + serviceTokenDataId: serviceTokenData._id.toString(), + authTokenType: AuthTokenType.SERVICE_REFRESH_TOKEN, + tokenVersion: serviceTokenData.tokenVersion + }, + secret: await getAuthSecret() + }); + } + + response.access_token = createToken({ + payload: { + serviceTokenDataId: serviceTokenData._id.toString(), + authTokenType: AuthTokenType.SERVICE_ACCESS_TOKEN, + tokenVersion: serviceTokenData.tokenVersion + }, + expiresIn: serviceTokenData.accessTokenTTL, + secret: await getAuthSecret() + }); + + response.expires_in = serviceTokenData.accessTokenTTL; + + await ServiceTokenDataV3.findByIdAndUpdate( + serviceTokenData._id, + { + refreshTokenLastUsed: new Date(), + $inc: { refreshTokenUsageCount: 1 } + }, + { + new: true + } + ); + + return res.status(200).send(response); +} + /** * Create service token data V3 * @param req @@ -71,8 +166,10 @@ export const createServiceTokenData = async (req: Request, res: Response) => { scopes, trustedIps, expiresIn, + accessTokenTTL, + isRefreshTokenRotationEnabled, encryptedKey, // for ServiceTokenDataV3Key - nonce // for ServiceTokenDataV3Key + nonce, // for ServiceTokenDataV3Key } } = await validateRequest(reqValidator.CreateServiceTokenV3, req); const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); @@ -118,11 +215,15 @@ export const createServiceTokenData = async (req: Request, res: Response) => { user, workspace: new Types.ObjectId(workspaceId), publicKey, - usageCount: 0, + refreshTokenUsageCount: 0, + accessTokenUsageCount: 0, + tokenVersion: 1, trustedIps: reformattedTrustedIps, scopes, isActive, - expiresAt + expiresAt, + accessTokenTTL, + isRefreshTokenRotationEnabled }).save(); await new ServiceTokenDataV3Key({ @@ -133,18 +234,19 @@ export const createServiceTokenData = async (req: Request, res: Response) => { workspace: new Types.ObjectId(workspaceId) }).save(); - const token = createToken({ + const refreshToken = createToken({ payload: { - _id: serviceTokenData._id.toString() + serviceTokenDataId: serviceTokenData._id.toString(), + authTokenType: AuthTokenType.SERVICE_REFRESH_TOKEN, + tokenVersion: serviceTokenData.tokenVersion }, - expiresIn, - secret: await getJwtServiceTokenSecret() + secret: await getAuthSecret() }); await EEAuditLogService.createAuditLog( req.authData, { - type: EventType.CREATE_SERVICE_TOKEN_V3, + type: EventType.CREATE_SERVICE_TOKEN_V3, // TODO: update metadata: { name, isActive, @@ -160,7 +262,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => { return res.status(200).send({ serviceTokenData, - serviceToken: `stv3.${token}` + refreshToken }); } @@ -178,7 +280,9 @@ export const updateServiceTokenData = async (req: Request, res: Response) => { isActive, scopes, trustedIps, - expiresIn + expiresIn, + accessTokenTTL, + isRefreshTokenRotationEnabled } } = await validateRequest(reqValidator.UpdateServiceTokenV3, req); @@ -233,13 +337,15 @@ export const updateServiceTokenData = async (req: Request, res: Response) => { isActive, scopes, trustedIps: reformattedTrustedIps, - expiresAt + expiresAt, + accessTokenTTL, + isRefreshTokenRotationEnabled }, { new: true } ); - + if (!serviceTokenData) throw BadRequestError({ message: "Failed to update service token" }); diff --git a/backend/src/ee/models/auditLog/enums.ts b/backend/src/ee/models/auditLog/enums.ts index a7be37a65..45ba56345 100644 --- a/backend/src/ee/models/auditLog/enums.ts +++ b/backend/src/ee/models/auditLog/enums.ts @@ -1,7 +1,8 @@ export enum ActorType { - USER = "user", - SERVICE = "service", - SERVICE_V3 = "service-v3" + USER = "user", + SERVICE = "service", + SERVICE_V3 = "service-v3", + Machine = "machine" } export enum UserAgentType { diff --git a/backend/src/ee/models/auditLog/types.ts b/backend/src/ee/models/auditLog/types.ts index 579a02ccb..24796c70a 100644 --- a/backend/src/ee/models/auditLog/types.ts +++ b/backend/src/ee/models/auditLog/types.ts @@ -1,11 +1,5 @@ -import { - ActorType, - EventType -} from "./enums"; -import { - IServiceTokenV3Scope, - IServiceTokenV3TrustedIp -} from "../../../models/serviceTokenDataV3"; +import { ActorType, EventType } from "./enums"; +import { IServiceTokenV3Scope, IServiceTokenV3TrustedIp } from "../../../models/serviceTokenDataV3"; interface UserActorMetadata { userId: string; @@ -28,14 +22,15 @@ export interface ServiceActor { } export interface ServiceActorV3 { - type: ActorType.SERVICE_V3; - metadata: ServiceActorMetadata; + type: ActorType.SERVICE_V3; + metadata: ServiceActorMetadata; } -export type Actor = - | UserActor - | ServiceActor - | ServiceActorV3; +export interface MachineActor { + type: ActorType.Machine; +} + +export type Actor = UserActor | ServiceActor | ServiceActorV3 | MachineActor; interface GetSecretsEvent { type: EventType.GET_SECRETS; @@ -226,36 +221,36 @@ interface DeleteServiceTokenEvent { } interface CreateServiceTokenV3Event { - type: EventType.CREATE_SERVICE_TOKEN_V3; - metadata: { - name: string; - isActive: boolean; - scopes: Array; - trustedIps: Array; - expiresAt?: Date; - } + type: EventType.CREATE_SERVICE_TOKEN_V3; + metadata: { + name: string; + isActive: boolean; + scopes: Array; + trustedIps: Array; + expiresAt?: Date; + }; } interface UpdateServiceTokenV3Event { - type: EventType.UPDATE_SERVICE_TOKEN_V3; - metadata: { - name?: string; - isActive?: boolean; - scopes?: Array; - trustedIps?: Array; - expiresAt?: Date; - } + type: EventType.UPDATE_SERVICE_TOKEN_V3; + metadata: { + name?: string; + isActive?: boolean; + scopes?: Array; + trustedIps?: Array; + expiresAt?: Date; + }; } interface DeleteServiceTokenV3Event { - type: EventType.DELETE_SERVICE_TOKEN_V3; - metadata: { - name: string; - isActive: boolean; - scopes: Array; - expiresAt?: Date; - trustedIps: Array; - } + type: EventType.DELETE_SERVICE_TOKEN_V3; + metadata: { + name: string; + isActive: boolean; + scopes: Array; + expiresAt?: Date; + trustedIps: Array; + }; } interface CreateEnvironmentEvent { @@ -427,15 +422,15 @@ interface UpdateUserRole { } interface UpdateUserDeniedPermissions { - type: EventType.UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS, - metadata: { - userId: string; - email: string; - deniedPermissions: { - environmentSlug: string; - ability: string; - }[] - } + type: EventType.UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS; + metadata: { + userId: string; + email: string; + deniedPermissions: { + environmentSlug: string; + ability: string; + }[]; + }; } interface SecretApprovalMerge { type: EventType.SECRET_APPROVAL_MERGED; diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index 34477cf72..40145e81f 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -10,6 +10,8 @@ import secretScanning from "./secretScanning"; import roles from "./role"; import secretApprovalPolicy from "./secretApprovalPolicy"; import secretApprovalRequest from "./secretApprovalRequest"; +import secretRotationProvider from "./secretRotationProvider"; +import secretRotation from "./secretRotation"; export { secret, @@ -23,5 +25,7 @@ export { secretScanning, roles, secretApprovalPolicy, - secretApprovalRequest + secretApprovalRequest, + secretRotationProvider, + secretRotation }; diff --git a/backend/src/ee/routes/v1/secretRotation.ts b/backend/src/ee/routes/v1/secretRotation.ts new file mode 100644 index 000000000..a4da8a72f --- /dev/null +++ b/backend/src/ee/routes/v1/secretRotation.ts @@ -0,0 +1,41 @@ +import express from "express"; + +import { AuthMode } from "../../../variables"; +import { requireAuth } from "../../../middleware"; +import { secretRotationController } from "../../controllers/v1"; + +const router = express.Router(); + +router.post( + "/", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + secretRotationController.createSecretRotation +); + +router.post( + "/restart", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + secretRotationController.restartSecretRotations +); + +router.get( + "/", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + secretRotationController.getSecretRotations +); + +router.delete( + "/:id", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + secretRotationController.deleteSecretRotations +); + +export default router; diff --git a/backend/src/ee/routes/v1/secretRotationProvider.ts b/backend/src/ee/routes/v1/secretRotationProvider.ts new file mode 100644 index 000000000..16ab17184 --- /dev/null +++ b/backend/src/ee/routes/v1/secretRotationProvider.ts @@ -0,0 +1,17 @@ +import express from "express"; + +import { AuthMode } from "../../../variables"; +import { requireAuth } from "../../../middleware"; +import { secretRotationProviderController } from "../../controllers/v1"; + +const router = express.Router(); + +router.get( + "/:workspaceId", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + secretRotationProviderController.getProviderTemplates +); + +export default router; diff --git a/backend/src/ee/routes/v3/serviceTokenData.ts b/backend/src/ee/routes/v3/serviceTokenData.ts index 2f421c7ec..ba1d04854 100644 --- a/backend/src/ee/routes/v3/serviceTokenData.ts +++ b/backend/src/ee/routes/v3/serviceTokenData.ts @@ -7,11 +7,16 @@ import { serviceTokenDataController } from "../../controllers/v3"; router.get( "/me/key", requireAuth({ - acceptedAuthModes: [AuthMode.SERVICE_TOKEN_V3] + acceptedAuthModes: [AuthMode.SERVICE_ACCESS_TOKEN] }), serviceTokenDataController.getServiceTokenDataKey ); +router.post( + "/me/token", + serviceTokenDataController.refreshToken +); + router.post( "/", requireAuth({ diff --git a/backend/src/ee/secretRotation/db.ts b/backend/src/ee/secretRotation/db.ts new file mode 100644 index 000000000..e69de29bb diff --git a/backend/src/ee/secretRotation/models.ts b/backend/src/ee/secretRotation/models.ts new file mode 100644 index 000000000..0ddde5d83 --- /dev/null +++ b/backend/src/ee/secretRotation/models.ts @@ -0,0 +1,91 @@ +import { Schema, model } from "mongoose"; +import { + ALGORITHM_AES_256_GCM, + ENCODING_SCHEME_BASE64, + ENCODING_SCHEME_UTF8 +} from "../../variables"; +import { ISecretRotation } from "./types"; + +const secretRotationSchema = new Schema( + { + workspace: { + type: Schema.Types.ObjectId, + ref: "Workspace" + }, + provider: { + type: String, + required: true + }, + customProvider: { + type: Schema.Types.ObjectId, + ref: "SecretRotationProvider" + }, + environment: { + type: String, + required: true + }, + secretPath: { + type: String, + required: true + }, + interval: { + type: Number, + required: true + }, + lastRotatedAt: { + type: String + }, + status: { + type: String, + enum: ["success", "failed"] + }, + statusMessage: { + type: String + }, + // encrypted data on input keys and secrets got + encryptedData: { + type: String, + select: false + }, + encryptedDataIV: { + type: String, + select: false + }, + encryptedDataTag: { + type: String, + select: false + }, + algorithm: { + // the encryption algorithm used + type: String, + enum: [ALGORITHM_AES_256_GCM], + required: true, + select: false, + default: ALGORITHM_AES_256_GCM + }, + keyEncoding: { + type: String, + enum: [ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64], + required: true, + select: false, + default: ENCODING_SCHEME_UTF8 + }, + outputs: [ + { + key: { + type: String, + required: true + }, + secret: { + type: Schema.Types.ObjectId, + ref: "Secret" + } + } + ] + }, + { + timestamps: true + } +); + +export const SecretRotation = model("SecretRotation", secretRotationSchema); diff --git a/backend/src/ee/secretRotation/queue/queue.ts b/backend/src/ee/secretRotation/queue/queue.ts new file mode 100644 index 000000000..0127bbd9c --- /dev/null +++ b/backend/src/ee/secretRotation/queue/queue.ts @@ -0,0 +1,288 @@ +import Queue, { Job } from "bull"; +import { client, getEncryptionKey, getRootEncryptionKey } from "../../../config"; +import { BotService, EventService, TelemetryService } from "../../../services"; +import { SecretRotation } from "../models"; +import { rotationTemplates } from "../templates"; +import { + ISecretRotationData, + ISecretRotationEncData, + ISecretRotationProviderTemplate, + TProviderFunctionTypes +} from "../types"; +import { + decryptSymmetric128BitHexKeyUTF8, + encryptSymmetric128BitHexKeyUTF8 +} from "../../../utils/crypto"; +import { ISecret, Secret } from "../../../models"; +import { ENCODING_SCHEME_BASE64, ENCODING_SCHEME_UTF8, SECRET_SHARED } from "../../../variables"; +import { EESecretService } from "../../services"; +import { SecretVersion } from "../../models"; +import { eventPushSecrets } from "../../../events"; +import { logger } from "../../../utils/logging"; + +import { + secretRotationPreSetFn, + secretRotationRemoveFn, + secretRotationSetFn, + secretRotationTestFn +} from "./queue.utils"; + +const secretRotationQueue = new Queue("secret-rotation-service", process.env.REDIS_URL as string); + +secretRotationQueue.process(async (job: Job) => { + logger.info(`secretRotationQueue.process: [rotationDocument=${job.data.rotationDocId}]`); + const rotationStratDocId = job.data.rotationDocId; + const secretRotation = await SecretRotation.findById(rotationStratDocId) + .select("+encryptedData +encryptedDataTag +encryptedDataIV +keyEncoding") + .populate<{ + outputs: [ + { + key: string; + secret: ISecret; + } + ]; + }>("outputs.secret"); + + const infisicalRotationProvider = rotationTemplates.find( + ({ name }) => name === secretRotation?.provider + ); + + try { + if (!infisicalRotationProvider || !secretRotation) + throw new Error("Failed to find rotation strategy"); + + if (secretRotation.outputs.some(({ secret }) => !secret)) + throw new Error("Secrets not found in dashboard"); + + const workspaceId = secretRotation.workspace; + + // deep copy + const provider = JSON.parse( + JSON.stringify(infisicalRotationProvider) + ) as ISecretRotationProviderTemplate; + + // decrypt user provided inputs for secret rotation + const encryptionKey = await getEncryptionKey(); + const rootEncryptionKey = await getRootEncryptionKey(); + let decryptedData = ""; + if (rootEncryptionKey && secretRotation.keyEncoding === ENCODING_SCHEME_BASE64) { + // case: encoding scheme is base64 + decryptedData = client.decryptSymmetric( + secretRotation.encryptedData, + rootEncryptionKey, + secretRotation.encryptedDataIV, + secretRotation.encryptedDataTag + ); + } else if (encryptionKey && secretRotation.keyEncoding === ENCODING_SCHEME_UTF8) { + // case: encoding scheme is utf8 + decryptedData = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secretRotation.encryptedData, + iv: secretRotation.encryptedDataIV, + tag: secretRotation.encryptedDataTag, + key: encryptionKey + }); + } + + const variables = JSON.parse(decryptedData) as ISecretRotationEncData; + + // rotation set cycle + const newCredential: ISecretRotationData = { + inputs: variables.inputs, + outputs: {}, + internal: {} + }; + // special glue code for database + if (provider.template.functions.set.type === TProviderFunctionTypes.DB) { + const lastCred = variables.creds.at(-1); + if (lastCred && variables.creds.length === 1) { + newCredential.internal.username = + lastCred.internal.username === variables.inputs.username1 + ? variables.inputs.username2 + : variables.inputs.username1; + } else { + newCredential.internal.username = lastCred + ? lastCred.internal.username + : variables.inputs.username1; + } + } + if (provider.template.functions.set?.pre) { + secretRotationPreSetFn(provider.template.functions.set.pre, newCredential); + } + await secretRotationSetFn(provider.template.functions.set, newCredential); + await secretRotationTestFn(provider.template.functions.test, newCredential); + + if (variables.creds.length === 2) { + const deleteCycleCred = variables.creds.pop(); + if (deleteCycleCred && provider.template.functions.remove) { + const deleteCycleVar = { inputs: variables.inputs, ...deleteCycleCred }; + await secretRotationRemoveFn(provider.template.functions.remove, deleteCycleVar); + } + } + variables.creds.unshift({ outputs: newCredential.outputs, internal: newCredential.internal }); + const { ciphertext, iv, tag } = client.encryptSymmetric( + JSON.stringify(variables), + rootEncryptionKey + ); + + // save the rotation state + await SecretRotation.findByIdAndUpdate(rotationStratDocId, { + encryptedData: ciphertext, + encryptedDataIV: iv, + encryptedDataTag: tag, + status: "success", + statusMessage: "Rotated successfully", + lastRotatedAt: new Date().toUTCString() + }); + + const key = await BotService.getWorkspaceKeyWithBot({ + workspaceId: secretRotation.workspace + }); + + const encryptedSecrets = secretRotation.outputs.map(({ key: outputKey, secret }) => ({ + secret, + value: encryptSymmetric128BitHexKeyUTF8({ + plaintext: + typeof newCredential.outputs[outputKey] === "object" + ? JSON.stringify(newCredential.outputs[outputKey]) + : String(newCredential.outputs[outputKey]), + key + }) + })); + + // now save the secret do a bulk update + // can't use the updateSecret function due to various parameter required issue + // REFACTOR(akhilmhdh): secret module should be lot more flexible. Ability to update bulk or individually by blindIndex, by id etc + await Secret.bulkWrite( + encryptedSecrets.map(({ secret, value }) => ({ + updateOne: { + filter: { + workspace: workspaceId, + environment: secretRotation.environment, + _id: secret._id, + type: SECRET_SHARED + }, + update: { + $inc: { + version: 1 + }, + secretValueCiphertext: value.ciphertext, + secretValueIV: value.iv, + secretValueTag: value.tag + } + } + })) + ); + + await EESecretService.addSecretVersions({ + secretVersions: encryptedSecrets.map(({ secret, value }) => { + const { + _id, + version, + workspace, + type, + folder, + secretBlindIndex, + secretKeyIV, + secretKeyTag, + secretKeyCiphertext, + skipMultilineEncoding, + environment, + algorithm, + keyEncoding + } = secret; + + return new SecretVersion({ + secret: _id, + version: version + 1, + workspace: workspace, + type, + folder, + environment, + isDeleted: false, + secretBlindIndex: secretBlindIndex, + secretKeyCiphertext: secretKeyCiphertext, + secretKeyIV: secretKeyIV, + secretKeyTag: secretKeyTag, + secretValueCiphertext: value.ciphertext, + secretValueIV: value.iv, + secretValueTag: value.tag, + algorithm, + keyEncoding, + skipMultilineEncoding + }); + }) + }); + + // akhilmhdh: @tony need to do something about this as its depend on authData which is not possibile in here + // await EEAuditLogService.createAuditLog( + // {actor:ActorType.Machine}, + // { + // type: EventType.UPDATE_SECRETS, + // metadata: { + // environment, + // secretPath, + // secrets: secretsToBeUpdated.map(({ _id, version, secretBlindIndex }) => ({ + // secretId: _id.toString(), + // secretKey: secretBlindIndexToKey[secretBlindIndex || ""], + // secretVersion: version + 1 + // })) + // } + // }, + // { + // workspaceId + // } + // ); + + const folderId = encryptedSecrets?.[0]?.secret?.folder; + // (EE) take a secret snapshot + await EESecretService.takeSecretSnapshot({ + workspaceId, + environment: secretRotation.environment, + folderId + }); + + await EventService.handleEvent({ + event: eventPushSecrets({ + workspaceId: secretRotation.workspace, + environment: secretRotation.environment, + secretPath: secretRotation.secretPath + }) + }); + + const postHogClient = await TelemetryService.getPostHogClient(); + if (postHogClient) { + postHogClient.capture({ + event: "secrets rotated", + properties: { + numberOfSecrets: encryptedSecrets.length, + environment: secretRotation.environment, + workspaceId, + folderId + } + }); + } + } catch (err) { + logger.error(err); + await SecretRotation.findByIdAndUpdate(rotationStratDocId, { + status: "failed", + statusMessage: (err as Error).message, + lastRotatedAt: new Date().toUTCString() + }); + } + + return Promise.resolve(); +}); + +const daysToMillisecond = (days: number) => days * 24 * 60 * 60 * 1000; +export const startSecretRotationQueue = async (rotationDocId: string, interval: number) => { + // when migration to bull mq just use the option immedite to trigger repeatable immediately + secretRotationQueue.add({ rotationDocId }, { jobId: rotationDocId, removeOnComplete: true }); + return secretRotationQueue.add( + { rotationDocId }, + { repeat: { every: daysToMillisecond(interval) }, jobId: rotationDocId } + ); +}; + +export const removeSecretRotationQueue = async (rotationDocId: string, interval: number) => { + return secretRotationQueue.removeRepeatable({ every: interval * 1000, jobId: rotationDocId }); +}; diff --git a/backend/src/ee/secretRotation/queue/queue.utils.ts b/backend/src/ee/secretRotation/queue/queue.utils.ts new file mode 100644 index 000000000..c1ddbefc1 --- /dev/null +++ b/backend/src/ee/secretRotation/queue/queue.utils.ts @@ -0,0 +1,179 @@ +import axios from "axios"; +import jmespath from "jmespath"; +import { customAlphabet } from "nanoid"; +import { Client as PgClient } from "pg"; +import mysql from "mysql2"; +import { + ISecretRotationData, + TAssignOp, + TDbProviderClients, + TDbProviderFunction, + TDirectAssignOp, + THttpProviderFunction, + TProviderFunction, + TProviderFunctionTypes +} from "../types"; +const REGEX = /\${([^}]+)}/g; +const SLUG_ALPHABETS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; +const nanoId = customAlphabet(SLUG_ALPHABETS, 10); + +export const interpolate = (data: any, getValue: (key: string) => unknown) => { + if (!data) return; + + if (typeof data === "number") return data; + + if (typeof data === "string") { + return data.replace(REGEX, (_a, b) => getValue(b) as string); + } + + if (typeof data === "object" && Array.isArray(data)) { + data.forEach((el, index) => { + data[index] = interpolate(el, getValue); + }); + } + + if (typeof data === "object") { + if ((data as { ref: string })?.ref) return getValue((data as { ref: string }).ref); + const temp = data as Record; // for converting ts object to record type + Object.keys(temp).forEach((key) => { + temp[key as keyof typeof temp] = interpolate(data[key as keyof typeof temp], getValue); + }); + } + return data; +}; + +const getInterpolationValue = (variables: ISecretRotationData) => (key: string) => { + if (key.includes("|")) { + const [keyword, ...arg] = key.split("|").map((el) => el.trim()); + switch (keyword) { + case "random": { + return nanoId(parseInt(arg[0], 10)); + } + default: { + throw Error(`Interpolation key not found - ${key}`); + } + } + } + const [type, keyName] = key.split(".").map((el) => el.trim()); + return variables[type as keyof ISecretRotationData][keyName]; +}; + +export const secretRotationHttpFn = async ( + func: THttpProviderFunction, + variables: ISecretRotationData +) => { + // string interpolation + const headers = interpolate(func.header, getInterpolationValue(variables)); + const url = interpolate(func.url, getInterpolationValue(variables)); + const body = interpolate(func.body, getInterpolationValue(variables)); + // axios will automatically throw error if req status is not between 2xx range + return axios({ method: func.method, url, headers, data: body }); +}; + +export const secretRotationDbFn = async ( + func: TDbProviderFunction, + variables: ISecretRotationData +) => { + const { type, client, pre, ...dbConnection } = func; + const { username, password, host, database, port, query, ca } = interpolate( + dbConnection, + getInterpolationValue(variables) + ); + const ssl = ca ? { rejectUnauthorized: false, ca } : undefined; + if (host === "localhost" || host === "127.0.0.1") throw new Error("Invalid db host"); + if (client === TDbProviderClients.Pg) { + const pgClient = new PgClient({ user: username, password, host, database, port, ssl }); + await pgClient.connect(); + const res = await pgClient.query(query); + await pgClient.end(); + return res.rows[0]; + } else if (client === TDbProviderClients.Sql) { + const sqlClient = mysql.createPool({ + user: username, + password, + host, + database, + port, + connectionLimit: 1, + ssl + }); + const res = await new Promise((resolve, reject) => { + sqlClient.query(query, (err, data) => { + if (err) return reject(err); + resolve(data); + }); + }); + await new Promise((resolve, reject) => { + sqlClient.end(function (err) { + if (err) return reject(err); + return resolve({}); + }); + }); + return (res as any)?.[0]; + } +}; + +export const secretRotationPreSetFn = ( + op: Record, + variables: ISecretRotationData +) => { + const getValFn = getInterpolationValue(variables); + Object.entries(op || {}).forEach(([key, assignFn]) => { + const [type, keyName] = key.split(".") as [keyof ISecretRotationData, string]; + variables[type][keyName] = interpolate(assignFn.value, getValFn); + }); +}; + +export const secretRotationSetFn = async ( + func: TProviderFunction, + variables: ISecretRotationData +) => { + const getValFn = getInterpolationValue(variables); + // http setter + if (func.type === TProviderFunctionTypes.HTTP) { + const res = await secretRotationHttpFn(func, variables); + Object.entries(func.setter || {}).forEach(([key, assignFn]) => { + const [type, keyName] = key.split(".") as [keyof ISecretRotationData, string]; + if (assignFn.assign === TAssignOp.JmesPath) { + variables[type][keyName] = jmespath.search(res.data, assignFn.path); + } else if (assignFn.value) { + variables[type][keyName] = interpolate(assignFn.value, getValFn); + } + }); + // db setter + } else if (func.type === TProviderFunctionTypes.DB) { + const data = await secretRotationDbFn(func, variables); + Object.entries(func.setter || {}).forEach(([key, assignFn]) => { + const [type, keyName] = key.split(".") as [keyof ISecretRotationData, string]; + if (assignFn.assign === TAssignOp.JmesPath) { + if (typeof data === "object") { + variables[type][keyName] = jmespath.search(data, assignFn.path); + } + } else if (assignFn.value) { + variables[type][keyName] = interpolate(assignFn.value, getValFn); + } + }); + } +}; + +export const secretRotationTestFn = async ( + func: TProviderFunction, + variables: ISecretRotationData +) => { + if (func.type === TProviderFunctionTypes.HTTP) { + await secretRotationHttpFn(func, variables); + } else if (func.type === TProviderFunctionTypes.DB) { + await secretRotationDbFn(func, variables); + } +}; + +export const secretRotationRemoveFn = async ( + func: TProviderFunction, + variables: ISecretRotationData +) => { + if (!func) return; + if (func.type === TProviderFunctionTypes.HTTP) { + // string interpolation + return await secretRotationHttpFn(func, variables); + } +}; diff --git a/backend/src/ee/secretRotation/service.ts b/backend/src/ee/secretRotation/service.ts new file mode 100644 index 000000000..9e00f20e1 --- /dev/null +++ b/backend/src/ee/secretRotation/service.ts @@ -0,0 +1,130 @@ +import { ISecretRotationEncData, TCreateSecretRotation, TGetProviderTemplates } from "./types"; +import { rotationTemplates } from "./templates"; +import { SecretRotation } from "./models"; +import { client, getEncryptionKey, getRootEncryptionKey } from "../../config"; +import { BadRequestError } from "../../utils/errors"; +import Ajv from "ajv"; +import { removeSecretRotationQueue, startSecretRotationQueue } from "./queue/queue"; +import { + ALGORITHM_AES_256_GCM, + ENCODING_SCHEME_BASE64, + ENCODING_SCHEME_UTF8 +} from "../../variables"; +import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto"; + +const ajv = new Ajv({ strict: false }); + +export const getProviderTemplate = async ({ workspaceId }: TGetProviderTemplates) => { + return { + custom: [], + providers: rotationTemplates + }; +}; + +export const createSecretRotation = async ({ + workspaceId, + secretPath, + environment, + provider, + interval, + inputs, + outputs +}: TCreateSecretRotation) => { + const rotationTemplate = rotationTemplates.find(({ name }) => name === provider); + if (!rotationTemplate) throw BadRequestError({ message: "Provider not found" }); + + const formattedInputs: Record = {}; + Object.entries(inputs).forEach(([key, value]) => { + const type = rotationTemplate.template.inputs.properties[key].type; + if (type === "string") { + formattedInputs[key] = value; + return; + } + if (type === "integer") { + formattedInputs[key] = parseInt(value as string, 10); + return; + } + formattedInputs[key] = JSON.parse(value as string); + }); + // ensure input one follows the correct schema + const valid = ajv.validate(rotationTemplate.template.inputs, formattedInputs); + if (!valid) { + throw BadRequestError({ message: ajv.errors?.[0].message }); + } + + const encData: Partial = { + inputs: formattedInputs, + creds: [] + }; + + const secretRotation = new SecretRotation({ + workspace: workspaceId, + provider, + environment, + secretPath, + interval, + outputs: Object.entries(outputs).map(([key, secret]) => ({ key, secret })) + }); + + const encryptionKey = await getEncryptionKey(); + const rootEncryptionKey = await getRootEncryptionKey(); + + if (rootEncryptionKey) { + const { ciphertext, iv, tag } = client.encryptSymmetric( + JSON.stringify(encData), + rootEncryptionKey + ); + secretRotation.encryptedDataIV = iv; + secretRotation.encryptedDataTag = tag; + secretRotation.encryptedData = ciphertext; + secretRotation.algorithm = ALGORITHM_AES_256_GCM; + secretRotation.keyEncoding = ENCODING_SCHEME_BASE64; + } else if (encryptionKey) { + const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({ + plaintext: JSON.stringify(encData), + key: encryptionKey + }); + secretRotation.encryptedDataIV = iv; + secretRotation.encryptedDataTag = tag; + secretRotation.encryptedData = ciphertext; + secretRotation.algorithm = ALGORITHM_AES_256_GCM; + secretRotation.keyEncoding = ENCODING_SCHEME_UTF8; + } + + await secretRotation.save(); + await startSecretRotationQueue(secretRotation._id.toString(), interval); + + return secretRotation; +}; + +export const deleteSecretRotation = async ({ id }: { id: string }) => { + const doc = await SecretRotation.findByIdAndRemove(id); + if (!doc) throw BadRequestError({ message: "Rotation not found" }); + + await removeSecretRotationQueue(doc._id.toString(), doc.interval); + return doc; +}; + +export const restartSecretRotation = async ({ id }: { id: string }) => { + const secretRotation = await SecretRotation.findById(id); + if (!secretRotation) throw BadRequestError({ message: "Rotation not found" }); + + await removeSecretRotationQueue(secretRotation._id.toString(), secretRotation.interval); + await startSecretRotationQueue(secretRotation._id.toString(), secretRotation.interval); + + return secretRotation; +}; + +export const getSecretRotationById = async ({ id }: { id: string }) => { + const doc = await SecretRotation.findById(id); + if (!doc) throw BadRequestError({ message: "Rotation not found" }); + return doc; +}; + +export const getSecretRotationOfWorkspace = async (workspaceId: string) => { + const secretRotations = await SecretRotation.find({ + workspace: workspaceId + }).populate("outputs.secret"); + + return secretRotations; +}; diff --git a/backend/src/ee/secretRotation/templates/index.ts b/backend/src/ee/secretRotation/templates/index.ts new file mode 100644 index 000000000..063d5149f --- /dev/null +++ b/backend/src/ee/secretRotation/templates/index.ts @@ -0,0 +1,28 @@ +import { ISecretRotationProviderTemplate } from "../types"; +import { MYSQL_TEMPLATE } from "./mysql"; +import { POSTGRES_TEMPLATE } from "./postgres"; +import { SENDGRID_TEMPLATE } from "./sendgrid"; + +export const rotationTemplates: ISecretRotationProviderTemplate[] = [ + { + name: "sendgrid", + title: "Twilio Sendgrid", + image: "sendgrid.png", + description: "Rotate Twilio Sendgrid API keys", + template: SENDGRID_TEMPLATE + }, + { + name: "postgres", + title: "PostgreSQL", + image: "postgres.png", + description: "Rotate PostgreSQL/CockroachDB user credentials", + template: POSTGRES_TEMPLATE + }, + { + name: "mysql", + title: "MySQL", + image: "mysql.png", + description: "Rotate MySQL@7/MariaDB user credentials", + template: MYSQL_TEMPLATE + } +]; diff --git a/backend/src/ee/secretRotation/templates/mysql.ts b/backend/src/ee/secretRotation/templates/mysql.ts new file mode 100644 index 000000000..ce44c753f --- /dev/null +++ b/backend/src/ee/secretRotation/templates/mysql.ts @@ -0,0 +1,83 @@ +import { TAssignOp, TDbProviderClients, TProviderFunctionTypes } from "../types"; + +export const MYSQL_TEMPLATE = { + inputs: { + type: "object" as const, + properties: { + admin_username: { type: "string" as const }, + admin_password: { type: "string" as const }, + host: { type: "string" as const }, + database: { type: "string" as const }, + port: { type: "integer" as const, default: "3306" }, + username1: { + type: "string", + default: "infisical-sql-user1", + desc: "This user must be created in your database" + }, + username2: { + type: "string", + default: "infisical-sql-user2", + desc: "This user must be created in your database" + }, + ca: { type: "string", desc: "SSL certificate for db auth(string)" } + }, + required: [ + "admin_username", + "admin_password", + "host", + "database", + "username1", + "username2", + "port" + ], + additionalProperties: false + }, + outputs: { + db_username: { type: "string" }, + db_password: { type: "string" } + }, + internal: { + rotated_password: { type: "string" }, + username: { type: "string" } + }, + functions: { + set: { + type: TProviderFunctionTypes.DB as const, + client: TDbProviderClients.Sql, + username: "${inputs.admin_username}", + password: "${inputs.admin_password}", + host: "${inputs.host}", + database: "${inputs.database}", + port: "${inputs.port}", + ca: "${inputs.ca}", + query: "ALTER USER ${internal.username} IDENTIFIED BY '${internal.rotated_password}'", + setter: { + "outputs.db_username": { + assign: TAssignOp.Direct as const, + value: "${internal.username}" + }, + "outputs.db_password": { + assign: TAssignOp.Direct as const, + value: "${internal.rotated_password}" + } + }, + pre: { + "internal.rotated_password": { + assign: TAssignOp.Direct as const, + value: "${random | 32}" + } + } + }, + test: { + type: TProviderFunctionTypes.DB as const, + client: TDbProviderClients.Sql, + username: "${internal.username}", + password: "${internal.rotated_password}", + host: "${inputs.host}", + database: "${inputs.database}", + port: "${inputs.port}", + ca: "${inputs.ca}", + query: "SELECT NOW()" + } + } +}; diff --git a/backend/src/ee/secretRotation/templates/postgres.ts b/backend/src/ee/secretRotation/templates/postgres.ts new file mode 100644 index 000000000..3b3153be1 --- /dev/null +++ b/backend/src/ee/secretRotation/templates/postgres.ts @@ -0,0 +1,83 @@ +import { TAssignOp, TDbProviderClients, TProviderFunctionTypes } from "../types"; + +export const POSTGRES_TEMPLATE = { + inputs: { + type: "object" as const, + properties: { + admin_username: { type: "string" as const }, + admin_password: { type: "string" as const }, + host: { type: "string" as const }, + database: { type: "string" as const }, + port: { type: "integer" as const, default: "5432" }, + username1: { + type: "string", + default: "infisical-pg-user1", + desc: "This user must be created in your database" + }, + username2: { + type: "string", + default: "infisical-pg-user2", + desc: "This user must be created in your database" + }, + ca: { type: "string", desc: "SSL certificate for db auth(string)" } + }, + required: [ + "admin_username", + "admin_password", + "host", + "database", + "username1", + "username2", + "port" + ], + additionalProperties: false + }, + outputs: { + db_username: { type: "string" }, + db_password: { type: "string" } + }, + internal: { + rotated_password: { type: "string" }, + username: { type: "string" } + }, + functions: { + set: { + type: TProviderFunctionTypes.DB as const, + client: TDbProviderClients.Pg, + username: "${inputs.admin_username}", + password: "${inputs.admin_password}", + host: "${inputs.host}", + database: "${inputs.database}", + port: "${inputs.port}", + ca: "${inputs.ca}", + query: "ALTER USER ${internal.username} WITH PASSWORD '${internal.rotated_password}'", + setter: { + "outputs.db_username": { + assign: TAssignOp.Direct as const, + value: "${internal.username}" + }, + "outputs.db_password": { + assign: TAssignOp.Direct as const, + value: "${internal.rotated_password}" + } + }, + pre: { + "internal.rotated_password": { + assign: TAssignOp.Direct as const, + value: "${random | 32}" + } + } + }, + test: { + type: TProviderFunctionTypes.DB as const, + client: TDbProviderClients.Pg, + username: "${internal.username}", + password: "${internal.rotated_password}", + host: "${inputs.host}", + database: "${inputs.database}", + port: "${inputs.port}", + ca: "${inputs.ca}", + query: "SELECT NOW()" + } + } +}; diff --git a/backend/src/ee/secretRotation/templates/sendgrid.ts b/backend/src/ee/secretRotation/templates/sendgrid.ts new file mode 100644 index 000000000..b600f3e0c --- /dev/null +++ b/backend/src/ee/secretRotation/templates/sendgrid.ts @@ -0,0 +1,63 @@ +import { TAssignOp, TProviderFunctionTypes } from "../types"; + +export const SENDGRID_TEMPLATE = { + inputs: { + type: "object" as const, + properties: { + admin_api_key: { type: "string" as const, desc: "Sendgrid admin api key to create new keys" }, + api_key_scopes: { + type: "array", + items: { type: "string" as const }, + desc: "Scopes for created tokens by rotation(Array)" + } + }, + required: ["admin_api_key", "api_key_scopes"], + additionalProperties: false + }, + outputs: { + api_key: { type: "string" } + }, + internal: { + api_key_id: { type: "string" } + }, + functions: { + set: { + type: TProviderFunctionTypes.HTTP as const, + url: "https://api.sendgrid.com/v3/api_keys", + method: "POST", + header: { + Authorization: "Bearer ${inputs.admin_api_key}" + }, + body: { + name: "infisical-${random | 16}", + scopes: { ref: "inputs.api_key_scopes" } + }, + setter: { + "outputs.api_key": { + assign: TAssignOp.JmesPath as const, + path: "api_key" + }, + "internal.api_key_id": { + assign: TAssignOp.JmesPath as const, + path: "api_key_id" + } + } + }, + remove: { + type: TProviderFunctionTypes.HTTP as const, + url: "https://api.sendgrid.com/v3/api_keys/${internal.api_key_id}", + header: { + Authorization: "Bearer ${inputs.admin_api_key}" + }, + method: "DELETE" + }, + test: { + type: TProviderFunctionTypes.HTTP as const, + url: "https://api.sendgrid.com/v3/api_keys/${internal.api_key_id}", + header: { + Authorization: "Bearer ${inputs.admin_api_key}" + }, + method: "GET" + } + } +}; diff --git a/backend/src/ee/secretRotation/types.ts b/backend/src/ee/secretRotation/types.ts new file mode 100644 index 000000000..36ad36798 --- /dev/null +++ b/backend/src/ee/secretRotation/types.ts @@ -0,0 +1,131 @@ +import { Document, Types } from "mongoose"; + +export interface ISecretRotation extends Document { + _id: Types.ObjectId; + name: string; + interval: number; + provider: string; + customProvider: Types.ObjectId; + workspace: Types.ObjectId; + environment: string; + secretPath: string; + outputs: Array<{ + key: string; + secret: Types.ObjectId; + }>; + status?: "success" | "failed"; + lastRotatedAt?: string; + statusMessage?: string; + encryptedData: string; + encryptedDataIV: string; + encryptedDataTag: string; + algorithm: string; + keyEncoding: string; +} + +export type ISecretRotationEncData = { + inputs: Record; + creds: Array<{ + outputs: Record; + internal: Record; + }>; +}; + +export type ISecretRotationData = { + inputs: Record; + outputs: Record; + internal: Record; +}; + +export type ISecretRotationProviderTemplate = { + name: string; + title: string; + image?: string; + description?: string; + template: TProviderTemplate; +}; + +export enum TProviderFunctionTypes { + HTTP = "http", + DB = "database" +} + +export enum TDbProviderClients { + // postgres, cockroack db, amazon red shift + Pg = "pg", + // mysql and maria db + Sql = "sql" +} + +export enum TAssignOp { + Direct = "direct", + JmesPath = "jmesopath" +} + +export type TJmesPathAssignOp = { + assign: TAssignOp.JmesPath; + path: string; +}; + +export type TDirectAssignOp = { + assign: TAssignOp.Direct; + value: string; +}; + +export type TAssignFunction = TJmesPathAssignOp | TDirectAssignOp; + +export type THttpProviderFunction = { + type: TProviderFunctionTypes.HTTP; + url: string; + method: string; + header?: Record; + query?: Record; + body?: Record; + setter?: Record; + pre?: Record; +}; + +export type TDbProviderFunction = { + type: TProviderFunctionTypes.DB; + client: TDbProviderClients; + username: string; + password: string; + host: string; + database: string; + port: string; + query: string; + setter?: Record; + pre?: Record; +}; + +export type TProviderFunction = THttpProviderFunction | TDbProviderFunction; + +export type TProviderTemplate = { + inputs: { + type: "object"; + properties: Record; + required?: string[]; + }; + outputs: Record; + functions: { + set: TProviderFunction; + remove?: TProviderFunction; + test: TProviderFunction; + }; +}; + +// function type args +export type TGetProviderTemplates = { + workspaceId: string; +}; + +export type TCreateSecretRotation = { + provider: string; + customProvider?: string; + workspaceId: string; + secretPath: string; + environment: string; + interval: number; + inputs: Record; + outputs: Record; +}; diff --git a/backend/src/ee/services/EELicenseService.ts b/backend/src/ee/services/EELicenseService.ts index f403f55d6..6baea04dc 100644 --- a/backend/src/ee/services/EELicenseService.ts +++ b/backend/src/ee/services/EELicenseService.ts @@ -38,6 +38,7 @@ interface FeatureSet { trial_end: number | null; has_used_trial: boolean; secretApproval: boolean; + secretRotation: boolean; } /** @@ -74,7 +75,8 @@ class EELicenseService { status: null, trial_end: null, has_used_trial: true, - secretApproval: false + secretApproval: false, + secretRotation: true, } public localFeatureSet: NodeCache; diff --git a/backend/src/ee/services/ProjectRoleService.ts b/backend/src/ee/services/ProjectRoleService.ts index 867edb7e5..ac71121de 100644 --- a/backend/src/ee/services/ProjectRoleService.ts +++ b/backend/src/ee/services/ProjectRoleService.ts @@ -50,7 +50,8 @@ export enum ProjectPermissionSub { Workspace = "workspace", Secrets = "secrets", SecretRollback = "secret-rollback", - SecretApproval = "secret-approval" + SecretApproval = "secret-approval", + SecretRotation = "secret-rotation" } type SubjectFields = { @@ -74,6 +75,7 @@ export type ProjectPermissionSet = | [ProjectPermissionActions, ProjectPermissionSub.Settings] | [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens] | [ProjectPermissionActions, ProjectPermissionSub.SecretApproval] + | [ProjectPermissionActions, ProjectPermissionSub.SecretRotation] | [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace] | [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace] | [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback] @@ -92,6 +94,11 @@ const buildAdminPermission = () => { can(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval); can(ProjectPermissionActions.Delete, ProjectPermissionSub.SecretApproval); + can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation); + can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRotation); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretRotation); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.SecretRotation); + can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback); @@ -162,6 +169,7 @@ const buildMemberPermission = () => { can(ProjectPermissionActions.Delete, ProjectPermissionSub.Secrets); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); + can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback); @@ -214,6 +222,7 @@ const buildViewerPermission = () => { can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); + can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation); can(ProjectPermissionActions.Read, ProjectPermissionSub.Member); can(ProjectPermissionActions.Read, ProjectPermissionSub.Role); can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); diff --git a/backend/src/ee/validation/secretRotation.ts b/backend/src/ee/validation/secretRotation.ts new file mode 100644 index 000000000..616844aaf --- /dev/null +++ b/backend/src/ee/validation/secretRotation.ts @@ -0,0 +1,32 @@ +import { z } from "zod"; + +export const createSecretRotationV1 = z.object({ + body: z.object({ + workspaceId: z.string().trim(), + secretPath: z.string().trim(), + environment: z.string().trim(), + interval: z.number().min(1), + provider: z.string().trim(), + customProvider: z.string().trim().optional(), + inputs: z.record(z.unknown()), + outputs: z.record(z.string()) + }) +}); + +export const restartSecretRotationV1 = z.object({ + body: z.object({ + id: z.string().trim() + }) +}); + +export const getSecretRotationV1 = z.object({ + query: z.object({ + workspaceId: z.string().trim() + }) +}); + +export const removeSecretRotationV1 = z.object({ + params: z.object({ + id: z.string().trim() + }) +}); diff --git a/backend/src/ee/validation/secretRotationProvider.ts b/backend/src/ee/validation/secretRotationProvider.ts new file mode 100644 index 000000000..d322939bb --- /dev/null +++ b/backend/src/ee/validation/secretRotationProvider.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; + +export const getSecretRotationProvidersV1 = z.object({ + params: z.object({ + workspaceId: z.string() + }) +}); diff --git a/backend/src/helpers/auth.ts b/backend/src/helpers/auth.ts index a8b13fd82..14024bd03 100644 --- a/backend/src/helpers/auth.ts +++ b/backend/src/helpers/auth.ts @@ -1,381 +1,13 @@ -import { Request } from "express"; import { Types } from "mongoose"; import jwt from "jsonwebtoken"; -import bcrypt from "bcrypt"; -import { - APIKeyData, - APIKeyDataV2, - ITokenVersion, - IUser, - ServiceTokenData, - ServiceTokenDataV3, - TokenVersion, - User, -} from "../models"; -import { - APIKeyDataNotFoundError, - AccountNotFoundError, - BadRequestError, - ServiceTokenDataNotFoundError, - UnauthorizedRequestError, -} from "../utils/errors"; +import { ITokenVersion, TokenVersion } from "../models"; +import { UnauthorizedRequestError } from "../utils/errors"; import { getAuthSecret, getJwtAuthLifetime, - getJwtRefreshLifetime, - getJwtServiceTokenSecret + getJwtRefreshLifetime } from "../config"; -import { - AuthMode, - AuthTokenType -} from "../variables"; -import { - ServiceTokenAuthData, - ServiceTokenV3AuthData, - UserAuthData -} from "../interfaces/middleware"; - -import { ActorType } from "../ee/models"; -import { getUserAgentType } from "../utils/posthog"; - -/** - * - * @param {Object} obj - * @param {Object} obj.headers - HTTP request headers object - */ -export const validateAuthMode = ({ - headers, - acceptedAuthModes, -}: { - headers: { [key: string]: string | string[] | undefined }, - acceptedAuthModes: AuthMode[] -}) => { - - const apiKey = headers["x-api-key"]; - const authHeader = headers["authorization"]; - - let authMode, authTokenValue; - if (apiKey === undefined && authHeader === undefined) { - // case: no auth or X-API-KEY header present - throw BadRequestError({ message: "Missing Authorization or X-API-KEY in request header." }); - } - - if (typeof apiKey === "string") { - // case: treat request authentication type as via X-API-KEY (i.e. API Key) - authMode = AuthMode.API_KEY; - authTokenValue = apiKey; - } - - if (typeof authHeader === "string") { - // case: treat request authentication type as via Authorization header (i.e. either JWT or service token) - const [tokenType, tokenValue] = <[string, string]>authHeader.split(" ", 2) ?? [null, null] - - if (tokenType === null) - throw BadRequestError({ message: "Missing Authorization Header in the request header." }); - if (tokenType.toLowerCase() !== "bearer") - throw BadRequestError({ message: `The provided authentication type '${tokenType}' is not supported.` }); - if (tokenValue === null) - throw BadRequestError({ message: "Missing Authorization Body in the request header." }); - - const parts = tokenValue.split("."); - - switch (parts[0]) { - case "st": - authMode = AuthMode.SERVICE_TOKEN; - authTokenValue = tokenValue; - break; - case "stv3": - authMode = AuthMode.SERVICE_TOKEN_V3; - authTokenValue = parts.slice(1).join("."); - break; - default: - authMode = AuthMode.JWT; - authTokenValue = tokenValue; - } - } - - if (!authMode || !authTokenValue) throw BadRequestError({ message: "Missing valid Authorization or X-API-KEY in request header." }); - - if (!acceptedAuthModes.includes(authMode)) throw BadRequestError({ message: "The provided authentication type is not supported." }); - - return ({ - authMode, - authTokenValue, - }); -} - -/** - * Return user payload corresponding to JWT token [authTokenValue] - * that is either for browser / CLI or API Key - * @param {Object} obj - * @param {String} obj.authTokenValue - JWT token value - * @returns {User} user - user corresponding to JWT token - */ -export const getAuthUserPayload = async ({ - req, - authTokenValue, -}: { - req: Request, - authTokenValue: string; -}): Promise => { - const decodedToken = ( - jwt.verify(authTokenValue, await getAuthSecret()) - ); - - if ( - decodedToken.authTokenType !== AuthTokenType.ACCESS_TOKEN && - decodedToken.authTokenType !== AuthTokenType.API_KEY - ) { - throw UnauthorizedRequestError(); - } - - if (decodedToken.authTokenType === AuthTokenType.ACCESS_TOKEN) { - const tokenVersion = await TokenVersion.findOneAndUpdate({ - _id: new Types.ObjectId(decodedToken.tokenVersionId), - user: decodedToken.userId - }, { - lastUsed: new Date(), - }); - - if (!tokenVersion) throw UnauthorizedRequestError(); - - if (decodedToken.accessVersion !== tokenVersion.accessVersion) throw UnauthorizedRequestError(); - } else if (decodedToken.authTokenType === AuthTokenType.API_KEY) { - const apiKeyData = await APIKeyDataV2.findOneAndUpdate( - { - _id: new Types.ObjectId(decodedToken.apiKeyDataId), - user: new Types.ObjectId(decodedToken.userId) - }, - { - lastUsed: new Date(), - $inc: { usageCount: 1 } - }, - { - new: true - } - ); - - if (!apiKeyData) throw UnauthorizedRequestError(); - } - - const user = await User.findOne({ - _id: new Types.ObjectId(decodedToken.userId), - }).select("+publicKey +accessVersion"); - - if (!user) throw AccountNotFoundError({ message: "Failed to find user" }); - - if (!user?.publicKey) throw UnauthorizedRequestError({ message: "Failed to authenticate user with partially set up account" }); - - return { - actor: { - type: ActorType.USER, - metadata: { - userId: user._id.toString(), - email: user.email - } - }, - authPayload: user, - ipAddress: req.realIP, - userAgent: req.headers["user-agent"] ?? "", - userAgentType: getUserAgentType(req.headers["user-agent"]) - } -} - -/** - * Return service token data payload corresponding to service token [authTokenValue] - * @param {Object} obj - * @param {String} obj.authTokenValue - service token value - * @returns {ServiceTokenData} serviceTokenData - service token data - */ -export const getAuthSTDPayload = async ({ - req, - authTokenValue, -}: { - req: Request, - authTokenValue: string; -}): Promise => { - const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split(".", 3); - - const serviceTokenData = await ServiceTokenData - .findById(TOKEN_IDENTIFIER, "+secretHash +expiresAt") - - if (!serviceTokenData) { - throw ServiceTokenDataNotFoundError({ message: "Failed to find service token data" }); - } else if (serviceTokenData?.expiresAt && new Date(serviceTokenData.expiresAt) < new Date()) { - // case: service token expired - await ServiceTokenData.findByIdAndDelete(serviceTokenData._id); - throw UnauthorizedRequestError({ - message: "Failed to authenticate expired service token", - }); - } - - const isMatch = await bcrypt.compare(TOKEN_SECRET, serviceTokenData.secretHash); - if (!isMatch) throw UnauthorizedRequestError({ - message: "Failed to authenticate service token", - }); - - const serviceTokenDataToReturn = await ServiceTokenData - .findOneAndUpdate({ - _id: new Types.ObjectId(TOKEN_IDENTIFIER), - }, { - lastUsed: new Date(), - }, { - new: true, - }) - .select("+encryptedKey +iv +tag") - - if (!serviceTokenDataToReturn) throw ServiceTokenDataNotFoundError({ message: "Failed to find service token data" }); - - return { - actor: { - type: ActorType.SERVICE, - metadata: { - serviceId: serviceTokenDataToReturn._id.toString(), - name: serviceTokenDataToReturn.name - } - }, - authPayload: serviceTokenDataToReturn, - ipAddress: req.realIP, - userAgent: req.headers["user-agent"] ?? "", - userAgentType: getUserAgentType(req.headers["user-agent"]) - } -} - -/** - * Return service token data V3 payload corresponding to service token [authTokenValue] - * @param {Object} obj - * @param {String} obj.authTokenValue - service token value - * @returns {ServiceTokenData} serviceTokenData - service token data - */ - export const getAuthSTDV3Payload = async ({ - req, - authTokenValue, -}: { - req: Request, - authTokenValue: string; -}): Promise => { - const decodedToken = ( - jwt.verify(authTokenValue, await getJwtServiceTokenSecret()) - ); - - const serviceTokenData = await ServiceTokenDataV3.findOneAndUpdate( - { - _id: new Types.ObjectId(decodedToken._id), - isActive: true - }, - { - lastUsed: new Date(), - $inc: { usageCount: 1 } - }, - { - new: true - } - ); - - if (!serviceTokenData) { - throw UnauthorizedRequestError({ - message: "Failed to authenticate" - }); - } else if (serviceTokenData?.expiresAt && new Date(serviceTokenData.expiresAt) < new Date()) { - // case: service token expired - await ServiceTokenDataV3.findByIdAndUpdate( - serviceTokenData._id, - { - isActive: false - }, - { - new: true - } - ); - - throw UnauthorizedRequestError({ - message: "Failed to authenticate", - }); - } - - return { - actor: { - type: ActorType.SERVICE_V3, - metadata: { - serviceId: serviceTokenData._id.toString(), - name: serviceTokenData.name - } - }, - authPayload: serviceTokenData, - ipAddress: req.realIP, - userAgent: req.headers["user-agent"] ?? "", - userAgentType: getUserAgentType(req.headers["user-agent"]) - } -} - -/** - * Return API key data payload corresponding to API key [authTokenValue] - * @param {Object} obj - * @param {String} obj.authTokenValue - API key value - * @returns {APIKeyData} apiKeyData - API key data - */ -export const getAuthAPIKeyPayload = async ({ - req, - authTokenValue, -}: { - req: Request, - authTokenValue: string; -}): Promise => { - const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split(".", 3); - - let apiKeyData = await APIKeyData - .findById(TOKEN_IDENTIFIER, "+secretHash +expiresAt") - .populate<{ user: IUser }>("user", "+publicKey"); - - if (!apiKeyData) { - throw APIKeyDataNotFoundError({ message: "Failed to find API key data" }); - } else if (apiKeyData?.expiresAt && new Date(apiKeyData.expiresAt) < new Date()) { - // case: API key expired - await APIKeyData.findByIdAndDelete(apiKeyData._id); - throw UnauthorizedRequestError({ - message: "Failed to authenticate expired API key", - }); - } - - const isMatch = await bcrypt.compare(TOKEN_SECRET, apiKeyData.secretHash); - if (!isMatch) throw UnauthorizedRequestError({ - message: "Failed to authenticate API key", - }); - - apiKeyData = await APIKeyData.findOneAndUpdate({ - _id: new Types.ObjectId(TOKEN_IDENTIFIER), - }, { - lastUsed: new Date(), - }, { - new: true, - }); - - if (!apiKeyData) { - throw APIKeyDataNotFoundError({ message: "Failed to find API key data" }); - } - - const user = await User.findById(apiKeyData.user).select("+publicKey"); - - if (!user) { - throw AccountNotFoundError({ - message: "Failed to find user", - }); - } - - return { - actor: { - type: ActorType.USER, - metadata: { - userId: user._id.toString(), - email: user.email - } - }, - authPayload: user, - ipAddress: req.realIP, - userAgent: req.headers["user-agent"] ?? "", - userAgentType: getUserAgentType(req.headers["user-agent"]) - } -} +import { AuthTokenType } from "../variables"; /** * Return newly issued (JWT) auth and refresh tokens to user with id [userId] diff --git a/backend/src/helpers/database.ts b/backend/src/helpers/database.ts index 611e941f5..dc6d2faa6 100644 --- a/backend/src/helpers/database.ts +++ b/backend/src/helpers/database.ts @@ -1,5 +1,5 @@ import mongoose from "mongoose"; -import { getLogger } from "../utils/logger"; +import { logger } from "../utils/logging"; /** * Initialize database connection @@ -18,10 +18,10 @@ export const initDatabaseHelper = async ({ // allow empty strings to pass the required validator mongoose.Schema.Types.String.checkRequired(v => typeof v === "string"); - (await getLogger("database")).info("Database connection established"); + logger.info("Database connection established"); } catch (err) { - (await getLogger("database")).error(`Unable to establish Database connection due to the error.\n${err}`); + logger.error(err, "Unable to establish database connection"); } return mongoose.connection; diff --git a/backend/src/helpers/organization.ts b/backend/src/helpers/organization.ts index 2e5ee6ebb..a527c2c21 100644 --- a/backend/src/helpers/organization.ts +++ b/backend/src/helpers/organization.ts @@ -1,4 +1,4 @@ -import mongoose, { Types, mongo } from "mongoose"; +import { Types } from "mongoose"; import { Bot, BotKey, @@ -55,7 +55,7 @@ import { import { createBotOrg } from "./botOrg"; -import { InternalServerError, ResourceNotFoundError } from "../utils/errors"; +import { ResourceNotFoundError } from "../utils/errors"; /** * Create an organization with name [name] @@ -111,311 +111,215 @@ export const createOrganization = async ({ * @returns */ export const deleteOrganization = async ({ - organizationId, - existingSession + organizationId }: { organizationId: Types.ObjectId; - existingSession?: mongo.ClientSession; }) => { - let session; - - if (existingSession) { - session = existingSession; - } else { - session = await mongoose.startSession(); - session.startTransaction(); - } + const organization = await Organization.findByIdAndDelete( + organizationId + ); - try { - const organization = await Organization.findByIdAndDelete( - organizationId, - { - session - } + if (!organization) throw ResourceNotFoundError(); + + await MembershipOrg.deleteMany({ + organization: organization._id + }); + + await BotOrg.deleteMany({ + organization: organization._id + }); + + await SSOConfig.deleteMany({ + organization: organization._id + }); + + await Role.deleteMany({ + organization: organization._id + }); + + await IncidentContactOrg.deleteMany({ + organization: organization._id + }); + + await GitRisks.deleteMany({ + organization: organization._id + }); + + await GitAppInstallationSession.deleteMany({ + organization: organization._id + }); + + await GitAppOrganizationInstallation.deleteMany({ + organization: organization._id + }); + + const workspaceIds = await Workspace.distinct("_id", { + organization: organization._id + }); + + await Workspace.deleteMany({ + organization: organization._id + }); + + await Membership.deleteMany({ + workspace: { + $in: workspaceIds + } + }); + + await Key.deleteMany({ + workspace: { + $in: workspaceIds + } + }); + + await Bot.deleteMany({ + workspace: { + $in: workspaceIds + } + }); + + await BotKey.deleteMany({ + workspace: { + $in: workspaceIds + } + }); + + await SecretBlindIndexData.deleteMany({ + workspace: { + $in: workspaceIds + } + }); + + await Secret.deleteMany({ + workspace: { + $in: workspaceIds + } + }); + + await SecretVersion.deleteMany({ + workspace: { + $in: workspaceIds + } + }); + + await SecretSnapshot.deleteMany({ + workspace: { + $in: workspaceIds + } + }); + + await SecretImport.deleteMany({ + workspace: { + $in: workspaceIds + } + }); + + await Folder.deleteMany({ + workspace: { + $in: workspaceIds + } + }); + + await FolderVersion.deleteMany({ + workspace: { + $in: workspaceIds + } + }); + + await Webhook.deleteMany({ + workspace: { + $in: workspaceIds + } + }); + + await TrustedIP.deleteMany({ + workspace: { + $in: workspaceIds + } + }); + + await Tag.deleteMany({ + workspace: { + $in: workspaceIds + } + }); + + await IntegrationAuth.deleteMany({ + workspace: { + $in: workspaceIds + } + }); + + await Integration.deleteMany({ + workspace: { + $in: workspaceIds + } + }); + + await ServiceToken.deleteMany({ + workspace: { + $in: workspaceIds + } + }); + + await ServiceTokenData.deleteMany({ + workspace: { + $in: workspaceIds + } + }); + + await ServiceTokenDataV3.deleteMany({ + workspace: { + $in: workspaceIds + } + }); + + await ServiceTokenDataV3Key.deleteMany({ + workspace: { + $in: workspaceIds + } + }); + + await AuditLog.deleteMany({ + workspace: { + $in: workspaceIds + } + }); + + await Log.deleteMany({ + workspace: { + $in: workspaceIds + } + }); + + await Action.deleteMany({ + workspace: { + $in: workspaceIds + } + }); + + await SecretApprovalPolicy.deleteMany({ + workspace: { + $in: workspaceIds + } + }); + + await SecretApprovalRequest.deleteMany({ + workspace: { + $in: workspaceIds + } + }); + + if (organization.customerId) { + // delete from stripe here + await licenseServerKeyRequest.delete( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${organization.customerId}` ); - - if (!organization) throw ResourceNotFoundError(); - - await MembershipOrg.deleteMany({ - organization: organization._id - }, { - session - }); - - await BotOrg.deleteMany({ - organization: organization._id - }, { - session - }); - - await SSOConfig.deleteMany({ - organization: organization._id - }, { - session - }); - - await Role.deleteMany({ - organization: organization._id - }, { - session - }); - - await IncidentContactOrg.deleteMany({ - organization: organization._id - }, { - session - }); - - await GitRisks.deleteMany({ - organization: organization._id - }, { - session - }); - - await GitAppInstallationSession.deleteMany({ - organization: organization._id - }, { - session - }); - - await GitAppOrganizationInstallation.deleteMany({ - organization: organization._id - }, { - session - }); - - const workspaceIds = await Workspace.distinct("_id", { - organization: organization._id - }); - - await Workspace.deleteMany({ - organization: organization._id - }, { - session - }); - - await Membership.deleteMany({ - workspace: { - $in: workspaceIds - } - }, { - session - }); - - await Key.deleteMany({ - workspace: { - $in: workspaceIds - } - }, { - session - }); - - await Bot.deleteMany({ - workspace: { - $in: workspaceIds - } - }, { - session - }); - - await BotKey.deleteMany({ - workspace: { - $in: workspaceIds - } - }, { - session - }); - - await SecretBlindIndexData.deleteMany({ - workspace: { - $in: workspaceIds - } - }, { - session - }); - - await Secret.deleteMany({ - workspace: { - $in: workspaceIds - } - }, { - session - }); - - await SecretVersion.deleteMany({ - workspace: { - $in: workspaceIds - } - }, { - session - }); - - await SecretSnapshot.deleteMany({ - workspace: { - $in: workspaceIds - } - }, { - session - }); - - await SecretImport.deleteMany({ - workspace: { - $in: workspaceIds - } - }, { - session - }); - - await Folder.deleteMany({ - workspace: { - $in: workspaceIds - } - }, { - session - }); - - await FolderVersion.deleteMany({ - workspace: { - $in: workspaceIds - } - }, { - session - }); - - await Webhook.deleteMany({ - workspace: { - $in: workspaceIds - } - }, { - session - }); - - await TrustedIP.deleteMany({ - workspace: { - $in: workspaceIds - } - }, { - session - }); - - await Tag.deleteMany({ - workspace: { - $in: workspaceIds - } - }, { - session - }); - - await IntegrationAuth.deleteMany({ - workspace: { - $in: workspaceIds - } - }, { - session - }); - - await Integration.deleteMany({ - workspace: { - $in: workspaceIds - } - }, { - session - }); - - await ServiceToken.deleteMany({ - workspace: { - $in: workspaceIds - } - }, { - session - }); - - await ServiceTokenData.deleteMany({ - workspace: { - $in: workspaceIds - } - }, { - session - }); - - await ServiceTokenDataV3.deleteMany({ - workspace: { - $in: workspaceIds - } - }, { - session - }); - - await ServiceTokenDataV3Key.deleteMany({ - workspace: { - $in: workspaceIds - } - }, { - session - }); - - await AuditLog.deleteMany({ - workspace: { - $in: workspaceIds - } - }, { - session - }); - - await Log.deleteMany({ - workspace: { - $in: workspaceIds - } - }, { - session - }); - - await Action.deleteMany({ - workspace: { - $in: workspaceIds - } - }, { - session - }); - - await SecretApprovalPolicy.deleteMany({ - workspace: { - $in: workspaceIds - } - }, { - session - }); - - await SecretApprovalRequest.deleteMany({ - workspace: { - $in: workspaceIds - } - }, { - session - }); - - if (organization.customerId) { - // delete from stripe here - await licenseServerKeyRequest.delete( - `${await getLicenseServerUrl()}/api/license-server/v1/customers/${organization.customerId}` - ); - } - - return organization; - } catch (err) { - if (!existingSession) { - await session.abortTransaction(); - } - throw InternalServerError({ - message: "Failed to delete organization" - }); - } finally { - if (!existingSession) { - await session.commitTransaction(); - session.endSession(); - } } + + return organization; } /** diff --git a/backend/src/helpers/secrets.ts b/backend/src/helpers/secrets.ts index 82a98d1e9..f6252d5c6 100644 --- a/backend/src/helpers/secrets.ts +++ b/backend/src/helpers/secrets.ts @@ -49,7 +49,7 @@ import { import { TelemetryService } from "../services"; import { client, getEncryptionKey, getRootEncryptionKey } from "../config"; import { EEAuditLogService, EELogService, EESecretService } from "../ee/services"; -import { getAuthDataPayloadIdObj, getAuthDataPayloadUserObj } from "../utils/auth"; +import { getAuthDataPayloadIdObj, getAuthDataPayloadUserObj } from "../utils/authn/helpers"; import { getFolderByPath, getFolderIdFromServiceToken } from "../services/FolderService"; import picomatch from "picomatch"; import path from "path"; @@ -553,14 +553,22 @@ export const getSecretsHelper = async ({ workspaceId, environment, authData, - folderId, secretPath = "/" }: GetSecretsParams) => { let secrets: ISecret[] = []; // if using service token filter towards the folderId by secretpath - if (!folderId) { - folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath); + const folders = await Folder.findOne({ + workspace: workspaceId, + environment + }); + let folderId = "root"; + if (!folders && folderId !== "root") return []; + // get folder from folder tree + if (folders) { + const folder = getFolderByPath(folders.nodes, secretPath); + if (!folder) return []; + folderId = folder?.id; } // get personal secrets first diff --git a/backend/src/helpers/user.ts b/backend/src/helpers/user.ts index b78f471f1..5c840e906 100644 --- a/backend/src/helpers/user.ts +++ b/backend/src/helpers/user.ts @@ -1,4 +1,4 @@ -import mongoose, { Types, mongo } from "mongoose"; +import { Types } from "mongoose"; import { APIKeyData, BackupPrivateKey, @@ -222,141 +222,92 @@ const checkDeleteUserConditions = async ({ * @returns {User} user - deleted user */ export const deleteUser = async ({ - userId, - existingSession + userId }: { userId: Types.ObjectId; - existingSession?: mongo.ClientSession; }) => { + + const user = await User.findByIdAndDelete(userId); - let session; + if (!user) throw ResourceNotFoundError(); + + await checkDeleteUserConditions({ + userId: user._id + }); - if (existingSession) { - session = existingSession; - } else { - session = await mongoose.startSession(); - session.startTransaction(); + await UserAction.deleteMany({ + user: user._id + }); + + await BackupPrivateKey.deleteMany({ + user: user._id + }); + + await APIKeyData.deleteMany({ + user: user._id + }); + + await Action.deleteMany({ + user: user._id + }); + + await Log.deleteMany({ + user: user._id + }); + + await TokenVersion.deleteMany({ + user: user._id + }); + + await Key.deleteMany({ + receiver: user._id + }); + + const membershipOrgs = await MembershipOrg.find({ + user: userId + }); + + // delete organizations where user is only member + for await (const membershipOrg of membershipOrgs) { + const memberCount = await MembershipOrg.countDocuments({ + organization: membershipOrg.organization + }); + + if (memberCount === 1) { + // organization only has 1 member (the current user) + + await deleteOrganization({ + organizationId: membershipOrg.organization + }); + } } - try { - const user = await User.findByIdAndDelete(userId, { - session + const memberships = await Membership.find({ + user: userId + }); + + // delete workspaces where user is only member + for await (const membership of memberships) { + const memberCount = await Membership.countDocuments({ + workspace: membership.workspace }); - if (!user) throw ResourceNotFoundError(); + if (memberCount === 1) { + // workspace only has 1 member (the current user) -> delete workspace - await checkDeleteUserConditions({ - userId: user._id - }); - - await UserAction.deleteMany({ - user: user._id - }, { - session - }); - - await BackupPrivateKey.deleteMany({ - user: user._id - }, { - session - }); - - await APIKeyData.deleteMany({ - user: user._id - }, { - session - }); - - await Action.deleteMany({ - user: user._id - }, { - session - }); - - await Log.deleteMany({ - user: user._id - }, { - session - }); - - await TokenVersion.deleteMany({ - user: user._id - }); - - await Key.deleteMany({ - receiver: user._id - }, { - session - }); - - const membershipOrgs = await MembershipOrg.find({ - user: userId - }, null, { - session - }); - - // delete organizations where user is only member - for await (const membershipOrg of membershipOrgs) { - const memberCount = await MembershipOrg.countDocuments({ - organization: membershipOrg.organization + await deleteWorkspace({ + workspaceId: membership.workspace }); - - if (memberCount === 1) { - // organization only has 1 member (the current user) - - await deleteOrganization({ - organizationId: membershipOrg.organization, - existingSession: session - }); - } - } - - const memberships = await Membership.find({ - user: userId - }, null, { - session - }); - - // delete workspaces where user is only member - for await (const membership of memberships) { - const memberCount = await Membership.countDocuments({ - workspace: membership.workspace - }); - - if (memberCount === 1) { - // workspace only has 1 member (the current user) -> delete workspace - - await deleteWorkspace({ - workspaceId: membership.workspace, - existingSession: session - }); - } - } - - await MembershipOrg.deleteMany({ - user: userId - }, { - session - }); - - await Membership.deleteMany({ - user: userId - }, { - session - }); - - return user; - } catch (err) { - if (!existingSession) { - await session.abortTransaction(); - } - throw InternalServerError({ - message: "Failed to delete account" - }) - } finally { - if (!existingSession) { - await session.commitTransaction(); - session.endSession(); } } + + await MembershipOrg.deleteMany({ + user: userId + }); + + await Membership.deleteMany({ + user: userId + }); + + return user; } \ No newline at end of file diff --git a/backend/src/helpers/workspace.ts b/backend/src/helpers/workspace.ts index 71d945e78..413b187d1 100644 --- a/backend/src/helpers/workspace.ts +++ b/backend/src/helpers/workspace.ts @@ -1,4 +1,4 @@ -import mongoose, { Types, mongo } from "mongoose"; +import { Types } from "mongoose"; import { Bot, BotKey, @@ -33,8 +33,7 @@ import { import { createBot } from "../helpers/bot"; import { EELicenseService } from "../ee/services"; import { SecretService } from "../services"; -import { - InternalServerError, +import { ResourceNotFoundError } from "../utils/errors"; @@ -102,189 +101,113 @@ export const createWorkspace = async ({ * @param {String} obj.id - id of workspace to delete */ export const deleteWorkspace = async ({ - workspaceId, - existingSession + workspaceId }: { workspaceId: Types.ObjectId; - existingSession?: mongo.ClientSession; }) => { - - let session; - - if (existingSession) { - session = existingSession; - } else { - session = await mongoose.startSession(); - session.startTransaction(); - } + const workspace = await Workspace.findByIdAndDelete(workspaceId); - try { - const workspace = await Workspace.findByIdAndDelete(workspaceId, { session }); - - if (!workspace) throw ResourceNotFoundError(); - - await Membership.deleteMany({ - workspace: workspace._id - }, { - session - }); - - await Key.deleteMany({ - workspace: workspace._id - }, { - session - }); - - await Bot.deleteMany({ - workspace: workspace._id - }, { - session - }); + if (!workspace) throw ResourceNotFoundError(); + + await Membership.deleteMany({ + workspace: workspace._id + }); + + await Key.deleteMany({ + workspace: workspace._id + }); + + await Bot.deleteMany({ + workspace: workspace._id + }); - await BotKey.deleteMany({ - workspace: workspace._id - }, { - session - }); + await BotKey.deleteMany({ + workspace: workspace._id + }); - await SecretBlindIndexData.deleteMany({ - workspace: workspace._id - }, { - session - }); + await SecretBlindIndexData.deleteMany({ + workspace: workspace._id + }); - await Secret.deleteMany({ - workspace: workspace._id - }, { - session - }); - - await SecretVersion.deleteMany({ - workspace: workspace._id - }, { - session - }); + await Secret.deleteMany({ + workspace: workspace._id + }); + + await SecretVersion.deleteMany({ + workspace: workspace._id + }); - await SecretSnapshot.deleteMany({ - workspace: workspace._id - }, { - session - }); + await SecretSnapshot.deleteMany({ + workspace: workspace._id + }); - await SecretImport.deleteMany({ - workspace: workspace._id - }, { - session - }); + await SecretImport.deleteMany({ + workspace: workspace._id + }); - await Folder.deleteMany({ - workspace: workspace._id - }, { - session - }); + await Folder.deleteMany({ + workspace: workspace._id + }); - await FolderVersion.deleteMany({ - workspace: workspace._id - }, { - session - }); + await FolderVersion.deleteMany({ + workspace: workspace._id + }); - await Webhook.deleteMany({ - workspace: workspace._id - }, { - session - }); + await Webhook.deleteMany({ + workspace: workspace._id + }); - await TrustedIP.deleteMany({ - workspace: workspace._id - }, { - session - }); + await TrustedIP.deleteMany({ + workspace: workspace._id + }); - await Tag.deleteMany({ - workspace: workspace._id - }, { - session - }); + await Tag.deleteMany({ + workspace: workspace._id + }); - await IntegrationAuth.deleteMany({ - workspace: workspace._id - }, { - session - }); + await IntegrationAuth.deleteMany({ + workspace: workspace._id + }); - await Integration.deleteMany({ - workspace: workspace._id - }, { - session - }); + await Integration.deleteMany({ + workspace: workspace._id + }); - await ServiceToken.deleteMany({ - workspace: workspace._id - }, { - session - }); + await ServiceToken.deleteMany({ + workspace: workspace._id + }); - await ServiceTokenData.deleteMany({ - workspace: workspace._id - }, { - session - }); + await ServiceTokenData.deleteMany({ + workspace: workspace._id + }); - await ServiceTokenDataV3.deleteMany({ - workspace: workspace._id - }, { - session - }); + await ServiceTokenDataV3.deleteMany({ + workspace: workspace._id + }); - await ServiceTokenDataV3Key.deleteMany({ - workspace: workspace._id - }, { - session - }); + await ServiceTokenDataV3Key.deleteMany({ + workspace: workspace._id + }); - await AuditLog.deleteMany({ - workspace: workspace._id - }, { - session - }); + await AuditLog.deleteMany({ + workspace: workspace._id + }); - await Log.deleteMany({ - workspace: workspace._id - }, { - session - }); + await Log.deleteMany({ + workspace: workspace._id + }); - await Action.deleteMany({ - workspace: workspace._id - }, { - session - }); + await Action.deleteMany({ + workspace: workspace._id + }); - await SecretApprovalPolicy.deleteMany({ - workspace: workspace._id - }, { - session - }); + await SecretApprovalPolicy.deleteMany({ + workspace: workspace._id + }); - await SecretApprovalRequest.deleteMany({ - workspace: workspace._id - }, { - session - }); - - return workspace; - } catch (err) { - if (!existingSession) { - await session.abortTransaction(); - } - throw InternalServerError({ - message: "Failed to delete organization" - }); - } finally { - if (!existingSession) { - await session.commitTransaction(); - session.endSession(); - } - } + await SecretApprovalRequest.deleteMany({ + workspace: workspace._id + }); + + return workspace; }; diff --git a/backend/src/index.ts b/backend/src/index.ts index aa4ac2cd5..c1e0baab9 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -5,6 +5,8 @@ import express from "express"; require("express-async-errors"); import helmet from "helmet"; import cors from "cors"; +import { logger } from "./utils/logging"; +import httpLogger from "pino-http"; import { DatabaseService } from "./services"; import { EELicenseService, GithubSecretScanningService } from "./ee/services"; import { setUpHealthEndpoint } from "./services/health"; @@ -25,8 +27,10 @@ import { users as eeUsersRouter, workspace as eeWorkspaceRouter, roles as v1RoleRouter, - secretApprovalPolicy as v1SecretApprovalPolicy, - secretApprovalRequest as v1SecretApprovalRequest, + secretApprovalPolicy as v1SecretApprovalPolicyRouter, + secretApprovalRequest as v1SecretApprovalRequestRouter, + secretRotation as v1SecretRotation, + secretRotationProvider as v1SecretRotationProviderRouter, secretScanning as v1SecretScanningRouter } from "./ee/routes/v1"; import { apiKeyData as v3apiKeyDataRouter } from "./ee/routes/v3"; @@ -73,7 +77,7 @@ import { workspaces as v3WorkspacesRouter } from "./routes/v3"; import { healthCheck } from "./routes/status"; -import { getLogger } from "./utils/logger"; +// import { getLogger } from "./utils/logger"; import { RouteNotFoundError } from "./utils/errors"; import { requestErrorHandler } from "./middleware/requestErrorHandler"; import { @@ -94,12 +98,20 @@ import path from "path"; let handler: null | any = null; const main = async () => { + const port = await getPort(); + await setup(); await EELicenseService.initGlobalFeatureSet(); const app = express(); app.enable("trust proxy"); + + app.use(httpLogger({ + logger, + autoLogging: false + })); + app.use(express.json()); app.use(express.urlencoded({ extended: false })); app.use(cookieParser()); @@ -164,7 +176,7 @@ const main = async () => { const nextApp = new NextServer({ dev: false, dir: nextJsBuildPath, - port: await getPort(), + port, conf, hostname: "local", customServer: false @@ -184,6 +196,8 @@ const main = async () => { app.use("/api/v1/cloud-products", eeCloudProductsRouter); app.use("/api/v3/api-key", v3apiKeyDataRouter); // new app.use("/api/v3/service-token", v3ServiceTokenDataRouter); // new + app.use("/api/v1/secret-rotation-providers", v1SecretRotationProviderRouter); + app.use("/api/v1/secret-rotations", v1SecretRotation); // v1 routes app.use("/api/v1/signup", v1SignupRouter); @@ -207,9 +221,9 @@ const main = async () => { app.use("/api/v1/webhooks", v1WebhooksRouter); app.use("/api/v1/secret-imports", v1SecretImpsRouter); app.use("/api/v1/roles", v1RoleRouter); - app.use("/api/v1/secret-approvals", v1SecretApprovalPolicy); + app.use("/api/v1/secret-approvals", v1SecretApprovalPolicyRouter); app.use("/api/v1/sso", v1SSORouter); - app.use("/api/v1/secret-approval-requests", v1SecretApprovalRequest); + app.use("/api/v1/secret-approval-requests", v1SecretApprovalRequestRouter); // v2 routes (improvements) app.use("/api/v2/signup", v2SignupRouter); @@ -255,8 +269,8 @@ const main = async () => { app.use(requestErrorHandler); - const server = app.listen(await getPort(), async () => { - (await getLogger("backend-main")).info(`Server started listening at port ${await getPort()}`); + const server = app.listen(port, async () => { + logger.info(`Server started listening at port ${port}`); }); // await createTestUserForDevelopment(); diff --git a/backend/src/integrations/apps.ts b/backend/src/integrations/apps.ts index e1aaf08e2..e67136275 100644 --- a/backend/src/integrations/apps.ts +++ b/backend/src/integrations/apps.ts @@ -911,7 +911,7 @@ const getAppsSupabase = async ({ accessToken }: { accessToken: string }) => { }; /** - * Return list of projects for the Checkly integration + * Return list of accounts for the Checkly integration * @param {Object} obj * @param {String} obj.accessToken - api key for the Checkly API * @returns {Object[]} apps - Сheckly accounts diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts index 5f7d53345..1670651b2 100644 --- a/backend/src/integrations/sync.ts +++ b/backend/src/integrations/sync.ts @@ -2104,7 +2104,7 @@ const syncSecretsSupabase = async ({ }; /** - * Sync/push [secrets] to Checkly app + * Sync/push [secrets] to Checkly app/group * @param {Object} obj * @param {IIntegration} obj.integration - integration details * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) @@ -2121,94 +2121,154 @@ const syncSecretsCheckly = async ({ accessToken: string; appendices?: { prefix: string; suffix: string }; }) => { - let getSecretsRes = ( - await standardRequest.get(`${INTEGRATION_CHECKLY_API_URL}/v1/variables`, { - headers: { - Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - "X-Checkly-Account": integration.appId - } - }) - ).data.reduce( - (obj: any, secret: any) => ({ - ...obj, - [secret.key]: secret.value - }), - {} - ); - getSecretsRes = Object.keys(getSecretsRes).reduce( - ( - result: { - [key: string]: string; - }, - key - ) => { - if ( - (appendices?.prefix !== undefined ? key.startsWith(appendices?.prefix) : true) && - (appendices?.suffix !== undefined ? key.endsWith(appendices?.suffix) : true) - ) { - result[key] = getSecretsRes[key]; - } - return result; - }, - {} - ); + if (integration.targetServiceId) { + // sync secrets to checkly group envars - // add secrets - for await (const key of Object.keys(secrets)) { - if (!(key in getSecretsRes)) { - // case: secret does not exist in checkly - // -> add secret - await standardRequest.post( - `${INTEGRATION_CHECKLY_API_URL}/v1/variables`, - { - key, - value: secrets[key].value - }, - { - headers: { - Authorization: `Bearer ${accessToken}`, - Accept: "application/json", - "Content-Type": "application/json", - "X-Checkly-Account": integration.appId - } - } - ); - } else { - // case: secret exists in checkly - // -> update/set secret - - if (secrets[key] !== getSecretsRes[key]) { - await standardRequest.put( - `${INTEGRATION_CHECKLY_API_URL}/v1/variables/${key}`, - { - value: secrets[key].value - }, - { - headers: { - Authorization: `Bearer ${accessToken}`, - "Content-Type": "application/json", - Accept: "application/json", - "X-Checkly-Account": integration.appId - } - } - ); - } - } - } - - for await (const key of Object.keys(getSecretsRes)) { - if (!(key in secrets)) { - // delete secret - await standardRequest.delete(`${INTEGRATION_CHECKLY_API_URL}/v1/variables/${key}`, { + let getGroupSecretsRes = ( + await standardRequest.get(`${INTEGRATION_CHECKLY_API_URL}/v1/check-groups/${integration.targetServiceId}`, { headers: { Authorization: `Bearer ${accessToken}`, Accept: "application/json", "X-Checkly-Account": integration.appId } - }); + }) + ).data.environmentVariables.reduce( + (obj: any, secret: any) => ({ + ...obj, + [secret.key]: secret.value + }), + {} + ); + + getGroupSecretsRes = Object.keys(getGroupSecretsRes).reduce( + ( + result: { + [key: string]: string; + }, + key + ) => { + if ( + (appendices?.prefix !== undefined ? key.startsWith(appendices?.prefix) : true) && + (appendices?.suffix !== undefined ? key.endsWith(appendices?.suffix) : true) + ) { + result[key] = getGroupSecretsRes[key]; + } + return result; + }, + {} + ); + + const groupEnvironmentVariables = Object.keys(secrets).map(key => ({ + key, + value: secrets[key].value + })); + + await standardRequest.put( + `${INTEGRATION_CHECKLY_API_URL}/v1/check-groups/${integration.targetServiceId}`, + { + environmentVariables: groupEnvironmentVariables + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json", + "X-Checkly-Account": integration.appId + } + } + ); + } else { + // sync secrets to checkly global envars + + let getSecretsRes = ( + await standardRequest.get(`${INTEGRATION_CHECKLY_API_URL}/v1/variables`, { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json", + "X-Checkly-Account": integration.appId + } + }) + ).data.reduce( + (obj: any, secret: any) => ({ + ...obj, + [secret.key]: secret.value + }), + {} + ); + + getSecretsRes = Object.keys(getSecretsRes).reduce( + ( + result: { + [key: string]: string; + }, + key + ) => { + if ( + (appendices?.prefix !== undefined ? key.startsWith(appendices?.prefix) : true) && + (appendices?.suffix !== undefined ? key.endsWith(appendices?.suffix) : true) + ) { + result[key] = getSecretsRes[key]; + } + return result; + }, + {} + ); + + // add secrets + for await (const key of Object.keys(secrets)) { + if (!(key in getSecretsRes)) { + // case: secret does not exist in checkly + // -> add secret + await standardRequest.post( + `${INTEGRATION_CHECKLY_API_URL}/v1/variables`, + { + key, + value: secrets[key].value + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json", + "Content-Type": "application/json", + "X-Checkly-Account": integration.appId + } + } + ); + } else { + // case: secret exists in checkly + // -> update/set secret + + if (secrets[key] !== getSecretsRes[key]) { + await standardRequest.put( + `${INTEGRATION_CHECKLY_API_URL}/v1/variables/${key}`, + { + value: secrets[key].value + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + Accept: "application/json", + "X-Checkly-Account": integration.appId + } + } + ); + } + } } + + for await (const key of Object.keys(getSecretsRes)) { + if (!(key in secrets)) { + // delete secret + await standardRequest.delete(`${INTEGRATION_CHECKLY_API_URL}/v1/variables/${key}`, { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json", + "X-Checkly-Account": integration.appId + } + }); + } + } } }; diff --git a/backend/src/interfaces/middleware/index.ts b/backend/src/interfaces/middleware/index.ts index 5146d25cd..0100d3ff6 100644 --- a/backend/src/interfaces/middleware/index.ts +++ b/backend/src/interfaces/middleware/index.ts @@ -1,39 +1,27 @@ import { Types } from "mongoose"; -import { - IServiceTokenData, - IServiceTokenDataV3, - IUser, -} from "../../models"; -import { - ServiceActor, - ServiceActorV3, - UserActor, - UserAgentType -} from "../../ee/models"; +import { IServiceTokenData, IServiceTokenDataV3, IUser } from "../../models"; +import { ServiceActor, ServiceActorV3, UserActor, UserAgentType } from "../../ee/models"; interface BaseAuthData { - ipAddress: string; - userAgent: string; - userAgentType: UserAgentType; - tokenVersionId?: Types.ObjectId; + ipAddress: string; + userAgent: string; + userAgentType: UserAgentType; + tokenVersionId?: Types.ObjectId; } export interface UserAuthData extends BaseAuthData { - actor: UserActor; - authPayload: IUser; + actor: UserActor; + authPayload: IUser; } export interface ServiceTokenV3AuthData extends BaseAuthData { - actor: ServiceActorV3; - authPayload: IServiceTokenDataV3; + actor: ServiceActorV3; + authPayload: IServiceTokenDataV3; } export interface ServiceTokenAuthData extends BaseAuthData { - actor: ServiceActor; - authPayload: IServiceTokenData; + actor: ServiceActor; + authPayload: IServiceTokenData; } -export type AuthData = - | UserAuthData - | ServiceTokenV3AuthData - | ServiceTokenAuthData; \ No newline at end of file +export type AuthData = UserAuthData | ServiceTokenV3AuthData | ServiceTokenAuthData; diff --git a/backend/src/interfaces/services/SecretService/index.ts b/backend/src/interfaces/services/SecretService/index.ts index a2d9c5bb5..631114a79 100644 --- a/backend/src/interfaces/services/SecretService/index.ts +++ b/backend/src/interfaces/services/SecretService/index.ts @@ -26,7 +26,6 @@ export interface CreateSecretParams { export interface GetSecretsParams { workspaceId: Types.ObjectId; environment: string; - folderId?: string; secretPath: string; authData: AuthData; } diff --git a/backend/src/middleware/requestErrorHandler.ts b/backend/src/middleware/requestErrorHandler.ts index 01b37bf3d..3bcea0d98 100644 --- a/backend/src/middleware/requestErrorHandler.ts +++ b/backend/src/middleware/requestErrorHandler.ts @@ -2,49 +2,45 @@ import * as Sentry from "@sentry/node"; import { ErrorRequestHandler } from "express"; import { TokenExpiredError } from "jsonwebtoken"; import { InternalServerError, UnauthorizedRequestError } from "../utils/errors"; -import { getLogger } from "../utils/logger"; -import RequestError from "../utils/requestError"; +import { logger } from "../utils/logging"; +import RequestError, { mapToPinoLogLevel } from "../utils/requestError"; import { ForbiddenError } from "@casl/ability"; export const requestErrorHandler: ErrorRequestHandler = async ( - error: RequestError | Error, + err: RequestError | Error, req, res, next ) => { if (res.headersSent) return next(); - const logAndCaptureException = async (error: RequestError) => { - (await getLogger("backend-main")).log( - (error).levelName.toLowerCase(), - `${error.stack}\n${error.message}` - ); - - //* Set Sentry user identification if req.user is populated - if (req.user !== undefined && req.user !== null) { - Sentry.setUser({ email: (req.user as any).email }); - } - - Sentry.captureException(error); - }; - - if (error instanceof RequestError) { - if (error instanceof TokenExpiredError) { - error = UnauthorizedRequestError({ stack: error.stack, message: "Token expired" }); - } - await logAndCaptureException((error)); - } else { - if (error instanceof ForbiddenError) { - error = UnauthorizedRequestError({ context: { exception: error.message }, stack: error.stack }) - } else { - error = InternalServerError({ context: { exception: error.message }, stack: error.stack }); - } - - await logAndCaptureException((error)); + let error: RequestError; + + switch (true) { + case err instanceof TokenExpiredError: + error = UnauthorizedRequestError({ stack: err.stack, message: "Token expired" }); + break; + case err instanceof ForbiddenError: + error = UnauthorizedRequestError({ context: { exception: err.message }, stack: err.stack }) + break; + case err instanceof RequestError: + error = err as RequestError; + break; + default: + error = InternalServerError({ context: { exception: err.message }, stack: err.stack }); + break; } + logger[mapToPinoLogLevel(error.level)](error); + + if (req.user) { + Sentry.setUser({ email: (req.user as any).email }); + } + + Sentry.captureException(error); + delete (error).stacktrace // remove stack trace from being sent to client - res.status((error).statusCode).json(error); + res.status((error).statusCode).json(error); // revise json part here next(); }; diff --git a/backend/src/middleware/requireAuth.ts b/backend/src/middleware/requireAuth.ts index aa74b0dc7..929d22fea 100644 --- a/backend/src/middleware/requireAuth.ts +++ b/backend/src/middleware/requireAuth.ts @@ -1,14 +1,9 @@ import jwt from "jsonwebtoken"; import { NextFunction, Request, Response } from "express"; -import { - getAuthAPIKeyPayload, - getAuthSTDPayload, - getAuthSTDV3Payload, - getAuthUserPayload, - validateAuthMode, -} from "../helpers/auth"; import { AuthMode } from "../variables"; import { AuthData } from "../interfaces/middleware"; +import { extractAuthMode, getAuthData } from "../utils/authn/helpers"; +import { UnauthorizedRequestError } from "../utils/errors"; declare module "jsonwebtoken" { export interface UserIDJwtPayload extends jwt.JwtPayload { @@ -32,42 +27,39 @@ const requireAuth = ({ acceptedAuthModes: AuthMode[]; }) => { return async (req: Request, res: Response, next: NextFunction) => { + + // extract auth mode + const { authMode, authTokenValue } = await extractAuthMode({ + headers: req.headers + }); - // validate auth token against accepted auth modes [acceptedAuthModes] - // and return token type [authTokenType] and value [authTokenValue] - const { authMode, authTokenValue } = validateAuthMode({ - headers: req.headers, - acceptedAuthModes, + // validate auth mode + if (!acceptedAuthModes.includes(authMode)) throw UnauthorizedRequestError({ + message: "Failed to authenticate unaccepted authentication mode" }); - let authData: AuthData; + // get auth data / payload + const authData: AuthData = await getAuthData({ + authMode, + authTokenValue, + ipAddress: req.realIP, + userAgent: req.headers["user-agent"] ?? "" + }); switch (authMode) { case AuthMode.SERVICE_TOKEN: - authData = await getAuthSTDPayload({ - req, - authTokenValue, - }); req.serviceTokenData = authData.authPayload; break; - case AuthMode.SERVICE_TOKEN_V3: - authData = await getAuthSTDV3Payload({ - req, - authTokenValue - }); + case AuthMode.SERVICE_ACCESS_TOKEN: + req.serviceTokenData = authData.authPayload; break; case AuthMode.API_KEY: - authData = await getAuthAPIKeyPayload({ - req, - authTokenValue - }); + req.user = authData.authPayload; + break; + case AuthMode.API_KEY_V2: req.user = authData.authPayload; break; case AuthMode.JWT: - authData = await getAuthUserPayload({ - req, - authTokenValue - }); req.user = authData.authPayload; break; } diff --git a/backend/src/models/serviceTokenDataV3.ts b/backend/src/models/serviceTokenDataV3.ts index c9895402f..198b60507 100644 --- a/backend/src/models/serviceTokenDataV3.ts +++ b/backend/src/models/serviceTokenDataV3.ts @@ -25,9 +25,14 @@ export interface IServiceTokenDataV3 extends Document { user: Types.ObjectId; publicKey: string; isActive: boolean; - lastUsed?: Date; - usageCount: number; + refreshTokenLastUsed?: Date; + accessTokenLastUsed?: Date; + refreshTokenUsageCount: number; + accessTokenUsageCount: number; + tokenVersion: number; + isRefreshTokenRotationEnabled: boolean; expiresAt?: Date; + accessTokenTTL: number; scopes: Array; trustedIps: Array; } @@ -57,19 +62,43 @@ const serviceTokenDataV3Schema = new Schema( default: true, required: true }, - lastUsed: { + refreshTokenLastUsed: { type: Date, required: false }, - usageCount: { + accessTokenLastUsed: { + type: Date, + required: false + }, + refreshTokenUsageCount: { type: Number, default: 0, required: true }, - expiresAt: { + accessTokenUsageCount: { + type: Number, + default: 0, + required: true + }, + tokenVersion: { + type: Number, + default: 1, + required: true + }, + isRefreshTokenRotationEnabled: { + type: Boolean, + default: false, + required: true + }, + expiresAt: { // consider revising field name type: Date, required: false, - expires: 0 + // expires: 0 + }, + accessTokenTTL: { // seconds + type: Number, + default: 7200, + required: true }, scopes: { type: [ diff --git a/backend/src/routes/v1/integrationAuth.ts b/backend/src/routes/v1/integrationAuth.ts index e28874788..68d0433b3 100644 --- a/backend/src/routes/v1/integrationAuth.ts +++ b/backend/src/routes/v1/integrationAuth.ts @@ -60,6 +60,14 @@ router.get( integrationAuthController.getIntegrationAuthVercelBranches ); +router.get( + "/:integrationAuthId/checkly/groups", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + integrationAuthController.getIntegrationAuthChecklyGroups +); + router.get( "/:integrationAuthId/qovery/orgs", requireAuth({ diff --git a/backend/src/routes/v3/secrets.ts b/backend/src/routes/v3/secrets.ts index 960094f87..81e0cb4c0 100644 --- a/backend/src/routes/v3/secrets.ts +++ b/backend/src/routes/v3/secrets.ts @@ -7,7 +7,7 @@ import { AuthMode } from "../../variables"; router.get( "/raw", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN] }), secretsController.getSecretsRaw ); @@ -15,7 +15,7 @@ router.get( router.get( "/raw/:secretName", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN] }), requireBlindIndicesEnabled({ locationWorkspaceId: "query" @@ -29,7 +29,7 @@ router.get( router.post( "/raw/:secretName", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN] }), requireBlindIndicesEnabled({ locationWorkspaceId: "body" @@ -43,7 +43,7 @@ router.post( router.patch( "/raw/:secretName", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN] }), requireBlindIndicesEnabled({ locationWorkspaceId: "body" @@ -57,7 +57,7 @@ router.patch( router.delete( "/raw/:secretName", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN] }), requireBlindIndicesEnabled({ locationWorkspaceId: "body" @@ -71,7 +71,7 @@ router.delete( router.get( "/", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN] }), requireBlindIndicesEnabled({ locationWorkspaceId: "query" @@ -83,7 +83,7 @@ router.get( router.post( "/batch", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN] }), requireBlindIndicesEnabled({ locationWorkspaceId: "body" @@ -94,7 +94,7 @@ router.post( router.patch( "/batch", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN] }), requireBlindIndicesEnabled({ locationWorkspaceId: "body" @@ -105,7 +105,7 @@ router.patch( router.delete( "/batch", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN] }), requireBlindIndicesEnabled({ locationWorkspaceId: "body" @@ -116,7 +116,7 @@ router.delete( router.post( "/:secretName", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN] }), requireBlindIndicesEnabled({ locationWorkspaceId: "body" @@ -127,7 +127,7 @@ router.post( router.get( "/:secretName", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN] }), requireBlindIndicesEnabled({ locationWorkspaceId: "query" @@ -138,7 +138,7 @@ router.get( router.patch( "/:secretName", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN] }), requireBlindIndicesEnabled({ locationWorkspaceId: "body" @@ -149,7 +149,7 @@ router.patch( router.delete( "/:secretName", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN] }), requireBlindIndicesEnabled({ locationWorkspaceId: "body" diff --git a/backend/src/services/RedisService.ts b/backend/src/services/RedisService.ts index 23f945bb7..218234a1a 100644 --- a/backend/src/services/RedisService.ts +++ b/backend/src/services/RedisService.ts @@ -1,11 +1,12 @@ import Redis, { Redis as TRedis } from "ioredis"; +import { logger } from "../utils/logging"; let redisClient: TRedis | null; if (process.env.REDIS_URL) { redisClient = new Redis(process.env.REDIS_URL as string); } else { - console.warn("Redis URL not set, skipping Redis initialization."); + logger.warn("Redis URL not set, skipping Redis initialization."); redisClient = null; } diff --git a/backend/src/services/TelemetryService.ts b/backend/src/services/TelemetryService.ts index 3db2fdb89..63e4f0c12 100644 --- a/backend/src/services/TelemetryService.ts +++ b/backend/src/services/TelemetryService.ts @@ -1,5 +1,5 @@ import { PostHog } from "posthog-node"; -import { getLogger } from "../utils/logger"; +import { logger } from "../utils/logging"; import { AuthData } from "../interfaces/middleware"; import { getNodeEnv, @@ -22,13 +22,13 @@ class Telemetry { * Logs telemetry enable/disable notice. */ static logTelemetryMessage = async () => { + if(!(await getTelemetryEnabled())){ - (await getLogger("backend-main")).info([ - "", + [ "To improve, Infisical collects telemetry data about general usage.", "This 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 as we support Infisical as open-source software.", "To opt into telemetry, you can set `TELEMETRY_ENABLED=true` within the environment variables.", - ].join("\n")) + ].forEach(line => logger.info(line)); } } diff --git a/backend/src/services/health.ts b/backend/src/services/health.ts index 5ddfb8171..b9cb90fa2 100644 --- a/backend/src/services/health.ts +++ b/backend/src/services/health.ts @@ -1,10 +1,10 @@ import mongoose from "mongoose"; import { createTerminus } from "@godaddy/terminus"; -import { getLogger } from "../utils/logger"; +import { logger } from "../utils/logging"; export const setUpHealthEndpoint = (server: T) => { const onSignal = async () => { - (await getLogger("backend-main")).info("Server is starting clean-up"); + logger.info("Server is starting clean-up"); return Promise.all([ new Promise((resolve) => { if (mongoose.connection && mongoose.connection.readyState == 1) { diff --git a/backend/src/services/smtp.ts b/backend/src/services/smtp.ts index b067d499a..cf688f801 100644 --- a/backend/src/services/smtp.ts +++ b/backend/src/services/smtp.ts @@ -16,7 +16,7 @@ import { getSmtpSecure, getSmtpUsername, } from "../config"; -import { getLogger } from "../utils/logger"; +import { logger } from "../utils/logging"; export const initSmtp = async () => { const mailOpts: SMTPConnection.Options = { @@ -84,15 +84,14 @@ export const initSmtp = async () => { .then(async () => { Sentry.setUser(null); Sentry.captureMessage("SMTP - Successfully connected"); - (await getLogger("backend-main")).info( - "SMTP - Successfully connected" - ); + logger.info("SMTP - Successfully connected"); }) .catch(async (err) => { Sentry.setUser(null); Sentry.captureException( `SMTP - Failed to connect to ${await getSmtpHost()}:${await getSmtpPort()} \n\t${err}` ); + logger.error(err, `SMTP - Failed to connect to ${await getSmtpHost()}:${await getSmtpPort()}`); }); return transporter; diff --git a/backend/src/utils/auth.ts b/backend/src/utils/auth.ts deleted file mode 100644 index c1c0e6ac5..000000000 --- a/backend/src/utils/auth.ts +++ /dev/null @@ -1,435 +0,0 @@ -import express from "express"; -import passport from "passport"; -import { Types } from "mongoose"; -import { AuthData } from "../interfaces/middleware"; -import { - AuthMethod, - MembershipOrg, - Organization, - ServiceAccount, - ServiceTokenData, - ServiceTokenDataV3, - User -} from "../models"; -import { createToken } from "../helpers/auth"; -import { - getAuthSecret, - getClientIdGitHubLogin, - getClientIdGitLabLogin, - getClientIdGoogleLogin, - getClientSecretGitHubLogin, - getClientSecretGitLabLogin, - getClientSecretGoogleLogin, - getJwtProviderAuthLifetime, - getSiteURL, - getUrlGitLabLogin -} from "../config"; -import { getSSOConfigHelper } from "../ee/helpers/organizations"; -import { InternalServerError, OrganizationNotFoundError } from "./errors"; -import { ACCEPTED, AuthTokenType, INTEGRATION_GITHUB_API_URL, INVITED, MEMBER } from "../variables"; -import { standardRequest } from "../config/request"; - -// eslint-disable-next-line @typescript-eslint/no-var-requires -const GoogleStrategy = require("passport-google-oauth20").Strategy; -// eslint-disable-next-line @typescript-eslint/no-var-requires -const GitHubStrategy = require("passport-github").Strategy; -// eslint-disable-next-line @typescript-eslint/no-var-requires -const GitLabStrategy = require("passport-gitlab2").Strategy; -// eslint-disable-next-line @typescript-eslint/no-var-requires -const { MultiSamlStrategy } = require("@node-saml/passport-saml"); - -/** - * Returns an object containing the id of the authentication data payload - * @param {AuthData} authData - authentication data object - * @returns - */ -const getAuthDataPayloadIdObj = (authData: AuthData) => { - if (authData.authPayload instanceof User) { - return { userId: authData.authPayload._id }; - } - - if (authData.authPayload instanceof ServiceAccount) { - return { serviceAccountId: authData.authPayload._id }; - } - - if (authData.authPayload instanceof ServiceTokenData) { - return { serviceTokenDataId: authData.authPayload._id }; - } - - if (authData.authPayload instanceof ServiceTokenDataV3) { - return { serviceTokenDataId: authData.authPayload._id }; - } -}; - -/** - * Returns an object containing the user associated with the authentication data payload - * @param {AuthData} authData - authentication data object - * @returns - */ -const getAuthDataPayloadUserObj = (authData: AuthData) => { - if (authData.authPayload instanceof User) { - return { user: authData.authPayload._id }; - } - - if (authData.authPayload instanceof ServiceAccount) { - return { user: authData.authPayload.user }; - } - - if (authData.authPayload instanceof ServiceTokenData) { - return { user: authData.authPayload.user }; - } - - if (authData.authPayload instanceof ServiceTokenDataV3) { - return { user: authData.authPayload.user }; - } -} - -const initializePassport = async () => { - const clientIdGoogleLogin = await getClientIdGoogleLogin(); - const clientSecretGoogleLogin = await getClientSecretGoogleLogin(); - const clientIdGitHubLogin = await getClientIdGitHubLogin(); - const clientSecretGitHubLogin = await getClientSecretGitHubLogin(); - const urlGitLab = await getUrlGitLabLogin(); - const clientIdGitLabLogin = await getClientIdGitLabLogin(); - const clientSecretGitLabLogin = await getClientSecretGitLabLogin(); - - if (clientIdGoogleLogin && clientSecretGoogleLogin) { - passport.use(new GoogleStrategy({ - passReqToCallback: true, - clientID: clientIdGoogleLogin, - clientSecret: clientSecretGoogleLogin, - callbackURL: "/api/v1/sso/google", - scope: ["profile", " email"], - }, async ( - req: express.Request, - accessToken: string, - refreshToken: string, - profile: any, - done: any - ) => { - try { - const email = profile.emails[0].value; - - let user = await User.findOne({ - email - }).select("+publicKey"); - - if (!user) { - user = await new User({ - email, - authMethods: [AuthMethod.GOOGLE], - firstName: profile.name.givenName, - lastName: profile.name.familyName - }).save(); - } - - let isLinkingRequired = false; - if (!user.authMethods.includes(AuthMethod.GOOGLE)) { - isLinkingRequired = true; - } - - const isUserCompleted = !!user.publicKey; - const providerAuthToken = createToken({ - payload: { - authTokenType: AuthTokenType.PROVIDER_TOKEN, - userId: user._id.toString(), - email: user.email, - firstName: user.firstName, - lastName: user.lastName, - authMethod: AuthMethod.GOOGLE, - isUserCompleted, - isLinkingRequired, - ...(req.query.state ? { - callbackPort: req.query.state as string - } : {}) - }, - expiresIn: await getJwtProviderAuthLifetime(), - secret: await getAuthSecret(), - }); - - req.isUserCompleted = isUserCompleted; - req.providerAuthToken = providerAuthToken; - done(null, profile); - } catch (err) { - done(null, false); - } - })); - } - - if (clientIdGitHubLogin && clientSecretGitHubLogin) { - passport.use(new GitHubStrategy({ - passReqToCallback: true, - clientID: clientIdGitHubLogin, - clientSecret: clientSecretGitHubLogin, - callbackURL: "/api/v1/sso/github", - scope: ["user:email"] - }, - async (req : express.Request, accessToken : any, refreshToken : any, profile : any, done : any) => { - interface GitHubEmail { - email: string; - primary: boolean; - verified: boolean; - visibility: null | string; - } - - const { data }: { data: GitHubEmail[] } = await standardRequest.get( - `${INTEGRATION_GITHUB_API_URL}/user/emails`, - { - headers: { - Authorization: `Bearer ${accessToken}` - } - } - ); - - const primaryEmail = data.filter((gitHubEmail: GitHubEmail) => gitHubEmail.primary)[0]; - const email = primaryEmail.email; - - let user = await User.findOne({ - email - }).select("+publicKey"); - - if (!user) { - user = await new User({ - email: email, - authMethods: [AuthMethod.GITHUB], - firstName: profile.displayName, - lastName: "" - }).save(); - } - - let isLinkingRequired = false; - if (!user.authMethods.includes(AuthMethod.GITHUB)) { - isLinkingRequired = true; - } - - const isUserCompleted = !!user.publicKey; - const providerAuthToken = createToken({ - payload: { - authTokenType: AuthTokenType.PROVIDER_TOKEN, - userId: user._id.toString(), - email: user.email, - firstName: user.firstName, - lastName: user.lastName, - authMethod: AuthMethod.GITHUB, - isUserCompleted, - isLinkingRequired, - ...(req.query.state ? { - callbackPort: req.query.state as string - } : {}) - }, - expiresIn: await getJwtProviderAuthLifetime(), - secret: await getAuthSecret(), - }); - - req.isUserCompleted = isUserCompleted; - req.providerAuthToken = providerAuthToken; - return done(null, profile); - } - )); - } - - if (urlGitLab && clientIdGitLabLogin && clientSecretGitLabLogin) { - passport.use(new GitLabStrategy({ - passReqToCallback: true, - clientID: clientIdGitLabLogin, - clientSecret: clientSecretGitLabLogin, - callbackURL: "/api/v1/sso/gitlab", - baseURL: urlGitLab - }, - async (req : express.Request, accessToken : any, refreshToken : any, profile : any, done : any) => { - const email = profile.emails[0].value; - - let user = await User.findOne({ - email - }).select("+publicKey"); - - if (!user) { - user = await new User({ - email: email, - authMethods: [AuthMethod.GITLAB], - firstName: profile.displayName, - lastName: "" - }).save(); - } - - let isLinkingRequired = false; - if (!user.authMethods.includes(AuthMethod.GITLAB)) { - isLinkingRequired = true; - } - - const isUserCompleted = !!user.publicKey; - const providerAuthToken = createToken({ - payload: { - authTokenType: AuthTokenType.PROVIDER_TOKEN, - userId: user._id.toString(), - email: user.email, - firstName: user.firstName, - lastName: user.lastName, - authMethod: AuthMethod.GITLAB, - isUserCompleted, - isLinkingRequired, - ...(req.query.state ? { - callbackPort: req.query.state as string - } : {}) - }, - expiresIn: await getJwtProviderAuthLifetime(), - secret: await getAuthSecret(), - }); - - req.isUserCompleted = isUserCompleted; - req.providerAuthToken = providerAuthToken; - return done(null, profile); - } - )); - } - - passport.use("saml", new MultiSamlStrategy( - { - passReqToCallback: true, - getSamlOptions: async (req: any, done: any) => { - const { ssoIdentifier } = req.params; - - const ssoConfig = await getSSOConfigHelper({ - ssoConfigId: new Types.ObjectId(ssoIdentifier) - }); - - interface ISAMLConfig { - callbackUrl: string; - entryPoint: string; - issuer: string; - cert: string; - audience: string; - wantAuthnResponseSigned?: boolean; - } - - const samlConfig: ISAMLConfig = ({ - callbackUrl: `${await getSiteURL()}/api/v1/sso/saml2/${ssoIdentifier}`, - entryPoint: ssoConfig.entryPoint, - issuer: ssoConfig.issuer, - cert: ssoConfig.cert, - audience: await getSiteURL() - }); - - if (ssoConfig.authProvider.toString() === AuthMethod.JUMPCLOUD_SAML.toString()) { - samlConfig.wantAuthnResponseSigned = false; - } - - if (ssoConfig.authProvider.toString() === AuthMethod.AZURE_SAML.toString()) { - if (req.body.RelayState && JSON.parse(req.body.RelayState).spInitiated) { - samlConfig.audience = `spn:${ssoConfig.issuer}`; - } - } - - req.ssoConfig = ssoConfig; - - done(null, samlConfig); - }, - }, - async (req: any, profile: any, done: any) => { - if (!req.ssoConfig.isActive) return done(InternalServerError()); - - const organization = await Organization.findById(req.ssoConfig.organization); - - if (!organization) return done(OrganizationNotFoundError()); - - const email = profile.email; - const firstName = profile.firstName; - const lastName = profile.lastName; - - let user = await User.findOne({ - email - }).select("+publicKey"); - - if (user) { - // if user does not have SAML enabled then update - const hasSamlEnabled = user.authMethods - .some( - (authMethod: AuthMethod) => [ - AuthMethod.OKTA_SAML, - AuthMethod.AZURE_SAML, - AuthMethod.JUMPCLOUD_SAML - ].includes(authMethod) - ); - - if (!hasSamlEnabled) { - await User.findByIdAndUpdate( - user._id, - { - authMethods: [req.ssoConfig.authProvider] - }, - { - new: true - } - ); - } - - let membershipOrg = await MembershipOrg.findOne( - { - user: user._id, - organization: organization._id - } - ); - - if (!membershipOrg) { - membershipOrg = await new MembershipOrg({ - inviteEmail: email, - user: user._id, - organization: organization._id, - role: MEMBER, - status: ACCEPTED - }).save(); - } - - if (membershipOrg.status === INVITED) { - membershipOrg.status = ACCEPTED; - await membershipOrg.save(); - } - } else { - user = await new User({ - email, - authMethods: [req.ssoConfig.authProvider], - firstName, - lastName - }).save(); - - await new MembershipOrg({ - inviteEmail: email, - user: user._id, - organization: organization._id, - role: MEMBER, - status: INVITED - }).save(); - } - - const isUserCompleted = !!user.publicKey; - const providerAuthToken = createToken({ - payload: { - authTokenType: AuthTokenType.PROVIDER_TOKEN, - userId: user._id.toString(), - email: user.email, - firstName, - lastName, - organizationName: organization?.name, - authMethod: req.ssoConfig.authProvider, - isUserCompleted, - ...(req.body.RelayState ? { - callbackPort: JSON.parse(req.body.RelayState).callbackPort as string - } : {}) - }, - expiresIn: await getJwtProviderAuthLifetime(), - secret: await getAuthSecret(), - }); - - req.isUserCompleted = isUserCompleted; - req.providerAuthToken = providerAuthToken; - - done(null, profile); - } - )); -} - -export { - getAuthDataPayloadIdObj, - getAuthDataPayloadUserObj, - initializePassport, -} diff --git a/backend/src/utils/authn/authModeValidators/apiKey.ts b/backend/src/utils/authn/authModeValidators/apiKey.ts new file mode 100644 index 000000000..a1bfd28b8 --- /dev/null +++ b/backend/src/utils/authn/authModeValidators/apiKey.ts @@ -0,0 +1,50 @@ +import { Types } from "mongoose"; +import { + APIKeyData, + IUser, + User +} from "../../../models"; +import { AccountNotFoundError, UnauthorizedRequestError } from "../../errors"; +import bcrypt from "bcrypt"; + +interface ValidateAPIKeyParams { + authTokenValue: string; +} + +export const validateAPIKey = async ({ + authTokenValue +}: ValidateAPIKeyParams) => { + + const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split(".", 3); + + let apiKeyData = await APIKeyData + .findById(TOKEN_IDENTIFIER, "+secretHash +expiresAt") + .populate<{ user: IUser }>("user", "+publicKey"); + + if (!apiKeyData) { + throw UnauthorizedRequestError(); + } else if (apiKeyData?.expiresAt && new Date(apiKeyData.expiresAt) < new Date()) { + // case: API key expired + await APIKeyData.findByIdAndDelete(apiKeyData._id); + throw UnauthorizedRequestError(); + } + + const isMatch = await bcrypt.compare(TOKEN_SECRET, apiKeyData.secretHash); + if (!isMatch) throw UnauthorizedRequestError(); + + apiKeyData = await APIKeyData.findOneAndUpdate({ + _id: new Types.ObjectId(TOKEN_IDENTIFIER), + }, { + lastUsed: new Date(), + }, { + new: true, + }); + + if (!apiKeyData) throw UnauthorizedRequestError(); + + const user = await User.findById(apiKeyData.user).select("+publicKey"); + + if (!user) throw AccountNotFoundError(); + + return user; +} \ No newline at end of file diff --git a/backend/src/utils/authn/authModeValidators/apiKeyV2.ts b/backend/src/utils/authn/authModeValidators/apiKeyV2.ts new file mode 100644 index 000000000..2b57542b9 --- /dev/null +++ b/backend/src/utils/authn/authModeValidators/apiKeyV2.ts @@ -0,0 +1,39 @@ +import jwt from "jsonwebtoken"; +import { APIKeyDataV2, User } from "../../../models"; +import { getAuthSecret } from "../../../config"; +import { AuthTokenType } from "../../../variables"; +import { AccountNotFoundError, UnauthorizedRequestError } from "../../errors"; + +interface ValidateAPIKeyV2Params { + authTokenValue: string; +} + +export const validateAPIKeyV2 = async ({ + authTokenValue +}: ValidateAPIKeyV2Params) => { + + const decodedToken = ( + jwt.verify(authTokenValue, await getAuthSecret()) + ); + + if (decodedToken.authTokenType !== AuthTokenType.API_KEY) throw UnauthorizedRequestError(); + + const apiKeyData = await APIKeyDataV2.findByIdAndUpdate( + decodedToken.apiKeyDataId, + { + lastUsed: new Date(), + $inc: { usageCount: 1 } + }, + { + new: true + } + ); + + if (!apiKeyData) throw UnauthorizedRequestError(); + + const user = await User.findById(apiKeyData.user).select("+publicKey"); + + if (!user) throw AccountNotFoundError(); + + return user; +} diff --git a/backend/src/utils/authn/authModeValidators/index.ts b/backend/src/utils/authn/authModeValidators/index.ts new file mode 100644 index 000000000..0ac4c3c1c --- /dev/null +++ b/backend/src/utils/authn/authModeValidators/index.ts @@ -0,0 +1,5 @@ +export * from "./apiKey"; +export * from "./apiKeyV2"; +export * from "./jwt"; +export * from "./serviceTokenV2"; +export * from "./serviceTokenV3"; \ No newline at end of file diff --git a/backend/src/utils/authn/authModeValidators/jwt.ts b/backend/src/utils/authn/authModeValidators/jwt.ts new file mode 100644 index 000000000..f9f0971fc --- /dev/null +++ b/backend/src/utils/authn/authModeValidators/jwt.ts @@ -0,0 +1,41 @@ +import jwt from "jsonwebtoken"; +import { Types } from "mongoose"; +import { TokenVersion, User } from "../../../models"; +import { getAuthSecret } from "../../../config"; +import { AuthTokenType } from "../../../variables"; +import { AccountNotFoundError, UnauthorizedRequestError } from "../../errors"; + +interface ValidateJWTParams { + authTokenValue: string; +} + +export const validateJWT = async ({ + authTokenValue +}: ValidateJWTParams) => { + + const decodedToken = ( + jwt.verify(authTokenValue, await getAuthSecret()) + ); + + if (decodedToken.authTokenType !== AuthTokenType.ACCESS_TOKEN) throw UnauthorizedRequestError(); + + const tokenVersion = await TokenVersion.findOneAndUpdate({ + _id: new Types.ObjectId(decodedToken.tokenVersionId), + user: decodedToken.userId + }, { + lastUsed: new Date(), + }); + + if (!tokenVersion) throw UnauthorizedRequestError(); + if (decodedToken.accessVersion !== tokenVersion.accessVersion) throw UnauthorizedRequestError(); + + const user = await User.findOne({ + _id: new Types.ObjectId(decodedToken.userId), + }).select("+publicKey"); + + if (!user) throw AccountNotFoundError({ message: "Failed to find user" }); + + if (!user?.publicKey) throw UnauthorizedRequestError({ message: "Failed to authenticate user with partially set up account" }); + + return user; +} diff --git a/backend/src/utils/authn/authModeValidators/serviceTokenV2.ts b/backend/src/utils/authn/authModeValidators/serviceTokenV2.ts new file mode 100644 index 000000000..0ebed5963 --- /dev/null +++ b/backend/src/utils/authn/authModeValidators/serviceTokenV2.ts @@ -0,0 +1,44 @@ +import { Types } from "mongoose"; +import { ServiceTokenData } from "../../../models"; +import { ResourceNotFoundError, UnauthorizedRequestError } from "../../errors"; +import bcrypt from "bcrypt"; + +interface ValidateServiceTokenV2Params { + authTokenValue: string; +} + +export const validateServiceTokenV2 = async ({ + authTokenValue +}: ValidateServiceTokenV2Params) => { + const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split(".", 3); + + const serviceTokenData = await ServiceTokenData + .findById(TOKEN_IDENTIFIER, "+secretHash +expiresAt") + + if (!serviceTokenData) { + throw UnauthorizedRequestError(); + } else if (serviceTokenData?.expiresAt && new Date(serviceTokenData.expiresAt) < new Date()) { + // case: service token expired + await ServiceTokenData.findByIdAndDelete(serviceTokenData._id); + throw UnauthorizedRequestError({ + message: "Failed to authenticate expired service token", + }); + } + + const isMatch = await bcrypt.compare(TOKEN_SECRET, serviceTokenData.secretHash); + if (!isMatch) throw UnauthorizedRequestError(); + + const serviceTokenDataToReturn = await ServiceTokenData + .findOneAndUpdate({ + _id: new Types.ObjectId(TOKEN_IDENTIFIER), + }, { + lastUsed: new Date(), + }, { + new: true, + }) + .select("+encryptedKey +iv +tag") + + if (!serviceTokenDataToReturn) throw ResourceNotFoundError(); + + return serviceTokenDataToReturn; +} \ No newline at end of file diff --git a/backend/src/utils/authn/authModeValidators/serviceTokenV3.ts b/backend/src/utils/authn/authModeValidators/serviceTokenV3.ts new file mode 100644 index 000000000..330f60c1c --- /dev/null +++ b/backend/src/utils/authn/authModeValidators/serviceTokenV3.ts @@ -0,0 +1,64 @@ +import jwt from "jsonwebtoken"; +import { Types } from "mongoose"; +import { ServiceTokenDataV3 } from "../../../models"; +import { getAuthSecret } from "../../../config"; +import { AuthTokenType } from "../../../variables"; +import { UnauthorizedRequestError } from "../../errors"; + +interface ValidateServiceTokenV3Params { + authTokenValue: string; +} + +export const validateServiceTokenV3 = async ({ + authTokenValue +}: ValidateServiceTokenV3Params) => { + const decodedToken = ( + jwt.verify(authTokenValue, await getAuthSecret()) + ); + + if (decodedToken.authTokenType !== AuthTokenType.SERVICE_ACCESS_TOKEN) throw UnauthorizedRequestError(); + + const serviceTokenData = await ServiceTokenDataV3.findOne({ + _id: new Types.ObjectId(decodedToken.serviceTokenDataId), + isActive: true + }); + + if (!serviceTokenData) { + throw UnauthorizedRequestError({ + message: "Failed to authenticate" + }); + } else if (serviceTokenData?.expiresAt && new Date(serviceTokenData.expiresAt) < new Date()) { + // case: service token expired + await ServiceTokenDataV3.findByIdAndUpdate( + serviceTokenData._id, + { + isActive: false + }, + { + new: true + } + ); + + throw UnauthorizedRequestError({ + message: "Failed to authenticate", + }); + } else if (decodedToken.tokenVersion !== serviceTokenData.tokenVersion) { + // TODO: raise alarm + throw UnauthorizedRequestError({ + message: "Failed to authenticate", + }); + } + + await ServiceTokenDataV3.findByIdAndUpdate( + serviceTokenData._id, + { + accessTokenLastUsed: new Date(), + $inc: { accessTokenUsageCount: 1 } + }, + { + new: true + } + ); + + return serviceTokenData; +} \ No newline at end of file diff --git a/backend/src/utils/authn/helpers/authDataExtractors.ts b/backend/src/utils/authn/helpers/authDataExtractors.ts new file mode 100644 index 000000000..60639739e --- /dev/null +++ b/backend/src/utils/authn/helpers/authDataExtractors.ts @@ -0,0 +1,53 @@ +import { AuthData } from "../../../interfaces/middleware"; +import { + ServiceAccount, + ServiceTokenData, + ServiceTokenDataV3, + User +} from "../../../models"; + +/** + * Returns an object containing the id of the authentication data payload + * @param {AuthData} authData - authentication data object + * @returns + */ + export const getAuthDataPayloadIdObj = (authData: AuthData) => { + if (authData.authPayload instanceof User) { + return { userId: authData.authPayload._id }; + } + + if (authData.authPayload instanceof ServiceAccount) { + return { serviceAccountId: authData.authPayload._id }; + } + + if (authData.authPayload instanceof ServiceTokenData) { + return { serviceTokenDataId: authData.authPayload._id }; + } + + if (authData.authPayload instanceof ServiceTokenDataV3) { + return { serviceTokenDataId: authData.authPayload._id }; + } +}; + +/** + * Returns an object containing the user associated with the authentication data payload + * @param {AuthData} authData - authentication data object + * @returns + */ +export const getAuthDataPayloadUserObj = (authData: AuthData) => { + if (authData.authPayload instanceof User) { + return { user: authData.authPayload._id }; + } + + if (authData.authPayload instanceof ServiceAccount) { + return { user: authData.authPayload.user }; + } + + if (authData.authPayload instanceof ServiceTokenData) { + return { user: authData.authPayload.user }; + } + + if (authData.authPayload instanceof ServiceTokenDataV3) { + return { user: authData.authPayload.user }; + } +} \ No newline at end of file diff --git a/backend/src/utils/authn/helpers/index.ts b/backend/src/utils/authn/helpers/index.ts new file mode 100644 index 000000000..064efe133 --- /dev/null +++ b/backend/src/utils/authn/helpers/index.ts @@ -0,0 +1,195 @@ +import { AuthData } from "../../../interfaces/middleware"; +import jwt from "jsonwebtoken"; +import { getAuthSecret } from "../../../config"; +import { ActorType } from "../../../ee/models"; +import { AuthMode, AuthTokenType } from "../../../variables"; +import { UnauthorizedRequestError } from "../../errors"; +import { + validateAPIKey, + validateAPIKeyV2, + validateJWT, + validateServiceTokenV2, + validateServiceTokenV3 +} from "../authModeValidators"; +import { getUserAgentType } from "../../posthog"; + +export * from "./authDataExtractors"; + +interface ExtractAuthModeParams { + headers: { [key: string]: string | string[] | undefined } +} + +interface ExtractAuthModeReturn { + authMode: AuthMode; + authTokenValue: string; +} + +interface GetAuthDataParams { + authMode: AuthMode; + authTokenValue: string; + ipAddress: string; + userAgent: string; +} + +/** + * Returns the recognized authentication mode based on token in [headers]; accepted token types include: + * - SERVICE_TOKEN + * - API_KEY + * - JWT + * - SERVICE_ACCESS_TOKEN (from ST V3) + * - API_KEY_V2 + * @param {Object} params + * @param {Object.} params.headers - The HTTP request headers, usually from Express's `req.headers`. + * @returns {Promise} The derived authentication mode based on the headers. + * @throws {UnauthorizedError} Throws an error if no applicable authMode is found. + */ +export const extractAuthMode = async ({ + headers +}: ExtractAuthModeParams): Promise => { + + const apiKey = headers["x-api-key"] as string; + const authHeader = headers["authorization"] as string; + + if (apiKey) { + return { authMode: AuthMode.API_KEY, authTokenValue: apiKey }; + } + + if (!authHeader) throw UnauthorizedRequestError({ + message: "Failed to authenticate unknown authentication method" + }); + + if (!authHeader.startsWith("Bearer ")) throw UnauthorizedRequestError({ + message: "Failed to authenticate unknown authentication method" + }); + + const authTokenValue = authHeader.slice(7); + + if (authTokenValue.startsWith("st.")) { + return { authMode: AuthMode.SERVICE_TOKEN, authTokenValue }; + } + + const decodedToken = ( + jwt.verify(authTokenValue, await getAuthSecret()) + ); + + switch (decodedToken.authTokenType) { + case AuthTokenType.ACCESS_TOKEN: + return { authMode: AuthMode.JWT, authTokenValue }; + case AuthTokenType.API_KEY: + return { authMode: AuthMode.API_KEY_V2, authTokenValue }; + case AuthTokenType.SERVICE_ACCESS_TOKEN: + return { authMode: AuthMode.SERVICE_ACCESS_TOKEN, authTokenValue }; + default: + throw UnauthorizedRequestError({ + message: "Failed to authenticate unknown authentication method" + }); + } +} + +export const getAuthData = async ({ + authMode, + authTokenValue, + ipAddress, + userAgent +}: GetAuthDataParams): Promise => { + + const userAgentType = getUserAgentType(userAgent); + + switch (authMode) { + case AuthMode.SERVICE_TOKEN: { + const serviceTokenData = await validateServiceTokenV2({ + authTokenValue + }); + + return { + actor: { + type: ActorType.SERVICE, + metadata: { + serviceId: serviceTokenData._id.toString(), + name: serviceTokenData.name + } + }, + authPayload: serviceTokenData, + ipAddress, + userAgent, + userAgentType + } + } + case AuthMode.SERVICE_ACCESS_TOKEN: { + const serviceTokenData = await validateServiceTokenV3({ + authTokenValue + }); + + return { + actor: { + type: ActorType.SERVICE_V3, + metadata: { + serviceId: serviceTokenData._id.toString(), + name: serviceTokenData.name + } + }, + authPayload: serviceTokenData, + ipAddress, + userAgent, + userAgentType + } + } + case AuthMode.API_KEY: { + const user = await validateAPIKey({ + authTokenValue + }); + + return { + actor: { + type: ActorType.USER, + metadata: { + userId: user._id.toString(), + email: user.email + } + }, + authPayload: user, + ipAddress, + userAgent, + userAgentType + } + } + case AuthMode.API_KEY_V2: { + const user = await validateAPIKeyV2({ + authTokenValue + }); + + return { + actor: { + type: ActorType.USER, + metadata: { + userId: user._id.toString(), + email: user.email + } + }, + authPayload: user, + ipAddress, + userAgent, + userAgentType + } + } + case AuthMode.JWT: { + const user = await validateJWT({ + authTokenValue + }); + + return { + actor: { + type: ActorType.USER, + metadata: { + userId: user._id.toString(), + email: user.email + } + }, + authPayload: user, + ipAddress, + userAgent, + userAgentType + } + } + } +} \ No newline at end of file diff --git a/backend/src/utils/authn/passport/github.ts b/backend/src/utils/authn/passport/github.ts new file mode 100644 index 000000000..2f0a9b1ab --- /dev/null +++ b/backend/src/utils/authn/passport/github.ts @@ -0,0 +1,60 @@ +import express from "express"; +import passport from "passport"; +import { + getClientIdGitHubLogin, + getClientSecretGitHubLogin, +} from "../../../config"; +import { standardRequest } from "../../../config/request"; +import { AuthMethod } from "../../../models"; +import { INTEGRATION_GITHUB_API_URL } from "../../../variables"; +import { handleSSOUserTokenFlow } from "./helpers"; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const GitHubStrategy = require("passport-github").Strategy; + +export const initializeGitHubStrategy = async () => { + const clientIdGitHubLogin = await getClientIdGitHubLogin(); + const clientSecretGitHubLogin = await getClientSecretGitHubLogin(); + if (clientIdGitHubLogin && clientSecretGitHubLogin) { + passport.use( + new GitHubStrategy({ + passReqToCallback: true, + clientID: clientIdGitHubLogin, + clientSecret: clientSecretGitHubLogin, + callbackURL: "/api/v1/sso/github", + scope: ["user:email"] + }, async (req : express.Request, accessToken : any, refreshToken : any, profile : any, done : any) => { + interface GitHubEmail { + email: string; + primary: boolean; + verified: boolean; + visibility: null | string; + } + + const { data }: { data: GitHubEmail[] } = await standardRequest.get( + `${INTEGRATION_GITHUB_API_URL}/user/emails`, + { + headers: { + Authorization: `Bearer ${accessToken}` + } + } + ); + + const primaryEmail = data.filter((gitHubEmail: GitHubEmail) => gitHubEmail.primary)[0]; + const email = primaryEmail.email; + + const { isUserCompleted, providerAuthToken } = await handleSSOUserTokenFlow({ + email, + firstName: profile.displayName, + lastName: "", + authMethod: AuthMethod.GITHUB, + callbackPort: req.query.state as string + }); + + req.isUserCompleted = isUserCompleted; + req.providerAuthToken = providerAuthToken; + return done(null, profile); + }) + ); + } +} \ No newline at end of file diff --git a/backend/src/utils/authn/passport/gitlab.ts b/backend/src/utils/authn/passport/gitlab.ts new file mode 100644 index 000000000..22851a450 --- /dev/null +++ b/backend/src/utils/authn/passport/gitlab.ts @@ -0,0 +1,44 @@ +import express from "express"; +import passport from "passport"; +import { + getClientIdGitLabLogin, + getClientSecretGitLabLogin, + getUrlGitLabLogin +} from "../../../config"; +import { AuthMethod } from "../../../models"; +import { handleSSOUserTokenFlow } from "./helpers"; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const GitLabStrategy = require("passport-gitlab2").Strategy; + +export const initializeGitLabStrategy = async () => { + const urlGitLab = await getUrlGitLabLogin(); + const clientIdGitLabLogin = await getClientIdGitLabLogin(); + const clientSecretGitLabLogin = await getClientSecretGitLabLogin(); + + if (urlGitLab && clientIdGitLabLogin && clientSecretGitLabLogin) { + passport.use( + new GitLabStrategy({ + passReqToCallback: true, + clientID: clientIdGitLabLogin, + clientSecret: clientSecretGitLabLogin, + callbackURL: "/api/v1/sso/gitlab", + baseURL: urlGitLab + }, async (req : express.Request, accessToken : any, refreshToken : any, profile : any, done : any) => { + const email = profile.emails[0].value; + + const { isUserCompleted, providerAuthToken } = await handleSSOUserTokenFlow({ + email, + firstName: profile.displayName, + lastName: "", + authMethod: AuthMethod.GITLAB, + callbackPort: req.query.state as string + }); + + req.isUserCompleted = isUserCompleted; + req.providerAuthToken = providerAuthToken; + return done(null, profile); + }) + ); + } +} \ No newline at end of file diff --git a/backend/src/utils/authn/passport/google.ts b/backend/src/utils/authn/passport/google.ts new file mode 100644 index 000000000..126f2f9fb --- /dev/null +++ b/backend/src/utils/authn/passport/google.ts @@ -0,0 +1,48 @@ +import express from "express"; +import passport from "passport"; +import { getClientIdGoogleLogin, getClientSecretGoogleLogin } from "../../../config"; +import { AuthMethod } from "../../../models"; + +import { handleSSOUserTokenFlow } from "./helpers"; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const GoogleStrategy = require("passport-google-oauth20").Strategy; + +export const initializeGoogleStrategy = async () => { + const clientIdGoogleLogin = await getClientIdGoogleLogin(); + const clientSecretGoogleLogin = await getClientSecretGoogleLogin(); + + if (clientIdGoogleLogin && clientSecretGoogleLogin) { + passport.use(new GoogleStrategy({ + passReqToCallback: true, + clientID: clientIdGoogleLogin, + clientSecret: clientSecretGoogleLogin, + callbackURL: "/api/v1/sso/google", + scope: ["profile", " email"], + }, async ( + req: express.Request, + accessToken: string, + refreshToken: string, + profile: any, + done: any + ) => { + try { + const email = profile.emails[0].value; + + const { isUserCompleted, providerAuthToken } = await handleSSOUserTokenFlow({ + email, + firstName: profile.name.givenName, + lastName: profile.name.familyName, + authMethod: AuthMethod.GOOGLE, + callbackPort: req.query.state as string + }); + + req.isUserCompleted = isUserCompleted; + req.providerAuthToken = providerAuthToken; + done(null, profile); + } catch (err) { + done(null, false); + } + })); + } +} \ No newline at end of file diff --git a/backend/src/utils/authn/passport/helpers.ts b/backend/src/utils/authn/passport/helpers.ts new file mode 100644 index 000000000..e8a21c37d --- /dev/null +++ b/backend/src/utils/authn/passport/helpers.ts @@ -0,0 +1,62 @@ +import { + AuthMethod, + User +} from "../../../models"; +import { createToken } from "../../../helpers/auth"; +import { AuthTokenType } from "../../../variables"; +import { getAuthSecret, getJwtProviderAuthLifetime} from "../../../config"; + +interface SSOUserTokenFlowParams { + email: string; + firstName: string; + lastName: string; + authMethod: AuthMethod; + callbackPort?: string; +} + +export const handleSSOUserTokenFlow = async ({ + email, + firstName, + lastName, + authMethod, + callbackPort +}: SSOUserTokenFlowParams) => { + let user = await User.findOne({ + email + }).select("+publicKey"); + + if (!user) { + user = await new User({ + email, + authMethods: [authMethod], + firstName, + lastName + }).save(); + } + + let isLinkingRequired = false; + if (!user.authMethods.includes(authMethod)) { + isLinkingRequired = true; + } + + const isUserCompleted = !!user.publicKey; + const providerAuthToken = createToken({ + payload: { + authTokenType: AuthTokenType.PROVIDER_TOKEN, + userId: user._id.toString(), + email: user.email, + firstName: user.firstName, + lastName: user.lastName, + authMethod, + isUserCompleted, + isLinkingRequired, + ...(callbackPort ? { + callbackPort + } : {}) + }, + expiresIn: await getJwtProviderAuthLifetime(), + secret: await getAuthSecret(), + }); + + return { isUserCompleted, providerAuthToken }; +} \ No newline at end of file diff --git a/backend/src/utils/authn/passport/index.ts b/backend/src/utils/authn/passport/index.ts new file mode 100644 index 000000000..4346c7d67 --- /dev/null +++ b/backend/src/utils/authn/passport/index.ts @@ -0,0 +1,4 @@ +export { initializeGoogleStrategy } from "./google"; +export { initializeGitHubStrategy } from "./github"; +export { initializeGitLabStrategy } from "./gitlab"; +export { initializeSamlStrategy } from "./saml"; diff --git a/backend/src/utils/authn/passport/saml.ts b/backend/src/utils/authn/passport/saml.ts new file mode 100644 index 000000000..f02e78be1 --- /dev/null +++ b/backend/src/utils/authn/passport/saml.ts @@ -0,0 +1,173 @@ +import passport from "passport"; +import { + getAuthSecret, + getJwtProviderAuthLifetime, + getSiteURL +} from "../../../config"; +import { + AuthMethod, + MembershipOrg, + Organization, + User +} from "../../../models"; +import { + createToken +} from "../../../helpers/auth"; +import { + ACCEPTED, + AuthTokenType, + INVITED, + MEMBER +} from "../../../variables"; +import { Types } from "mongoose"; +import { getSSOConfigHelper } from "../../../ee/helpers/organizations"; +import { InternalServerError, OrganizationNotFoundError } from "../../errors"; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const { MultiSamlStrategy } = require("@node-saml/passport-saml"); + +export const initializeSamlStrategy = async () => { + passport.use("saml", new MultiSamlStrategy( + { + passReqToCallback: true, + getSamlOptions: async (req: any, done: any) => { + const { ssoIdentifier } = req.params; + + const ssoConfig = await getSSOConfigHelper({ + ssoConfigId: new Types.ObjectId(ssoIdentifier) + }); + + interface ISAMLConfig { + callbackUrl: string; + entryPoint: string; + issuer: string; + cert: string; + audience: string; + wantAuthnResponseSigned?: boolean; + } + + const samlConfig: ISAMLConfig = ({ + callbackUrl: `${await getSiteURL()}/api/v1/sso/saml2/${ssoIdentifier}`, + entryPoint: ssoConfig.entryPoint, + issuer: ssoConfig.issuer, + cert: ssoConfig.cert, + audience: await getSiteURL() + }); + + if (ssoConfig.authProvider.toString() === AuthMethod.JUMPCLOUD_SAML.toString()) { + samlConfig.wantAuthnResponseSigned = false; + } + + if (ssoConfig.authProvider.toString() === AuthMethod.AZURE_SAML.toString()) { + if (req.body.RelayState && JSON.parse(req.body.RelayState).spInitiated) { + samlConfig.audience = `spn:${ssoConfig.issuer}`; + } + } + + req.ssoConfig = ssoConfig; + + done(null, samlConfig); + }, + }, + async (req: any, profile: any, done: any) => { + if (!req.ssoConfig.isActive) return done(InternalServerError()); + + const organization = await Organization.findById(req.ssoConfig.organization); + + if (!organization) return done(OrganizationNotFoundError()); + + const email = profile.email; + const firstName = profile.firstName; + const lastName = profile.lastName; + + let user = await User.findOne({ + email + }).select("+publicKey"); + + if (user) { + // if user does not have SAML enabled then update + const hasSamlEnabled = user.authMethods + .some( + (authMethod: AuthMethod) => [ + AuthMethod.OKTA_SAML, + AuthMethod.AZURE_SAML, + AuthMethod.JUMPCLOUD_SAML + ].includes(authMethod) + ); + + if (!hasSamlEnabled) { + await User.findByIdAndUpdate( + user._id, + { + authMethods: [req.ssoConfig.authProvider] + }, + { + new: true + } + ); + } + + let membershipOrg = await MembershipOrg.findOne( + { + user: user._id, + organization: organization._id + } + ); + + if (!membershipOrg) { + membershipOrg = await new MembershipOrg({ + inviteEmail: email, + user: user._id, + organization: organization._id, + role: MEMBER, + status: ACCEPTED + }).save(); + } + + if (membershipOrg.status === INVITED) { + membershipOrg.status = ACCEPTED; + await membershipOrg.save(); + } + } else { + user = await new User({ + email, + authMethods: [req.ssoConfig.authProvider], + firstName, + lastName + }).save(); + + await new MembershipOrg({ + inviteEmail: email, + user: user._id, + organization: organization._id, + role: MEMBER, + status: INVITED + }).save(); + } + + const isUserCompleted = !!user.publicKey; + const providerAuthToken = createToken({ + payload: { + authTokenType: AuthTokenType.PROVIDER_TOKEN, + userId: user._id.toString(), + email: user.email, + firstName, + lastName, + organizationName: organization?.name, + authMethod: req.ssoConfig.authProvider, + isUserCompleted, + ...(req.body.RelayState ? { + callbackPort: JSON.parse(req.body.RelayState).callbackPort as string + } : {}) + }, + expiresIn: await getJwtProviderAuthLifetime(), + secret: await getAuthSecret(), + }); + + req.isUserCompleted = isUserCompleted; + req.providerAuthToken = providerAuthToken; + + done(null, profile); + } + )); +} \ No newline at end of file diff --git a/backend/src/utils/errors.ts b/backend/src/utils/errors.ts index 42cc20509..1751dcd92 100644 --- a/backend/src/utils/errors.ts +++ b/backend/src/utils/errors.ts @@ -29,7 +29,7 @@ export const UnauthorizedRequestError = (error?: Partial) = }); export const ForbiddenRequestError = (error?: Partial) => new RequestError({ - logLevel: error?.logLevel ?? LogLevel.INFO, + logLevel: error?.logLevel ?? LogLevel.WARN, statusCode: error?.statusCode ?? 403, type: error?.type ?? "forbidden", message: error?.message ?? "You are not allowed to access this resource", diff --git a/backend/src/utils/logger.ts b/backend/src/utils/logger.ts deleted file mode 100644 index 9673f1115..000000000 --- a/backend/src/utils/logger.ts +++ /dev/null @@ -1,67 +0,0 @@ -/* eslint-disable no-console */ -import { createLogger, format, transports } from "winston"; -import LokiTransport from "winston-loki"; -import { getLokiHost, getNodeEnv } 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 = async (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((await getLokiHost()) !== undefined){ - _transports.push( - new LokiTransport({ - host: await getLokiHost(), - 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: await getNodeEnv(), - }, - 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"] }) - ), - }); -} - -export const getLogger = async (loggerName: "backend-main" | "database") => { - const logger = { - "backend-main": await createLoggerWithLabel("info", "[IFSC:backend-main]"), - "database": await createLoggerWithLabel("info", "[IFSC:database]"), - } - return logger[loggerName] -} diff --git a/backend/src/utils/logging/index.ts b/backend/src/utils/logging/index.ts new file mode 100644 index 000000000..ef89344c7 --- /dev/null +++ b/backend/src/utils/logging/index.ts @@ -0,0 +1 @@ +export { logger } from "./logger"; \ No newline at end of file diff --git a/backend/src/utils/logging/logger.ts b/backend/src/utils/logging/logger.ts new file mode 100644 index 000000000..ecfe24b69 --- /dev/null +++ b/backend/src/utils/logging/logger.ts @@ -0,0 +1,15 @@ +import pino from "pino"; + +export const logger = pino({ + level: process.env.PINO_LOG_LEVEL || "trace", + timestamp: pino.stdTimeFunctions.isoTime, + formatters: { + bindings: (bindings) => { + return { + pid: bindings.pid, + hostname: bindings.hostname + // node_version: process.version + }; + }, + } +}); \ No newline at end of file diff --git a/backend/src/utils/requestError.ts b/backend/src/utils/requestError.ts index 73d29dbb8..f5f320b8d 100644 --- a/backend/src/utils/requestError.ts +++ b/backend/src/utils/requestError.ts @@ -2,34 +2,30 @@ import { Request } from "express" import { getVerboseErrorOutput } from "../config"; export enum LogLevel { - DEBUG = 100, - INFO = 200, - NOTICE = 250, - WARNING = 300, - ERROR = 400, - CRITICAL = 500, - ALERT = 550, - EMERGENCY = 600, + TRACE = 10, + DEBUG = 20, + INFO = 30, + WARN = 40, + ERROR = 50, + FATAL = 60 } -export const mapToWinstonLogLevel = (customLogLevel: LogLevel): string => { +type PinoLogLevel = "trace" | "debug" | "info" | "warn" | "error" | "fatal"; + +export const mapToPinoLogLevel = (customLogLevel: LogLevel): PinoLogLevel => { switch (customLogLevel) { + case LogLevel.TRACE: + return "trace"; case LogLevel.DEBUG: return "debug"; case LogLevel.INFO: return "info"; - case LogLevel.NOTICE: - return "notice"; - case LogLevel.WARNING: + case LogLevel.WARN: return "warn"; case LogLevel.ERROR: return "error"; - case LogLevel.CRITICAL: - return "crit"; - case LogLevel.ALERT: - return "alert"; - case LogLevel.EMERGENCY: - return "emerg"; + case LogLevel.FATAL: + return "fatal"; } } @@ -42,10 +38,10 @@ export type RequestErrorContext = { stack?: string|undefined } -export default class RequestError extends Error{ +export default class RequestError extends Error { private _logLevel: LogLevel - private _logName: string + private _logName: string; statusCode: number type: string context: Record @@ -55,9 +51,10 @@ export default class RequestError extends Error{ constructor( {logLevel, statusCode, type, message, context, stack} : RequestErrorContext ){ + super(message) this._logLevel = logLevel || LogLevel.INFO - this._logName = LogLevel[this._logLevel] + this._logName = LogLevel[this._logLevel]; this.statusCode = statusCode this.type = type this.context = context || {} @@ -83,8 +80,12 @@ export default class RequestError extends Error{ }) } - get level(){ return this._logLevel } - get levelName(){ return this._logName } + get level(){ + return this._logLevel + } + get levelName(){ + return this._logName + } withTags(...tags: string[]|number[]){ this.context["tags"] = Object.assign(tags, this.context["tags"]) diff --git a/backend/src/utils/setup/backfillData.ts b/backend/src/utils/setup/backfillData.ts index 3339d8927..319c2605b 100644 --- a/backend/src/utils/setup/backfillData.ts +++ b/backend/src/utils/setup/backfillData.ts @@ -1,4 +1,3 @@ -/* eslint-disable no-console */ import crypto from "crypto"; import { Types } from "mongoose"; import { encryptSymmetric128BitHexKeyUTF8 } from "../crypto"; @@ -47,6 +46,7 @@ import { ProjectPermissionSub, memberProjectPermissions } from "../../ee/services/ProjectRoleService"; +import { logger } from "../logging"; /** * Backfill secrets to ensure that they're all versioned and have @@ -88,7 +88,7 @@ export const backfillSecretVersions = async () => { ) }); } - console.log("Migration: Secret version migration v1 complete"); + logger.info("Migration: Secret version migration v1 complete"); }; /** @@ -518,7 +518,7 @@ export const backfillSecretFolders = async () => { .limit(50); } - console.log("Migration: Folder migration v1 complete"); + logger.info("Migration: Folder migration v1 complete"); }; export const backfillServiceToken = async () => { @@ -534,7 +534,7 @@ export const backfillServiceToken = async () => { } } ); - console.log("Migration: Service token migration v1 complete"); + logger.info("Migration: Service token migration v1 complete"); }; export const backfillIntegration = async () => { @@ -550,7 +550,7 @@ export const backfillIntegration = async () => { } } ); - console.log("Migration: Integration migration v1 complete"); + logger.info("Migration: Integration migration v1 complete"); }; export const backfillServiceTokenMultiScope = async () => { @@ -575,7 +575,7 @@ export const backfillServiceTokenMultiScope = async () => { } } - console.log("Migration: Service token migration v2 complete"); + logger.info("Migration: Service token migration v2 complete"); }; /** @@ -650,7 +650,7 @@ export const backfillTrustedIps = async () => { }); await TrustedIP.bulkWrite(operations); - console.log("Backfill: Trusted IPs complete"); + logger.info("Backfill: Trusted IPs complete"); } }; @@ -698,7 +698,7 @@ export const backfillPermission = async () => { if (lock) { try { - console.info("Lock acquired for script [backfillPermission]"); + logger.info("Lock acquired for script [backfillPermission]"); const memberships = await Membership.find({ deniedPermissions: { @@ -801,7 +801,7 @@ export const backfillPermission = async () => { } } - console.info("Backfill: Finished converting old denied permission in workspace to viewers"); + logger.info("Backfill: Finished converting old denied permission in workspace to viewers"); await MembershipOrg.updateMany( { @@ -814,14 +814,14 @@ export const backfillPermission = async () => { } ); - console.info("Backfill: Finished converting owner role to member"); + logger.info("Backfill: Finished converting owner role to member"); } catch (error) { - console.error("An error occurred when running script [backfillPermission]:", error); + logger.error(error, "An error occurred when running script [backfillPermission]"); } } else { - console.info("Could not acquire lock for script [backfillPermission], skipping"); + logger.info("Could not acquire lock for script [backfillPermission], skipping"); } }; @@ -837,5 +837,5 @@ export const migrateRoleFromOwnerToAdmin = async () => { } ); - console.info("Backfill: Finished converting owner role to member"); + logger.info("Backfill: Finished converting owner role to member"); } \ No newline at end of file diff --git a/backend/src/utils/setup/index.ts b/backend/src/utils/setup/index.ts index 6625bff7b..b9fe39992 100644 --- a/backend/src/utils/setup/index.ts +++ b/backend/src/utils/setup/index.ts @@ -11,7 +11,6 @@ import { backfillBots, backfillEncryptionMetadata, backfillIntegration, - backfillPermission, backfillSecretBlindIndexData, backfillSecretFolders, backfillSecretVersions, @@ -27,7 +26,13 @@ import { reencryptSecretBlindIndexDataSalts } from "./reencryptData"; import { getMongoURL, getNodeEnv, getRedisUrl, getSentryDSN } from "../../config"; -import { initializePassport } from "../auth"; +import { + initializeGitHubStrategy, + initializeGitLabStrategy, + initializeGoogleStrategy, + initializeSamlStrategy +} from "../authn/passport"; +import { logger } from "../logging"; /** * Prepare Infisical upon startup. This includes tasks like: @@ -41,7 +46,7 @@ import { initializePassport } from "../auth"; */ export const setup = async () => { if ((await getRedisUrl()) === undefined || (await getRedisUrl()) === "") { - console.error( + logger.error( "WARNING: Redis is not yet configured. Infisical may not function as expected without it." ); } @@ -55,7 +60,11 @@ export const setup = async () => { // initializing global feature set await EELicenseService.initGlobalFeatureSet(); - await initializePassport(); + // initializing auth strategies + await initializeGoogleStrategy(); + await initializeGitHubStrategy() + await initializeGitLabStrategy(); + await initializeSamlStrategy(); // re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY // to base64 256-bit ROOT_ENCRYPTION_KEY diff --git a/backend/src/validation/integrationAuth.ts b/backend/src/validation/integrationAuth.ts index 65eee4477..3eb51d833 100644 --- a/backend/src/validation/integrationAuth.ts +++ b/backend/src/validation/integrationAuth.ts @@ -117,6 +117,15 @@ export const GetIntegrationAuthVercelBranchesV1 = z.object({ }) }); +export const GetIntegrationAuthChecklyGroupsV1 = z.object({ + params: z.object({ + integrationAuthId: z.string().trim() + }), + query: z.object({ + accountId: z.string().trim() + }) +}); + export const GetIntegrationAuthQoveryOrgsV1 = z.object({ params: z.object({ integrationAuthId: z.string().trim() diff --git a/backend/src/validation/secrets.ts b/backend/src/validation/secrets.ts index 174ffa791..799e9531d 100644 --- a/backend/src/validation/secrets.ts +++ b/backend/src/validation/secrets.ts @@ -228,7 +228,6 @@ export const GetSecretsRawV3 = z.object({ workspaceId: z.string().trim().optional(), environment: z.string().trim().optional(), secretPath: z.string().trim().default("/"), - folderId: z.string().trim().optional(), include_imports: z .enum(["true", "false"]) .default("false") @@ -302,7 +301,6 @@ export const GetSecretsV3 = z.object({ workspaceId: z.string().trim(), environment: z.string().trim(), secretPath: z.string().trim().default("/"), - folderId: z.string().trim().optional(), include_imports: z .enum(["true", "false"]) .default("false") diff --git a/backend/src/validation/serviceTokenDataV3.ts b/backend/src/validation/serviceTokenDataV3.ts index 976f1a1e5..4be60c645 100644 --- a/backend/src/validation/serviceTokenDataV3.ts +++ b/backend/src/validation/serviceTokenDataV3.ts @@ -60,6 +60,12 @@ import { checkIPAgainstBlocklist } from "../utils/ip"; } }; +export const RefreshTokenV3 = z.object({ + body: z.object({ + refresh_token: z.string().trim() + }) +}); + export const CreateServiceTokenV3 = z.object({ body: z.object({ name: z.string().trim(), @@ -80,8 +86,10 @@ export const CreateServiceTokenV3 = z.object({ .array() .min(1), expiresIn: z.number().optional(), + accessTokenTTL: z.number().int().min(1), encryptedKey: z.string().trim(), - nonce: z.string().trim() + nonce: z.string().trim(), + isRefreshTokenRotationEnabled: z.boolean().default(false) }) }); @@ -108,7 +116,9 @@ export const UpdateServiceTokenV3 = z.object({ .array() .min(1) .optional(), - expiresIn: z.number().optional() + expiresIn: z.number().optional(), + accessTokenTTL: z.number().int().min(1).optional(), + isRefreshTokenRotationEnabled: z.boolean().optional() }), }); diff --git a/backend/src/variables/authentication.ts b/backend/src/variables/authentication.ts index 30ba4bf14..bdad84ff6 100644 --- a/backend/src/variables/authentication.ts +++ b/backend/src/variables/authentication.ts @@ -1,17 +1,22 @@ +// TODO: merge [AuthTokenType] and [AuthMode] + export enum AuthTokenType { ACCESS_TOKEN = "accessToken", REFRESH_TOKEN = "refreshToken", - SIGNUP_TOKEN = "signupToken", - MFA_TOKEN = "mfaToken", - PROVIDER_TOKEN = "providerToken", - API_KEY = "apiKey" + SIGNUP_TOKEN = "signupToken", // TODO: remove in favor of claim + MFA_TOKEN = "mfaToken", // TODO: remove in favor of claim + PROVIDER_TOKEN = "providerToken", // TODO: remove in favor of claim + API_KEY = "apiKey", + SERVICE_ACCESS_TOKEN = "serviceAccessToken", + SERVICE_REFRESH_TOKEN = "serviceRefreshToken" } export enum AuthMode { JWT = "jwt", SERVICE_TOKEN = "serviceToken", - SERVICE_TOKEN_V3 = "serviceTokenV3", - API_KEY = "apiKey" + SERVICE_ACCESS_TOKEN = "serviceAccessToken", + API_KEY = "apiKey", + API_KEY_V2 = "apiKeyV2" } export const K8_USER_AGENT_NAME = "k8-operator" \ No newline at end of file diff --git a/docs/documentation/platform/secret-rotation/mysql.mdx b/docs/documentation/platform/secret-rotation/mysql.mdx new file mode 100644 index 000000000..b630e349a --- /dev/null +++ b/docs/documentation/platform/secret-rotation/mysql.mdx @@ -0,0 +1,37 @@ +--- +title: "MySQL/MariaDB" +description: "Rotated database user password of a MySQL or MariaDB" +--- + +Infisical will update periodically the provided database user's password. + + + At present Infisical do require access to your database. We will soon be released Infisical agent based rotation which would help you rotate without direct database access from Infisical cloud. + + +## Working + +1. User's has to create the two user's for Infisical to rotate and provide them required database access +2. Infisical will connect with your database with admin access +3. If last rotated one was username1, then username2 is chosen to be rotated +5. Update it's password with random value +6. After testing it gets saved to the provided secret mapping + +## Rotation Configuration + +1. Head over to Secret Rotation configuration page of your project by clicking on side bar `Secret Rotation` +2. Click on `MySQL` +3. Provide the inputs + - Admin Username: DB admin username + - Admin Password: DB admin password + - Host: DB host + - Port: DB port(number) + - Username1: The first username in two to rotate + - Username2: The second username in two to rotate + - CA: Certificate to connect with database(string) +4. Final step + - Select `Environment`, `Secret Path` and `Interval` to rotate the secrets + - Finally select the secrets in your provided board to replace with new secret after each rotation + - Your done and good to go. + +Congrats. You have 10x your MySQL/MariaDB access security. diff --git a/docs/documentation/platform/secret-rotation/overview.mdx b/docs/documentation/platform/secret-rotation/overview.mdx new file mode 100644 index 000000000..284375401 --- /dev/null +++ b/docs/documentation/platform/secret-rotation/overview.mdx @@ -0,0 +1,44 @@ +# Secret Rotation Overview + +## Introduction + +Secret rotation is a process that involves updating secret credentials periodically to minimize the risk of their compromise. +Rotating secrets helps prevent unauthorized access to systems and sensitive data by ensuring that old credentials are replaced with new ones regularly. + +Rotated secrets may include, but are not limited to: + +1. API keys for external services +2. Database credentials for various platforms + +## Rotation Process + +The practice of rotating secrets is a systematic and interval-based operation, carried out in four fundamental phases. + +### 1. Creation + +The system initiates the rotation process by either making an API call to an external service or generating a new secret value internally. +Upon successful creation, the system will temporarily have three versions of the secret: + +- **Current active secret**: The one currently in use. +- **Future active secret (pending)**: The newly created secret, awaiting validation. +- **Previous active secret**: The old secret, soon to be retired. + +### 2. Testing + +The newly generated secret is subjected to a verification process to ensure its validity and functionality. +This involves conducting checks or tests that simulate actual operations the secret would perform. +Only the current active and the future active (pending) secrets are considered operational at this stage, while the previous active secret remains in standby mode. + +### 3. Deletion + +Post-verification, the system deactivates and deletes the previous active secret, leaving only the current and future active (pending) secrets in the system. + +### 4. Activation + +Finally, the system promotes the future active (pending) secret to be the new current active secret. It then triggers necessary side effects, such as invoking webhooks and generating events, to notify other services of the change. + +## Infisical Secret Rotation Strategies + +1. [SendGrid Integration](./sendgrid) +2. [PostgreSQL/CockroachDB Implementation](./postgres) +3. [MySQL/MariaDB Configuration](./mysql) diff --git a/docs/documentation/platform/secret-rotation/postgres.mdx b/docs/documentation/platform/secret-rotation/postgres.mdx new file mode 100644 index 000000000..167b60fcd --- /dev/null +++ b/docs/documentation/platform/secret-rotation/postgres.mdx @@ -0,0 +1,37 @@ +--- +title: "PostgreSQL/CockroachDB" +description: "Rotated database user password of a postgreSQL or cochroach db" +--- + +Infisical will update periodically the provided database user's password. + + + At present Infisical do require access to your database. We will soon be released Infisical agent based rotation which would help you rotate without direct database access from Infisical cloud. + + +## Working + +1. User's has to create the two user's for Infisical to rotate and provide them required database access +2. Infisical will connect with your database with admin access +3. If last rotated one was username1, then username2 is chosen to be rotated +5. Update it's password with random value +6. After testing it gets saved to the provided secret mapping + +## Rotation Configuration + +1. Head over to Secret Rotation configuration page of your project by clicking on side bar `Secret Rotation` +2. Click on `PostgreSQL` +3. Provide the inputs + - Admin Username: DB admin username + - Admin Password: DB admin password + - Host: DB host + - Port: DB port(number) + - Username1: The first username in two to rotate + - Username2: The second username in two to rotate + - CA: Certificate to connect with database(string) +4. Final step + - Select `Environment`, `Secret Path` and `Interval` to rotate the secrets + - Finally select the secrets in your provided board to replace with new secret after each rotation + - Your done and good to go. + +Congrats. You have 10x your PostgreSQL/CockroachDB access security. diff --git a/docs/documentation/platform/secret-rotation/sendgrid.mdx b/docs/documentation/platform/secret-rotation/sendgrid.mdx new file mode 100644 index 000000000..c4dd2797f --- /dev/null +++ b/docs/documentation/platform/secret-rotation/sendgrid.mdx @@ -0,0 +1,31 @@ +--- +title: "Twilio SendGrid" +description: "Rotate Twilio SendGrid API keys" +--- + +Twilio SendGrid is a cloud-based email delivery platform that helps businesses send transactional and marketing emails. +It uses an API key to do various operations. Using Infisical you can easily dynamically change the keys. + +## Working + +1. Infisical will need an admin token of SendGrid to create API keys dynamically. +2. Using the given admin token and scope by user Infisical will create and rotate API keys periodically +3. Under the hood infisical uses [SendGrid API](https://docs.sendgrid.com/api-reference/api-keys/create-api-keys) + +## Rotation Configuration + +1. Head over to Secret Rotation configuration page of your project by clicking on side bar `Secret Rotation` +2. Click on `Twilio SendGrid Card` +3. Provide the inputs + - Admin API Key: + SendGrid admin key to create lower scoped API keys. + - API Key Scopes + SendGrid generated API Key's scopes. For more info refer [this doc](https://docs.sendgrid.com/api-reference/api-key-permissions/api-key-permissions) + +4. Final step + - Select `Environment`, `Secret Path` and `Interval` to rotate the secrets + - Finally select the secrets in your provided board to replace with new secret after each rotation + - Your done and good to go. + +Now your output mapped secret value will be replaced periodically by SendGrid. + diff --git a/docs/images/integrations/checkly/integrations-checkly-auth.png b/docs/images/integrations/checkly/integrations-checkly-auth.png index b6299aefe..b9fbd4404 100644 Binary files a/docs/images/integrations/checkly/integrations-checkly-auth.png and b/docs/images/integrations/checkly/integrations-checkly-auth.png differ diff --git a/docs/images/integrations/checkly/integrations-checkly-create.png b/docs/images/integrations/checkly/integrations-checkly-create.png index 271ca1a9c..78faa5eac 100644 Binary files a/docs/images/integrations/checkly/integrations-checkly-create.png and b/docs/images/integrations/checkly/integrations-checkly-create.png differ diff --git a/docs/images/integrations/checkly/integrations-checkly.png b/docs/images/integrations/checkly/integrations-checkly.png index 4734d5257..9dcb4e497 100644 Binary files a/docs/images/integrations/checkly/integrations-checkly.png and b/docs/images/integrations/checkly/integrations-checkly.png differ diff --git a/docs/integrations/cloud/checkly.mdx b/docs/integrations/cloud/checkly.mdx index 711c8cec9..6b4709be9 100644 --- a/docs/integrations/cloud/checkly.mdx +++ b/docs/integrations/cloud/checkly.mdx @@ -34,6 +34,13 @@ Press on the Checkly tile and input your Checkly API Key to grant Infisical acce Select which Infisical environment secrets you want to sync to Checkly and press create integration to start syncing secrets. ![integrations checkly](../../images/integrations/checkly/integrations-checkly-create.png) + + + Infisical integrates with Checkly's environment variables at the **global** and **group** levels. + + To sync secrets to a specific group, you can select a group from the Checkly Group dropdown; otherwise, leaving it empty will sync secrets globally. + + ![integrations checkly](../../images/integrations/checkly/integrations-checkly.png) diff --git a/docs/mint.json b/docs/mint.json index 92876198d..05904b56b 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -34,7 +34,10 @@ } }, "topbarLinks": [ - { "name": "Log In", "url": "https://app.infisical.com/login" } + { + "name": "Log In", + "url": "https://app.infisical.com/login" + } ], "topbarCtaButton": { "name": "Start for Free", @@ -120,6 +123,15 @@ "documentation/platform/audit-logs", "documentation/platform/token", "documentation/platform/mfa", + { + "group": "Secret Rotation", + "pages": [ + "documentation/platform/secret-rotation/overview", + "documentation/platform/secret-rotation/sendgrid", + "documentation/platform/secret-rotation/postgres", + "documentation/platform/secret-rotation/mysql" + ] + }, { "group": "SSO", "pages": [ diff --git a/docs/self-hosting/configuration/envars.mdx b/docs/self-hosting/configuration/envars.mdx index 9b054e376..c05647627 100644 --- a/docs/self-hosting/configuration/envars.mdx +++ b/docs/self-hosting/configuration/envars.mdx @@ -153,29 +153,18 @@ Other environment variables are listed below to increase the functionality of yo JWT token lifetime expressed in seconds or a string describing a time span -{" "} - - - -{" "} - - - -#### Error logging +#### Logging Infisical uses Sentry to report error logs -{" "} + + The minimum log level for application logging; can be one of `trace`, `debug`, `info`, `warn`, `error`, or `fatal`. + = ({ allowedLabel, ...props }) => { - const permission = useOrgPermission(); + const { permission } = useOrgPermission(); return ( diff --git a/frontend/src/components/v2/FormControl/FormControl.tsx b/frontend/src/components/v2/FormControl/FormControl.tsx index 91dfd4139..8f2e57f48 100644 --- a/frontend/src/components/v2/FormControl/FormControl.tsx +++ b/frontend/src/components/v2/FormControl/FormControl.tsx @@ -9,16 +9,24 @@ export type FormLabelProps = { isRequired?: boolean; label?: ReactNode; icon?: ReactNode; + className?: string; }; -export const FormLabel = ({ id, label, isRequired, icon }: FormLabelProps) => ( +export const FormLabel = ({ id, label, isRequired, icon, className }: FormLabelProps) => ( {label} {isRequired && *} - {icon && {icon}} + {icon && ( + + {icon} + + )} ); diff --git a/frontend/src/components/v2/Stepper/Stepper.tsx b/frontend/src/components/v2/Stepper/Stepper.tsx new file mode 100644 index 000000000..8389121c0 --- /dev/null +++ b/frontend/src/components/v2/Stepper/Stepper.tsx @@ -0,0 +1,78 @@ +import { Children, cloneElement, ReactElement, ReactNode } from "react"; +import { faCheck } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { twMerge } from "tailwind-merge"; + +export type StepperProps = { + activeStep: number; + children: ReactNode; + direction: "vertical" | "horizontal"; + className?: string; +}; + +export const Stepper = ({ activeStep, children, direction, className }: StepperProps) => { + return ( +
+ {Children.map(children as ReactNode, (child: ReactNode, index) => { + const isCompleted = activeStep > index; + const isActive = index === activeStep; + const isNotLast = index + 1 !== (children as Array).length; + return ( +
+
+
+ {isCompleted ? : index + 1} +
+ {cloneElement(child as ReactElement, { + direction, + activeStep, + isCompleted, + isActive + })} +
+ {isNotLast && ( +
+ )} +
+ ); + })} +
+ ); +}; + +export type StepProps = { + title: string; + description?: ReactNode; + // isActive?: boolean; + // isCompleted?: boolean; + // activeStep?: number; + // direction?: "vertical" | "horizontal"; +}; + +export const Step = ({ title, description }: StepProps) => { + return ( +
+
{title}
+ {description &&
{description}
} +
+ ); +}; diff --git a/frontend/src/components/v2/Stepper/index.tsx b/frontend/src/components/v2/Stepper/index.tsx new file mode 100644 index 000000000..b90544cdd --- /dev/null +++ b/frontend/src/components/v2/Stepper/index.tsx @@ -0,0 +1,2 @@ +export type { StepperProps,StepProps } from "./Stepper"; +export { Step,Stepper } from "./Stepper"; diff --git a/frontend/src/components/v2/index.tsx b/frontend/src/components/v2/index.tsx index 31e2b4e57..26af93f37 100644 --- a/frontend/src/components/v2/index.tsx +++ b/frontend/src/components/v2/index.tsx @@ -22,6 +22,7 @@ export * from "./SecretInput"; export * from "./Select"; export * from "./Skeleton"; export * from "./Spinner"; +export * from "./Stepper"; export * from "./Switch"; export * from "./Table"; export * from "./Tabs"; diff --git a/frontend/src/context/OrgPermissionContext/OrgPermissionContext.tsx b/frontend/src/context/OrgPermissionContext/OrgPermissionContext.tsx index 5320ece6d..312ae0437 100644 --- a/frontend/src/context/OrgPermissionContext/OrgPermissionContext.tsx +++ b/frontend/src/context/OrgPermissionContext/OrgPermissionContext.tsx @@ -1,6 +1,7 @@ import { createContext, ReactNode, useContext } from "react"; import { useGetUserOrgPermissions } from "@app/hooks/api"; +import { OrgUser } from "@app/hooks/api/types"; import { useOrganization } from "../OrganizationContext"; import { TOrgPermission } from "./types"; @@ -9,7 +10,10 @@ type Props = { children: ReactNode; }; -const OrgPermissionContext = createContext(null); +const OrgPermissionContext = createContext(null); export const OrgPermissionProvider = ({ children }: Props): JSX.Element => { const { currentOrg } = useOrganization(); diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index ae4b49d63..5ae91e756 100644 --- a/frontend/src/context/ProjectPermissionContext/types.ts +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -21,7 +21,8 @@ export enum ProjectPermissionSub { Workspace = "workspace", Secrets = "secrets", SecretRollback = "secret-rollback", - SecretApproval = "secret-approval" + SecretApproval = "secret-approval", + SecretRotation = "secret-rotation" } type SubjectFields = { @@ -45,6 +46,7 @@ export type ProjectPermissionSet = | [ProjectPermissionActions, ProjectPermissionSub.Settings] | [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens] | [ProjectPermissionActions, ProjectPermissionSub.SecretApproval] + | [ProjectPermissionActions, ProjectPermissionSub.SecretRotation] | [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace] | [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace] | [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback] diff --git a/frontend/src/hoc/withPermission/withPermission.tsx b/frontend/src/hoc/withPermission/withPermission.tsx index e0c402af3..389d8509c 100644 --- a/frontend/src/hoc/withPermission/withPermission.tsx +++ b/frontend/src/hoc/withPermission/withPermission.tsx @@ -21,7 +21,7 @@ export const withPermission = ( { action, subject, className, containerClassName }: Props["abilities"]> ) => { const HOC = (hocProps: T) => { - const permission = useOrgPermission(); + const { permission } = useOrgPermission(); // akhilmhdh: Set as any due to casl/react ts type bug // REASON: casl due to its type checking can't seem to union even if union intersection is applied diff --git a/frontend/src/hooks/api/index.tsx b/frontend/src/hooks/api/index.tsx index 008f1e2fb..c707fe33f 100644 --- a/frontend/src/hooks/api/index.tsx +++ b/frontend/src/hooks/api/index.tsx @@ -12,6 +12,7 @@ export * from "./secretApproval"; export * from "./secretApprovalRequest"; export * from "./secretFolders"; export * from "./secretImports"; +export * from "./secretRotation"; export * from "./secrets"; export * from "./secretSnapshots"; export * from "./serviceAccounts"; diff --git a/frontend/src/hooks/api/integrationAuth/index.tsx b/frontend/src/hooks/api/integrationAuth/index.tsx index 7e7c355ca..f5065919f 100644 --- a/frontend/src/hooks/api/integrationAuth/index.tsx +++ b/frontend/src/hooks/api/integrationAuth/index.tsx @@ -4,6 +4,7 @@ export { useGetIntegrationAuthApps, useGetIntegrationAuthBitBucketWorkspaces, useGetIntegrationAuthById, + useGetIntegrationAuthChecklyGroups, useGetIntegrationAuthNorthflankSecretGroups, useGetIntegrationAuthRailwayEnvironments, useGetIntegrationAuthRailwayServices, @@ -11,4 +12,4 @@ export { useGetIntegrationAuthTeams, useGetIntegrationAuthVercelBranches, useSaveIntegrationAccessToken -} from "./queries"; +} from "./queries"; \ No newline at end of file diff --git a/frontend/src/hooks/api/integrationAuth/queries.tsx b/frontend/src/hooks/api/integrationAuth/queries.tsx index 9213919e0..7e5d3d711 100644 --- a/frontend/src/hooks/api/integrationAuth/queries.tsx +++ b/frontend/src/hooks/api/integrationAuth/queries.tsx @@ -6,14 +6,16 @@ import { workspaceKeys } from "../workspace/queries"; import { App, BitBucketWorkspace, + ChecklyGroup, Environment, IntegrationAuth, NorthflankSecretGroup, Org, Project, Service, - Team, - TeamCityBuildConfig} from "./types"; + Team, + TeamCityBuildConfig +} from "./types"; const integrationAuthKeys = { getIntegrationAuthById: (integrationAuthId: string) => @@ -29,6 +31,14 @@ const integrationAuthKeys = { integrationAuthId: string; appId: string; }) => [{ integrationAuthId, appId }, "integrationAuthVercelBranches"] as const, + getIntegrationAuthChecklyGroups: ({ + integrationAuthId, + accountId + }: { + integrationAuthId: string; + accountId: string; + }) => + [{ integrationAuthId, accountId }, "integrationAuthChecklyGroups"] as const, getIntegrationAuthQoveryOrgs: (integrationAuthId: string) => [{ integrationAuthId }, "integrationAuthQoveryOrgs"] as const, getIntegrationAuthQoveryProjects: ({ @@ -125,6 +135,24 @@ const fetchIntegrationAuthTeams = async (integrationAuthId: string) => { return data.teams; }; +const fetchIntegrationAuthChecklyGroups = async ({ + integrationAuthId, + accountId +}: { + integrationAuthId: string; + accountId: string; +}) => { + const { data } = await apiRequest.get<{ groups: ChecklyGroup[] }>( + `/api/v1/integration-auth/${integrationAuthId}/checkly/groups`, + { + params: { + accountId + } + } + ); + + return data.groups; +}; const fetchIntegrationAuthVercelBranches = async ({ integrationAuthId, @@ -413,6 +441,26 @@ export const useGetIntegrationAuthVercelBranches = ({ }); }; +export const useGetIntegrationAuthChecklyGroups = ({ + integrationAuthId, + accountId +}: { + integrationAuthId: string; + accountId: string; +}) => { + return useQuery({ + queryKey: integrationAuthKeys.getIntegrationAuthChecklyGroups({ + integrationAuthId, + accountId + }), + queryFn: () => fetchIntegrationAuthChecklyGroups({ + integrationAuthId, + accountId + }), + enabled: true + }); +}; + export const useGetIntegrationAuthQoveryOrgs = (integrationAuthId: string) => { return useQuery({ queryKey: integrationAuthKeys.getIntegrationAuthQoveryOrgs(integrationAuthId), diff --git a/frontend/src/hooks/api/integrationAuth/types.ts b/frontend/src/hooks/api/integrationAuth/types.ts index 2292e3222..5af8a3773 100644 --- a/frontend/src/hooks/api/integrationAuth/types.ts +++ b/frontend/src/hooks/api/integrationAuth/types.ts @@ -26,6 +26,11 @@ export type Environment = { environmentId: string; }; +export type ChecklyGroup = { + name: string; + groupId: number; +}; + export type Container = { name: string; containerId: string; diff --git a/frontend/src/hooks/api/roles/queries.tsx b/frontend/src/hooks/api/roles/queries.tsx index 3e68aa57d..5694ccfd6 100644 --- a/frontend/src/hooks/api/roles/queries.tsx +++ b/frontend/src/hooks/api/roles/queries.tsx @@ -8,6 +8,7 @@ import { apiRequest } from "@app/config/request"; import { OrgPermissionSet } from "@app/context/OrgPermissionContext/types"; import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext/types"; +import { OrgUser } from "../users/types"; import { TGetRolesDTO, TGetUserOrgPermissionsDTO, @@ -63,13 +64,16 @@ export const useGetRoles = ({ orgId, workspaceId }: TGetRolesDTO) => }); const getUserOrgPermissions = async ({ orgId }: TGetUserOrgPermissionsDTO) => { - if (orgId === "") return []; + if (orgId === "") return { permissions: [], membership: null }; const { data } = await apiRequest.get<{ - data: { permissions: PackRule>>[] }; - }>(`/api/v1/roles/organization/${orgId}/permissions`, {}); - - return data.data.permissions; + data: { + permissions: PackRule>>[]; + membership: OrgUser; + }; + }>(`/api/v1/roles/organization/${orgId}/permissions`); + + return data.data; }; export const useGetUserOrgPermissions = ({ orgId }: TGetUserOrgPermissionsDTO) => @@ -78,9 +82,9 @@ export const useGetUserOrgPermissions = ({ orgId }: TGetUserOrgPermissionsDTO) = queryFn: () => getUserOrgPermissions({ orgId }), // enabled: Boolean(orgId), select: (data) => { - const rule = unpackRules>>(data); + const rule = unpackRules>>(data.permissions); const ability = createMongoAbility(rule, { conditionsMatcher }); - return ability; + return { permission: ability, membership: data.membership }; } }); diff --git a/frontend/src/hooks/api/secretRotation/index.ts b/frontend/src/hooks/api/secretRotation/index.ts new file mode 100644 index 000000000..906c8b498 --- /dev/null +++ b/frontend/src/hooks/api/secretRotation/index.ts @@ -0,0 +1,6 @@ +export { + useCreateSecretRotation, + useDeleteSecretRotation, + useRestartSecretRotation +} from "./mutation"; +export { useGetSecretRotationProviders, useGetSecretRotations } from "./queries"; diff --git a/frontend/src/hooks/api/secretRotation/mutation.tsx b/frontend/src/hooks/api/secretRotation/mutation.tsx new file mode 100644 index 000000000..cda4f8074 --- /dev/null +++ b/frontend/src/hooks/api/secretRotation/mutation.tsx @@ -0,0 +1,52 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { secretRotationKeys } from "./queries"; +import { + TCreateSecretRotationDTO, + TDeleteSecretRotationDTO, + TRestartSecretRotationDTO +} from "./types"; + +export const useCreateSecretRotation = () => { + const queryClient = useQueryClient(); + + return useMutation<{}, {}, TCreateSecretRotationDTO>({ + mutationFn: async (dto) => { + const { data } = await apiRequest.post("/api/v1/secret-rotations", dto); + return data; + }, + onSuccess: (_, { workspaceId }) => { + queryClient.invalidateQueries(secretRotationKeys.list({ workspaceId })); + } + }); +}; + +export const useDeleteSecretRotation = () => { + const queryClient = useQueryClient(); + + return useMutation<{}, {}, TDeleteSecretRotationDTO>({ + mutationFn: async (dto) => { + const { data } = await apiRequest.delete(`/api/v1/secret-rotations/${dto.id}`); + return data; + }, + onSuccess: (_, { workspaceId }) => { + queryClient.invalidateQueries(secretRotationKeys.list({ workspaceId })); + } + }); +}; + +export const useRestartSecretRotation = () => { + const queryClient = useQueryClient(); + + return useMutation<{}, {}, TRestartSecretRotationDTO>({ + mutationFn: async (dto) => { + const { data } = await apiRequest.post("/api/v1/secret-rotations/restart", { id: dto.id }); + return data; + }, + onSuccess: (_, { workspaceId }) => { + queryClient.invalidateQueries(secretRotationKeys.list({ workspaceId })); + } + }); +}; diff --git a/frontend/src/hooks/api/secretRotation/queries.tsx b/frontend/src/hooks/api/secretRotation/queries.tsx new file mode 100644 index 000000000..a3c8e270e --- /dev/null +++ b/frontend/src/hooks/api/secretRotation/queries.tsx @@ -0,0 +1,110 @@ +import { useCallback } from "react"; +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; + +import { + decryptAssymmetric, + decryptSymmetric +} from "@app/components/utilities/cryptography/crypto"; +import { apiRequest } from "@app/config/request"; + +import { + TGetSecretRotationList, + TGetSecretRotationProviders, + TSecretRotation, + TSecretRotationProviderList +} from "./types"; + +export const secretRotationKeys = { + listProviders: ({ workspaceId }: TGetSecretRotationProviders) => [ + { workspaceId }, + "secret-rotation-providers" + ], + list: ({ workspaceId }: Omit) => + [{ workspaceId }, "secret-rotations"] as const +}; + +const fetchSecretRotationProviders = async ({ workspaceId }: TGetSecretRotationProviders) => { + const { data } = await apiRequest.get( + `/api/v1/secret-rotation-providers/${workspaceId}` + ); + return data; +}; + +export const useGetSecretRotationProviders = ({ + workspaceId, + options = {} +}: TGetSecretRotationProviders & { + options?: Omit< + UseQueryOptions< + TSecretRotationProviderList, + unknown, + TSecretRotationProviderList, + ReturnType + >, + "queryKey" | "queryFn" + >; +}) => + useQuery({ + ...options, + queryKey: secretRotationKeys.listProviders({ workspaceId }), + enabled: Boolean(workspaceId) && (options?.enabled ?? true), + queryFn: async () => fetchSecretRotationProviders({ workspaceId }) + }); + +const fetchSecretRotations = async ({ + workspaceId +}: Omit) => { + const { data } = await apiRequest.get<{ secretRotations: TSecretRotation[] }>( + "/api/v1/secret-rotations", + { params: { workspaceId } } + ); + return data.secretRotations; +}; + +export const useGetSecretRotations = ({ + workspaceId, + decryptFileKey, + options = {} +}: TGetSecretRotationList & { + options?: Omit< + UseQueryOptions< + TSecretRotation[], + unknown, + TSecretRotation<{ key: string }>[], + ReturnType + >, + "queryKey" | "queryFn" + >; +}) => + useQuery({ + ...options, + queryKey: secretRotationKeys.list({ workspaceId }), + enabled: Boolean(workspaceId) && (options?.enabled ?? true), + queryFn: async () => fetchSecretRotations({ workspaceId }), + select: useCallback( + (data: TSecretRotation[]) => { + const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string; + const decryptKey = decryptAssymmetric({ + ciphertext: decryptFileKey.encryptedKey, + nonce: decryptFileKey.nonce, + publicKey: decryptFileKey.sender.publicKey, + privateKey: PRIVATE_KEY + }); + return data.map((el) => ({ + ...el, + outputs: el.outputs.map(({ key, secret }) => ({ + key, + secret: { + key: decryptSymmetric({ + ciphertext: secret.secretValueCiphertext, + iv: secret.secretValueIV, + tag: secret.secretValueTag, + key: decryptKey + }) + } + })) + })); + }, + [decryptFileKey] + ) + }); diff --git a/frontend/src/hooks/api/secretRotation/types.ts b/frontend/src/hooks/api/secretRotation/types.ts new file mode 100644 index 000000000..76d3b5174 --- /dev/null +++ b/frontend/src/hooks/api/secretRotation/types.ts @@ -0,0 +1,133 @@ +import { UserWsKeyPair } from "../keys/types"; +import { EncryptedSecret } from "../secrets/types"; + +export enum TProviderFunctionTypes { + HTTP = "http", + DB = "database" +} + +export enum TDbProviderClients { + // postgres, cockroack db, amazon red shift + Pg = "pg", + // mysql and maria db + Sql = "sql" +} + +export enum TAssignOp { + Direct = "direct", + JmesPath = "jmesopath" +} + +export type TJmesPathAssignOp = { + assign: TAssignOp.JmesPath; + path: string; +}; + +export type TDirectAssignOp = { + assign: TAssignOp.Direct; + value: string; +}; + +export type TAssignFunction = TJmesPathAssignOp | TDirectAssignOp; + +export type THttpProviderFunction = { + type: TProviderFunctionTypes.HTTP; + url: string; + method: string; + header?: Record; + query?: Record; + body?: Record; + setter?: Record; + pre?: Record; +}; + +export type TDbProviderFunction = { + type: TProviderFunctionTypes.DB; + client: TDbProviderClients; + username: string; + password: string; + host: string; + database: string; + port: string; + query: string; + setter?: Record; + pre?: Record; +}; + +export type TProviderFunction = THttpProviderFunction | TDbProviderFunction; + +export type TProviderTemplate = { + inputs: { + properties: Record; + type: "object"; + required: string[]; + }; + outputs: Record; + functions: { + set: TProviderFunction; + remove?: TProviderFunction; + test: TProviderFunction; + }; +}; + +export type TSecretRotation = { + _id: string; + interval: number; + provider: string; + customProvider: string; + workspace: string; + environment: string; + secretPath: string; + outputs: Array<{ + key: string; + secret: T; + }>; + status?: "success" | "failed"; + lastRotatedAt?: string; + statusMessage?: string; + algorithm: string; + keyEncoding: string; +}; + +export type TSecretRotationProvider = { + name: string; + image: string; + title: string; + description: string; + template: TProviderTemplate; +}; + +export type TSecretRotationProviderList = { + custom: TSecretRotationProvider[]; + providers: TSecretRotationProvider[]; +}; + +export type TGetSecretRotationProviders = { + workspaceId: string; +}; + +export type TGetSecretRotationList = { + workspaceId: string; + decryptFileKey: UserWsKeyPair; +}; + +export type TCreateSecretRotationDTO = { + workspaceId: string; + secretPath: string; + environment: string; + interval: number; + provider: string; + customProvider?: string; + inputs: Record; + outputs: Record; +}; + +export type TDeleteSecretRotationDTO = { + id: string; + workspaceId: string; +}; + +export type TRestartSecretRotationDTO = { + id: string; + workspaceId: string; +}; diff --git a/frontend/src/hooks/api/serviceTokens/queries.tsx b/frontend/src/hooks/api/serviceTokens/queries.tsx index 900bb1d34..44786ba0d 100644 --- a/frontend/src/hooks/api/serviceTokens/queries.tsx +++ b/frontend/src/hooks/api/serviceTokens/queries.tsx @@ -88,14 +88,18 @@ export const useUpdateServiceTokenV3 = () => { isActive, scopes, trustedIps, - expiresIn + expiresIn, + accessTokenTTL, + isRefreshTokenRotationEnabled }) => { const { data: { serviceTokenData } } = await apiRequest.patch(`/api/v3/service-token/${serviceTokenDataId}`, { name, isActive, scopes, trustedIps, - expiresIn + expiresIn, + accessTokenTTL, + isRefreshTokenRotationEnabled }); return serviceTokenData; diff --git a/frontend/src/hooks/api/serviceTokens/types.ts b/frontend/src/hooks/api/serviceTokens/types.ts index d47c3bc4c..18e59c68e 100644 --- a/frontend/src/hooks/api/serviceTokens/types.ts +++ b/frontend/src/hooks/api/serviceTokens/types.ts @@ -56,11 +56,15 @@ export type ServiceTokenDataV3 = { name: string; workspace: string; isActive: boolean; - lastUsed?: string; - usageCount: number; + refreshTokenLastUsed?: string; + accessTokenLastUsed?: string; + refreshTokenUsageCount: number; + accessTokenUsageCount: number; scopes: ServiceTokenV3Scope[]; trustedIps: ServiceTokenV3TrustedIp[]; expiresAt?: string; + accessTokenTTL: number; + isRefreshTokenRotationEnabled: boolean; createdAt: string; updatedAt: string; }; @@ -74,12 +78,14 @@ export type CreateServiceTokenDataV3DTO = { ipAddress: string; }[]; expiresIn?: number; + accessTokenTTL: number; encryptedKey: string; nonce: string; + isRefreshTokenRotationEnabled: boolean; } export type CreateServiceTokenDataV3Res = { - serviceToken: string; + refreshToken: string; serviceTokenData: ServiceTokenDataV3; } @@ -92,6 +98,8 @@ export type UpdateServiceTokenDataV3DTO = { ipAddress: string; }[]; expiresIn?: number; + accessTokenTTL?: number; + isRefreshTokenRotationEnabled?: boolean; } export type DeleteServiceTokenDataV3DTO = { diff --git a/frontend/src/hooks/api/subscriptions/types.ts b/frontend/src/hooks/api/subscriptions/types.ts index c61967eec..e38a7527f 100644 --- a/frontend/src/hooks/api/subscriptions/types.ts +++ b/frontend/src/hooks/api/subscriptions/types.ts @@ -12,6 +12,7 @@ export type SubscriptionPlan = { secretVersioning: boolean; slug: string; secretApproval: string; + secretRotation: string; tier: number; workspaceLimit: number; workspacesUsed: number; diff --git a/frontend/src/hooks/api/types.ts b/frontend/src/hooks/api/types.ts index f6131e5b2..422219adf 100644 --- a/frontend/src/hooks/api/types.ts +++ b/frontend/src/hooks/api/types.ts @@ -13,6 +13,12 @@ export type { export { ApprovalStatus, CommitType } from "./secretApprovalRequest/types"; export type { TSecretFolder } from "./secretFolders/types"; export type { TImportedSecrets, TSecretImports } from "./secretImports/types"; +export type { + TGetSecretRotationProviders, + TProviderTemplate, + TSecretRotationProvider, + TSecretRotationProviderList +} from "./secretRotation/types"; export * from "./secrets/types"; export type { CreateServiceTokenDTO, ServiceToken } from "./serviceTokens/types"; export type { SubscriptionPlan } from "./subscriptions/types"; diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx index e3136fae3..5b185b97a 100644 --- a/frontend/src/hooks/api/workspace/queries.tsx +++ b/frontend/src/hooks/api/workspace/queries.tsx @@ -34,7 +34,8 @@ export const workspaceKeys = { getUserWsEnvironments: (workspaceId: string) => ["workspace-env", { workspaceId }] as const, getWorkspaceAuditLogs: (workspaceId: string) => [{ workspaceId }] as const, getWorkspaceUsers: (workspaceId: string) => [{ workspaceId }] as const, - getWorkspaceServiceTokenDataV3: (workspaceId: string) => [{ workspaceId }, "workspace-service-token-data-v3"] as const + getWorkspaceServiceTokenDataV3: (workspaceId: string) => + [{ workspaceId }, "workspace-service-token-data-v3"] as const }; const fetchWorkspaceById = async (workspaceId: string) => { @@ -53,7 +54,7 @@ const fetchWorkspaceIndexStatus = async (workspaceId: string) => { return data; }; -const fetchWorkspaceSecrets = async (workspaceId: string) => { +export const fetchWorkspaceSecrets = async (workspaceId: string) => { const { data: { secrets } } = await apiRequest.get<{ secrets: EncryptedSecret[] }>( @@ -253,9 +254,18 @@ export const useReorderWsEnvironment = () => { const queryClient = useQueryClient(); return useMutation<{}, {}, ReorderEnvironmentsDTO>({ - mutationFn: ({ workspaceID, environmentSlug, environmentName, otherEnvironmentSlug, otherEnvironmentName}) => { + mutationFn: ({ + workspaceID, + environmentSlug, + environmentName, + otherEnvironmentSlug, + otherEnvironmentName + }) => { return apiRequest.patch(`/api/v2/workspace/${workspaceID}/environments`, { - environmentSlug, environmentName, otherEnvironmentSlug, otherEnvironmentName + environmentSlug, + environmentName, + otherEnvironmentSlug, + otherEnvironmentName }); }, onSuccess: () => { @@ -378,4 +388,4 @@ export const useGetWorkspaceServiceTokenDataV3 = (workspaceId: string) => { }, enabled: true }); -}; \ No newline at end of file +}; diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index 04302548e..632449273 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -319,7 +319,7 @@ export const AppLayout = ({ children }: LayoutProps) => { ))} - + {/*
- + */}
-
-
-
Pro Tips
- After creating an integration, your secrets will start syncing immediately. This might cause an unexpected override of current secrets in Checkly with secrets from Infisical. - If you have multiple Checkly integrations and are using suffixes for at least one of them, you will have to add suffixes for all the active Checkly integrations – otherwise you might run into rare unexpected behavior. -
) : (
@@ -242,7 +273,7 @@ export default function ChecklyCreateIntegrationPage() { Set Up Checkly Integration - {isIntegrationAuthAppsLoading ? infisical loading indicator :
+ {isIntegrationAuthAppsLoading || isintegrationAuthGroupsLoading ? infisical loading indicator : )} - {!isBotActive && ( + {!isBotActive && Boolean(integrations.length) && (
@@ -119,7 +119,7 @@ export const IntegrationsSection = ({ {integrationSlugNameMapping[integration.integration]}
- {(integration.integration === "qovery") && ( + {integration.integration === "qovery" && (
@@ -163,12 +163,22 @@ export const IntegrationsSection = ({
)} {((integration.integration === "checkly") || (integration.integration === "github")) && ( -
- -
- {integration?.metadata?.secretSuffix || "-"} + <> + {integration.targetService && ( +
+ +
+ {integration.targetService} +
+
+ )} +
+ +
+ {integration?.metadata?.secretSuffix || "-"} +
-
+ )}
diff --git a/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx b/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx index 7667eaeb6..0cb590631 100644 --- a/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx +++ b/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx @@ -41,6 +41,7 @@ import { } from "@app/hooks/api"; import { FolderBreadCrumbs } from "./components/FolderBreadCrumbs"; +import { ProjectIndexSecretsSection } from "./components/ProjectIndexSecretsSection"; import { SecretOverviewFolderRow } from "./components/SecretOverviewFolderRow"; import { SecretOverviewTableRow } from "./components/SecretOverviewTableRow"; @@ -259,6 +260,7 @@ export const SecretOverviewPage = () => { return (
+
diff --git a/frontend/src/views/SecretOverviewPage/components/ProjectIndexSecretsSection/ProjectIndexSecretsSection.tsx b/frontend/src/views/SecretOverviewPage/components/ProjectIndexSecretsSection/ProjectIndexSecretsSection.tsx new file mode 100644 index 000000000..23cfa750d --- /dev/null +++ b/frontend/src/views/SecretOverviewPage/components/ProjectIndexSecretsSection/ProjectIndexSecretsSection.tsx @@ -0,0 +1,98 @@ +import { ProjectPermissionCan } from "@app/components/permissions"; +import { + decryptAssymmetric, + decryptSymmetric +} from "@app/components/utilities/cryptography/crypto"; +import { Button, Spinner } from "@app/components/v2"; +import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { useToggle } from "@app/hooks"; +import { useGetWorkspaceIndexStatus, useNameWorkspaceSecrets } from "@app/hooks/api"; +import { UserWsKeyPair } from "@app/hooks/api/types"; +import { fetchWorkspaceSecrets } from "@app/hooks/api/workspace/queries"; + +// TODO: add check so that this only shows up if user is +// an admin in the workspace +type Props = { + decryptFileKey: UserWsKeyPair; +}; + +export const ProjectIndexSecretsSection = ({ decryptFileKey }: Props) => { + const { currentWorkspace } = useWorkspace(); + const { data: isBlindIndexed, isLoading: isBlindIndexedLoading } = useGetWorkspaceIndexStatus( + currentWorkspace?._id ?? "" + ); + const [isIndexing, setIsIndexing] = useToggle(); + const nameWorkspaceSecrets = useNameWorkspaceSecrets(); + + const onEnableBlindIndices = async () => { + if (!currentWorkspace?._id) return; + setIsIndexing.on(); + try { + const encryptedSecrets = await fetchWorkspaceSecrets(currentWorkspace._id); + + const key = decryptAssymmetric({ + ciphertext: decryptFileKey.encryptedKey, + nonce: decryptFileKey.nonce, + publicKey: decryptFileKey.sender.publicKey, + privateKey: localStorage.getItem("PRIVATE_KEY") as string + }); + + const secretsToUpdate = encryptedSecrets.map((encryptedSecret) => { + const secretName = decryptSymmetric({ + ciphertext: encryptedSecret.secretKeyCiphertext, + iv: encryptedSecret.secretKeyIV, + tag: encryptedSecret.secretKeyTag, + key + }); + + return { + secretName, + _id: encryptedSecret._id + }; + }); + await nameWorkspaceSecrets.mutateAsync({ + workspaceId: currentWorkspace._id, + secretsToUpdate + }); + } catch (err) { + console.log(err); + } finally { + setIsIndexing.off(); + } + }; + + return !isBlindIndexedLoading && !isBlindIndexed ? ( +
+ {isIndexing && ( +
+ +
+
Please wait
+ Re-indexing your secrets... +
+
+ )} +

Enable Blind Indices

+

+ Your project was created before the introduction of blind indexing. + To continue accessing secrets by name through the SDK, public API and web dashboard, please enable blind + indexing. This is a one time process. +

+ + {(isAllowed) => ( + + )} + +
+ ) : ( +
+ ); +}; diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/ProjectIndexSecretsSection/index.tsx b/frontend/src/views/SecretOverviewPage/components/ProjectIndexSecretsSection/index.tsx similarity index 100% rename from frontend/src/views/Settings/ProjectSettingsPage/components/ProjectIndexSecretsSection/index.tsx rename to frontend/src/views/SecretOverviewPage/components/ProjectIndexSecretsSection/index.tsx diff --git a/frontend/src/views/SecretRotationPage/SecretRotationPage.tsx b/frontend/src/views/SecretRotationPage/SecretRotationPage.tsx new file mode 100644 index 000000000..0cb179db3 --- /dev/null +++ b/frontend/src/views/SecretRotationPage/SecretRotationPage.tsx @@ -0,0 +1,407 @@ +import { useTranslation } from "react-i18next"; +import { + faArrowsSpin, + faExclamationTriangle, + faFolder, + faInfoCircle, + faRotate, + faTrash +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { formatDistance } from "date-fns"; + +import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; +import { ProjectPermissionCan } from "@app/components/permissions"; +import { + Button, + DeleteActionModal, + EmptyState, + IconButton, + Modal, + ModalContent, + Skeleton, + Spinner, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tooltip, + Tr, + UpgradePlanModal +} from "@app/components/v2"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + useProjectPermission, + useSubscription, + useWorkspace +} from "@app/context"; +import { withProjectPermission } from "@app/hoc"; +import { usePopUp } from "@app/hooks"; +import { + useDeleteSecretRotation, + useGetSecretRotationProviders, + useGetSecretRotations, + useGetUserWsKey, + useGetWorkspaceBot, + useRestartSecretRotation, + useUpdateBotActiveStatus +} from "@app/hooks/api"; +import { TSecretRotationProvider } from "@app/hooks/api/types"; + +import { CreateRotationForm } from "./components/CreateRotationForm"; +import { generateBotKey } from "./SecretRotationPage.utils"; + +export const SecretRotationPage = withProjectPermission( + () => { + const { currentWorkspace } = useWorkspace(); + const { t } = useTranslation(); + const permission = useProjectPermission(); + const { createNotification } = useNotificationContext(); + const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([ + "createRotation", + "activeBot", + "deleteRotation", + "upgradePlan" + ] as const); + const workspaceId = currentWorkspace?._id || ""; + const canCreateRotation = permission.can( + ProjectPermissionActions.Create, + ProjectPermissionSub.SecretRotation + ); + const { subscription } = useSubscription(); + + const { data: userWsKey } = useGetUserWsKey(workspaceId); + + const { data: secretRotationProviders, isLoading: isRotationProviderLoading } = + useGetSecretRotationProviders({ workspaceId }); + const { data: secretRotations, isLoading: isRotationLoading } = useGetSecretRotations({ + workspaceId, + decryptFileKey: userWsKey! + }); + + const { + mutateAsync: deleteSecretRotation, + variables: deleteSecretRotationVars, + isLoading: isDeletingRotation + } = useDeleteSecretRotation(); + const { + mutateAsync: restartSecretRotation, + variables: restartSecretRotationVar, + isLoading: isRestartingRotation + } = useRestartSecretRotation(); + + const { data: bot } = useGetWorkspaceBot(workspaceId); + const { mutateAsync: updateBotActiveStatus } = useUpdateBotActiveStatus(); + + const isBotActive = Boolean(bot?.isActive); + + const handleDeleteRotation = async () => { + const { id } = popUp.deleteRotation.data as { id: string }; + try { + await deleteSecretRotation({ + id, + workspaceId + }); + handlePopUpClose("deleteRotation"); + createNotification({ + type: "success", + text: "Successfully removed rotation" + }); + } catch (error) { + console.log(error); + createNotification({ + type: "error", + text: "Failed to remove rotation" + }); + } + }; + + const handleRestartRotation = async (id: string) => { + try { + await restartSecretRotation({ + id, + workspaceId + }); + createNotification({ + type: "success", + text: "Secret rotation initiated" + }); + } catch (error) { + console.log(error); + createNotification({ + type: "error", + text: "Failed to restart rotation" + }); + } + }; + + const handleUserAcceptBotCondition = async () => { + const provider = popUp.activeBot?.data as TSecretRotationProvider; + try { + if (bot?._id) { + const botKey = generateBotKey(bot.publicKey, userWsKey!); + await updateBotActiveStatus({ + isActive: true, + botId: bot._id, + workspaceId, + botKey + }); + } + handlePopUpOpen("createRotation", provider); + handlePopUpClose("activeBot"); + } catch (error) { + console.log(error); + createNotification({ + type: "error", + text: "Failed to create bot" + }); + } + }; + + const handleCreateRotation = async (provider: TSecretRotationProvider) => { + if (subscription && !subscription?.secretRotation) { + handlePopUpOpen("upgradePlan"); + return; + } + if (!canCreateRotation) { + createNotification({ type: "error", text: "Access permission denied!!" }); + return; + } + if (isBotActive) { + handlePopUpOpen("createRotation", provider); + } else { + handlePopUpOpen("activeBot", provider); + } + }; + + return ( +
+
+

Secret Rotation

+

Auto rotate secrets for better security

+
+
+
Rotated Secrets
+
+ + + + + + + + + + + + + + {isRotationLoading && ( + + )} + {!isRotationLoading && secretRotations?.length === 0 && ( + + + + )} + {secretRotations?.map( + ({ + environment, + secretPath, + outputs, + provider, + _id, + lastRotatedAt, + status, + statusMessage + }) => { + const isDeleting = deleteSecretRotationVars?.id === _id && isDeletingRotation; + const isRestarting = + restartSecretRotationVar?.id === _id && isRestartingRotation; + return ( + + + + + + + + + ); + } + )} + +
Secret NameEnvironmentProviderStatusLast RotationAction
+ +
+ {outputs + .map(({ key }) => key) + .join(",") + .toUpperCase()} + +
+
{environment}
+
+ + {secretPath} +
+
+
{provider} +
+ {status} + {status === "failed" && ( + + + + )} +
+
+ {lastRotatedAt + ? formatDistance(new Date(lastRotatedAt), new Date()) + : "-"} + +
+ + {(isAllowed) => ( + handleRestartRotation(_id)} + > + {isRestarting ? ( + + ) : ( + + )} + + )} + + + {(isAllowed) => ( + handlePopUpOpen("deleteRotation", { id: _id })} + > + {isDeleting ? ( + + ) : ( + + )} + + )} + +
+
+
+
+
+
Infisical Rotation Providers
+
+ {isRotationProviderLoading && + Array.from({ length: 12 }).map((_, index) => ( + + ))} + {!isRotationProviderLoading && + secretRotationProviders?.providers.map((provider) => ( +
{ + if (evt.key === "Enter") handlePopUpOpen("createRotation", provider); + }} + onClick={() => handleCreateRotation(provider)} + > + rotation provider logo +
+ {provider.title} +
+
+ + + +
+
+ ))} +
+ handlePopUpToggle("createRotation", isOpen)} + provider={(popUp.createRotation.data as TSecretRotationProvider) || {}} + /> + handlePopUpToggle("activeBot", isOpen)} + > + + + +
+ } + > + {t("integrations.why-infisical-needs-access")} + + + handlePopUpToggle("deleteRotation", isOpen)} + deleteKey="delete" + onDeleteApproved={handleDeleteRotation} + /> + handlePopUpToggle("upgradePlan", isOpen)} + text="You can add secret rotation if you switch to Infisical's Team plan." + /> +
+ ); + }, + { action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.SecretRotation } +); diff --git a/frontend/src/views/SecretRotationPage/SecretRotationPage.utils.ts b/frontend/src/views/SecretRotationPage/SecretRotationPage.utils.ts new file mode 100644 index 000000000..6dd346f8e --- /dev/null +++ b/frontend/src/views/SecretRotationPage/SecretRotationPage.utils.ts @@ -0,0 +1,30 @@ +import { UserWsKeyPair } from "@app/hooks/api/types"; + +import { + decryptAssymmetric, + encryptAssymmetric +} from "../../components/utilities/cryptography/crypto"; + +// refactor these to common function in frontend +export const generateBotKey = (botPublicKey: string, latestKey: UserWsKeyPair) => { + const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY"); + + if (!PRIVATE_KEY) { + throw new Error("Private Key missing"); + } + + const WORKSPACE_KEY = decryptAssymmetric({ + ciphertext: latestKey.encryptedKey, + nonce: latestKey.nonce, + publicKey: latestKey.sender.publicKey, + privateKey: PRIVATE_KEY + }); + + const { ciphertext, nonce } = encryptAssymmetric({ + plaintext: WORKSPACE_KEY, + publicKey: botPublicKey, + privateKey: PRIVATE_KEY + }); + + return { encryptedKey: ciphertext, nonce }; +}; diff --git a/frontend/src/views/SecretRotationPage/components/CreateRotationForm/CreateRotationForm.tsx b/frontend/src/views/SecretRotationPage/components/CreateRotationForm/CreateRotationForm.tsx new file mode 100644 index 000000000..5d4ed8073 --- /dev/null +++ b/frontend/src/views/SecretRotationPage/components/CreateRotationForm/CreateRotationForm.tsx @@ -0,0 +1,145 @@ +import { useRef, useState } from "react"; +import { AnimatePresence, motion } from "framer-motion"; + +import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; +import { Modal, ModalContent, Step, Stepper } from "@app/components/v2"; +import { useCreateSecretRotation } from "@app/hooks/api"; +import { TSecretRotationProvider } from "@app/hooks/api/types"; + +import { RotationInputForm } from "./steps/RotationInputForm"; +import { + RotationOutputForm, + TFormSchema as TRotationOutputSchema +} from "./steps/RotationOutputForm"; + +const WIZARD_STEPS = [ + { + title: "Inputs", + description: "Provider secrets" + }, + { + title: "Outputs", + description: "Map rotated secrets to keys" + } +]; + +type Props = { + isOpen?: boolean; + onToggle: (isOpen: boolean) => void; + customProvider?: string; + workspaceId: string; + provider: TSecretRotationProvider; +}; + +export const CreateRotationForm = ({ + isOpen, + onToggle, + provider, + workspaceId, + customProvider +}: Props) => { + const [wizardStep, setWizardStep] = useState(0); + const wizardData = useRef<{ + input?: Record; + output?: TRotationOutputSchema; + }>({}); + const { createNotification } = useNotificationContext(); + + const { mutateAsync: createSecretRotation } = useCreateSecretRotation(); + + const handleFormCancel = () => { + onToggle(false); + setWizardStep(0); + wizardData.current = {}; + }; + + const handleFormSubmit = async () => { + if (!wizardData.current.input || !wizardData.current.output) return; + try { + await createSecretRotation({ + workspaceId, + provider: provider.name, + customProvider, + secretPath: wizardData.current.output.secretPath, + environment: wizardData.current.output.environment, + interval: wizardData.current.output.interval, + inputs: wizardData.current.input, + outputs: wizardData.current.output.secrets + }); + setWizardStep(0); + onToggle(false); + wizardData.current = {}; + } catch (error) { + console.log(error); + createNotification({ + type: "error", + text: "Failed to create secret rotation" + }); + } + }; + + return ( + { + onToggle(state); + setWizardStep(0); + wizardData.current = {}; + }} + > + + + {WIZARD_STEPS.map(({ title, description }, index) => ( + + ))} + + + {wizardStep === 0 && ( + + { + wizardData.current.input = data; + setWizardStep((state) => state + 1); + }} + inputSchema={provider.template?.inputs || {}} + /> + + )} + {wizardStep === 1 && ( + + { + wizardData.current.output = data; + await handleFormSubmit(); + }} + /> + + )} + + + + ); +}; diff --git a/frontend/src/views/SecretRotationPage/components/CreateRotationForm/index.tsx b/frontend/src/views/SecretRotationPage/components/CreateRotationForm/index.tsx new file mode 100644 index 000000000..8392c4cdb --- /dev/null +++ b/frontend/src/views/SecretRotationPage/components/CreateRotationForm/index.tsx @@ -0,0 +1 @@ +export { CreateRotationForm } from "./CreateRotationForm"; diff --git a/frontend/src/views/SecretRotationPage/components/CreateRotationForm/steps/RotationInputForm.tsx b/frontend/src/views/SecretRotationPage/components/CreateRotationForm/steps/RotationInputForm.tsx new file mode 100644 index 000000000..125515bf1 --- /dev/null +++ b/frontend/src/views/SecretRotationPage/components/CreateRotationForm/steps/RotationInputForm.tsx @@ -0,0 +1,78 @@ +import { Controller, useForm } from "react-hook-form"; +import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { Button, FormControl, FormLabel, SecretInput, Tooltip } from "@app/components/v2"; + +type Props = { + onSubmit: (data: Record) => void; + onCancel: () => void; + inputSchema: { + properties: Record; + required: string[]; + }; +}; + +const formSchema = z.record(z.string().trim().optional()); + +export const RotationInputForm = ({ onSubmit, onCancel, inputSchema }: Props) => { + const { + control, + handleSubmit, + formState: { isSubmitting } + } = useForm>({ + resolver: zodResolver(formSchema) + }); + + return ( +
+ {Object.keys(inputSchema.properties || {}).map((inputName) => ( + ( + + + {Boolean(inputSchema.properties[inputName]?.desc) && ( + + + + )} +
+ } + > + + + )} + /> + ))} +
+ + +
+ + ); +}; diff --git a/frontend/src/views/SecretRotationPage/components/CreateRotationForm/steps/RotationOutputForm.tsx b/frontend/src/views/SecretRotationPage/components/CreateRotationForm/steps/RotationOutputForm.tsx new file mode 100644 index 000000000..e11038f7c --- /dev/null +++ b/frontend/src/views/SecretRotationPage/components/CreateRotationForm/steps/RotationOutputForm.tsx @@ -0,0 +1,153 @@ +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { Button, FormControl, Input, Select, SelectItem, Spinner } from "@app/components/v2"; +import { useWorkspace } from "@app/context"; +import { useGetProjectSecrets, useGetUserWsKey } from "@app/hooks/api"; + +const formSchema = z.object({ + environment: z.string().trim(), + secretPath: z.string().trim().default("/"), + interval: z.number().min(1), + secrets: z.record(z.string()) +}); + +export type TFormSchema = z.infer; +type Props = { + outputSchema: Record; + onSubmit: (data: TFormSchema) => void; + onCancel: () => void; +}; + +export const RotationOutputForm = ({ onSubmit, onCancel, outputSchema = {} }: Props) => { + const { currentWorkspace } = useWorkspace(); + const environments = currentWorkspace?.environments || []; + const workspaceId = currentWorkspace?._id || ""; + const { + control, + handleSubmit, + watch, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(formSchema) + }); + + const environment = watch("environment", environments?.[0]?.slug); + const secretPath = watch("secretPath"); + const selectedSecrets = watch("secrets"); + + const { data: userWsKey } = useGetUserWsKey(workspaceId); + const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({ + workspaceId, + environment, + secretPath, + decryptFileKey: userWsKey! + }); + + return ( +
+ ( + + + + )} + /> + ( + + + + )} + /> + ( + + field.onChange(parseInt(evt.target.value, 10))} + /> + + )} + /> +
+
Mapping
+
Select keys for rotated value to get saved
+
+ {Object.keys(outputSchema).map((outputName) => ( + ( + + + + )} + /> + ))} +
+ + +
+ + ); +}; diff --git a/frontend/src/views/SecretRotationPage/index.tsx b/frontend/src/views/SecretRotationPage/index.tsx new file mode 100644 index 000000000..c2f41e76e --- /dev/null +++ b/frontend/src/views/SecretRotationPage/index.tsx @@ -0,0 +1 @@ +export { SecretRotationPage } from "./SecretRotationPage"; diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgDeleteSection/OrgDeleteSection.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgDeleteSection/OrgDeleteSection.tsx index dd810f1ac..a336355db 100644 --- a/frontend/src/views/Settings/OrgSettingsPage/components/OrgDeleteSection/OrgDeleteSection.tsx +++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgDeleteSection/OrgDeleteSection.tsx @@ -1,81 +1,70 @@ import { useRouter } from "next/router"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; -import { - Button, - DeleteActionModal -} from "@app/components/v2"; -import { useOrganization, useUser } from "@app/context"; -import { - useDeleteOrgById, - useGetOrgUsers -} from "@app/hooks/api"; +import { Button, DeleteActionModal } from "@app/components/v2"; +import { useOrganization, useOrgPermission } from "@app/context"; +import { useDeleteOrgById } from "@app/hooks/api"; import { usePopUp } from "@app/hooks/usePopUp"; import { navigateUserToOrg } from "@app/views/Login/Login.utils"; export const OrgDeleteSection = () => { - const router = useRouter(); - const { currentOrg } = useOrganization(); - const { user } = useUser(); - const { createNotification } = useNotificationContext(); - const { data: members } = useGetOrgUsers(currentOrg?._id ?? ""); - - const membershipOrg = members?.find((member) => member.user._id === user._id); + const router = useRouter(); + const { currentOrg } = useOrganization(); + const { createNotification } = useNotificationContext(); + const { membership } = useOrgPermission(); - const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ - "deleteOrg" - ] as const); + const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ + "deleteOrg" + ] as const); - const { mutateAsync, isLoading } = useDeleteOrgById(); - - const handleDeleteOrgSubmit = async () => { - try { - if (!currentOrg?._id) return; - - await mutateAsync({ - organizationId: currentOrg?._id - }); - - createNotification({ - text: "Successfully deleted organization", - type: "success" - }); + const { mutateAsync, isLoading } = useDeleteOrgById(); - await navigateUserToOrg(router); - - handlePopUpClose("deleteOrg"); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to delete organization", - type: "error" - }); - } + const handleDeleteOrgSubmit = async () => { + try { + if (!currentOrg?._id) return; + + await mutateAsync({ + organizationId: currentOrg?._id + }); + + createNotification({ + text: "Successfully deleted organization", + type: "success" + }); + + await navigateUserToOrg(router); + + handlePopUpClose("deleteOrg"); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to delete organization", + type: "error" + }); } + }; - return ( -
-

- Danger Zone -

- - handlePopUpToggle("deleteOrg", isOpen)} - deleteKey="confirm" - onDeleteApproved={handleDeleteOrgSubmit} - /> -
- ); -} \ No newline at end of file + return ( +
+

Danger Zone

+ + handlePopUpToggle("deleteOrg", isOpen)} + deleteKey="confirm" + onDeleteApproved={handleDeleteOrgSubmit} + /> +
+ ); +}; diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgGeneralTab/OrgGeneralTab.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgGeneralTab/OrgGeneralTab.tsx index d8e48966d..9c8279caf 100644 --- a/frontend/src/views/Settings/OrgSettingsPage/components/OrgGeneralTab/OrgGeneralTab.tsx +++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgGeneralTab/OrgGeneralTab.tsx @@ -1,24 +1,17 @@ -import { useOrganization, useUser } from "@app/context"; -import { useGetOrgUsers } from "@app/hooks/api"; +import { useOrgPermission } from "@app/context"; import { OrgDeleteSection } from "../OrgDeleteSection"; import { OrgIncidentContactsSection } from "../OrgIncidentContactsSection"; import { OrgNameChangeSection } from "../OrgNameChangeSection"; export const OrgGeneralTab = () => { - const { currentOrg } = useOrganization(); - const { user } = useUser(); - const { data: members } = useGetOrgUsers(currentOrg?._id ?? ""); - - const membershipOrg = members?.find((member) => member.user._id === user?._id); + const { membership } = useOrgPermission(); return (
- {(membershipOrg && membershipOrg.role === "admin") && ( - - )} + {membership && membership.role === "admin" && }
); }; diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgIncidentContactsSection/OrgIncidentContactsSection.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgIncidentContactsSection/OrgIncidentContactsSection.tsx index bc2547528..fbb4987a7 100644 --- a/frontend/src/views/Settings/OrgSettingsPage/components/OrgIncidentContactsSection/OrgIncidentContactsSection.tsx +++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgIncidentContactsSection/OrgIncidentContactsSection.tsx @@ -15,7 +15,7 @@ export const OrgIncidentContactsSection = () => { const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ "addContact" ] as const); - const permission = useOrgPermission(); + const { permission } = useOrgPermission(); return (
diff --git a/frontend/src/views/Settings/PersonalSettingsPage/PersonalAPIKeyTab/PersonalAPIKeyTab.tsx b/frontend/src/views/Settings/PersonalSettingsPage/PersonalAPIKeyTab/PersonalAPIKeyTab.tsx index c27e280a5..aba48b23e 100644 --- a/frontend/src/views/Settings/PersonalSettingsPage/PersonalAPIKeyTab/PersonalAPIKeyTab.tsx +++ b/frontend/src/views/Settings/PersonalSettingsPage/PersonalAPIKeyTab/PersonalAPIKeyTab.tsx @@ -1,5 +1,5 @@ -// import { APIKeyV2Section } from "../APIKeyV2Section"; import { APIKeySection } from "../APIKeySection"; +// import { APIKeyV2Section } from "../APIKeyV2Section"; export const PersonalAPIKeyTab = () => { return ( diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx index cacfda8bb..bbd8e78d4 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx @@ -2,20 +2,18 @@ import { AutoCapitalizationSection } from "../AutoCapitalizationSection"; import { DeleteProjectSection } from "../DeleteProjectSection"; import { E2EESection } from "../E2EESection"; import { EnvironmentSection } from "../EnvironmentSection"; -import { ProjectIndexSecretsSection } from "../ProjectIndexSecretsSection"; import { ProjectNameChangeSection } from "../ProjectNameChangeSection"; import { SecretTagsSection } from "../SecretTagsSection"; export const ProjectGeneralTab = () => { - return ( -
- - - - - - - -
- ); -} \ No newline at end of file + return ( +
+ + + + + + +
+ ); +}; diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/ProjectIndexSecretsSection/ProjectIndexSecretsSection.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/ProjectIndexSecretsSection/ProjectIndexSecretsSection.tsx deleted file mode 100644 index ca2e14eb9..000000000 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/ProjectIndexSecretsSection/ProjectIndexSecretsSection.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { ProjectPermissionCan } from "@app/components/permissions"; -import { - decryptAssymmetric, - decryptSymmetric -} from "@app/components/utilities/cryptography/crypto"; -import { Button } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; -import { - useGetUserWsKey, - useGetWorkspaceIndexStatus, - useGetWorkspaceSecrets, - useNameWorkspaceSecrets -} from "@app/hooks/api"; - -// TODO: add check so that this only shows up if user is -// an admin in the workspace - -export const ProjectIndexSecretsSection = () => { - const { currentWorkspace } = useWorkspace(); - const { data: isBlindIndexed, isLoading: isBlindIndexedLoading } = useGetWorkspaceIndexStatus( - currentWorkspace?._id ?? "" - ); - const { data: latestFileKey } = useGetUserWsKey(currentWorkspace?._id ?? ""); - const { data: encryptedSecrets } = useGetWorkspaceSecrets(currentWorkspace?._id ?? ""); - const nameWorkspaceSecrets = useNameWorkspaceSecrets(); - - const onEnableBlindIndices = async () => { - if (!currentWorkspace?._id) return; - if (!encryptedSecrets) return; - if (!latestFileKey) return; - - const key = decryptAssymmetric({ - ciphertext: latestFileKey.encryptedKey, - nonce: latestFileKey.nonce, - publicKey: latestFileKey.sender.publicKey, - privateKey: localStorage.getItem("PRIVATE_KEY") as string - }); - - const secretsToUpdate = encryptedSecrets.map((encryptedSecret) => { - const secretName = decryptSymmetric({ - ciphertext: encryptedSecret.secretKeyCiphertext, - iv: encryptedSecret.secretKeyIV, - tag: encryptedSecret.secretKeyTag, - key - }); - - return { - secretName, - _id: encryptedSecret._id - }; - }); - - await nameWorkspaceSecrets.mutateAsync({ - workspaceId: currentWorkspace._id, - secretsToUpdate - }); - }; - - return !isBlindIndexedLoading && !isBlindIndexed ? ( -
-

Blind Indices

-

- Your project, created before the introduction of blind indexing, contains unindexed secrets. - To access individual secrets by name through the SDK and public API, please enable blind - indexing. -

- - {(isAllowed) => ( - - )} - -
- ) : ( -
- ); -}; diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/AddServiceTokenV3Modal.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/AddServiceTokenV3Modal.tsx index cc3c81bab..3d76b2435 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/AddServiceTokenV3Modal.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/AddServiceTokenV3Modal.tsx @@ -1,8 +1,9 @@ -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { Controller, useFieldArray, useForm } from "react-hook-form"; -import { faPlus,faXmark } from "@fortawesome/free-solid-svg-icons"; +import { faCheck, faCopy,faPlus, faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { yupResolver } from "@hookform/resolvers/yup"; +import { motion } from "framer-motion"; import nacl from "tweetnacl"; import { encodeBase64 } from "tweetnacl-util"; import * as yup from "yup"; @@ -21,12 +22,17 @@ import { ModalContent, Select, SelectItem, - UpgradePlanModal -} from "@app/components/v2"; + Switch, + Tab, + TabList, + TabPanel, + Tabs, + UpgradePlanModal} from "@app/components/v2"; import { useSubscription, useWorkspace } from "@app/context"; +import { useToggle } from "@app/hooks"; import { useCreateServiceTokenV3, useGetUserWsKey, @@ -41,8 +47,13 @@ import { } from "@app/hooks/api/serviceTokens/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; +enum TabSections { + General = "general", + Advanced = "advanced" +} + const expirations = [ - { label: "Never", value: undefined }, + { label: "Never", value: "" }, { label: "1 day", value: "86400" }, { label: "7 days", value: "604800" }, { label: "1 month", value: "2592000" }, @@ -60,6 +71,17 @@ const permissionsMap: { const schema = yup.object({ name: yup.string().required("ST V3 name is required"), expiresIn: yup.string(), + accessTokenTTL: yup + .string() + .test("is-positive-integer", "Access Token TTL must be a positive integer", (value) => { + if (typeof value === "undefined") { + return false; + } + + const num = parseInt(value, 10); + return !Number.isNaN(num) && num > 0 && String(num) === value; + }) + .required("Access Token TTL is required"), scopes: yup .array( yup.object({ @@ -79,14 +101,15 @@ const schema = yup.object({ .required() .label("Scope"), trustedIps: yup - .array( - yup.object({ - ipAddress: yup.string().max(50).required().label("IP Address") - }) - ) - .min(1) - .required() - .label("Trusted IP") + .array( + yup.object({ + ipAddress: yup.string().max(50).required().label("IP Address") + }) + ) + .min(1) + .required() + .label("Trusted IP"), + isRefreshTokenRotationEnabled: yup.boolean().default(false) }).required(); export type FormData = yup.InferType; @@ -102,6 +125,9 @@ export const AddServiceTokenV3Modal = ({ handlePopUpOpen, handlePopUpToggle }: Props) => { + const [newServiceTokenJSON, setNewServiceTokenJSON] = useState(""); + const [isServiceTokenJSONCopied, setIsServiceTokenJSONCopied] = useToggle(false); + const { subscription } = useSubscription(); const { currentWorkspace } = useWorkspace(); @@ -118,6 +144,7 @@ export const AddServiceTokenV3Modal = ({ resolver: yupResolver(schema), defaultValues: { name: "", + accessTokenTTL: "7200", scopes: [{ permission: "read", environment: currentWorkspace?.environments?.[0]?.slug, @@ -128,6 +155,21 @@ export const AddServiceTokenV3Modal = ({ }] } }); + + useEffect(() => { + let timer: NodeJS.Timeout; + + if (isServiceTokenJSONCopied) { + timer = setTimeout(() => setIsServiceTokenJSONCopied.off(), 2000); + } + + return () => clearTimeout(timer); + }, [setIsServiceTokenJSONCopied]); + + const copyTokenToClipboard = () => { + navigator.clipboard.writeText(newServiceTokenJSON); + setIsServiceTokenJSONCopied.on(); + }; useEffect(() => { const serviceTokenData = popUp?.serviceTokenV3?.data as { @@ -135,6 +177,8 @@ export const AddServiceTokenV3Modal = ({ name: string; scopes: ServiceTokenV3Scope[]; trustedIps: ServiceTokenV3TrustedIp[]; + accessTokenTTL: number; + isRefreshTokenRotationEnabled: boolean; }; if (serviceTokenData) { @@ -163,11 +207,14 @@ export const AddServiceTokenV3Modal = ({ return ({ ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}` }); - }) + }), + accessTokenTTL: String(serviceTokenData.accessTokenTTL), + isRefreshTokenRotationEnabled: serviceTokenData.isRefreshTokenRotationEnabled }); } else { reset({ name: "", + accessTokenTTL: "7200", scopes: [{ permission: "read", environment: currentWorkspace?.environments?.[0]?.slug, @@ -186,8 +233,10 @@ export const AddServiceTokenV3Modal = ({ const onFormSubmit = async ({ name, expiresIn, + accessTokenTTL, scopes, - trustedIps + trustedIps, + isRefreshTokenRotationEnabled }: FormData) => { try { const serviceTokenData = popUp?.serviceTokenV3?.data as { @@ -213,8 +262,12 @@ export const AddServiceTokenV3Modal = ({ name, scopes: reformattedScopes, trustedIps, - expiresIn: expiresIn === "" ? undefined : Number(expiresIn) + expiresIn: expiresIn === "" ? undefined : Number(expiresIn), + accessTokenTTL: Number(accessTokenTTL), + isRefreshTokenRotationEnabled }); + + handlePopUpToggle("serviceTokenV3", false); } else { // create if (!currentWorkspace?._id) return; @@ -239,24 +292,29 @@ export const AddServiceTokenV3Modal = ({ privateKey: localStorage.getItem("PRIVATE_KEY") as string }); - const { serviceToken } = await createMutateAsync({ + const { refreshToken } = await createMutateAsync({ name, workspaceId: currentWorkspace._id, publicKey, scopes: reformattedScopes, trustedIps, expiresIn: expiresIn === "" ? undefined : Number(expiresIn), + accessTokenTTL: Number(accessTokenTTL), encryptedKey: ciphertext, - nonce + nonce, + isRefreshTokenRotationEnabled }); const downloadData = { - publicKey, - privateKey, - serviceToken + public_key: publicKey, + private_key: privateKey, + refresh_token: refreshToken }; - const blob = new Blob([JSON.stringify(downloadData, null, 2)], { type: "application/json" }); + const serviceTokenJSON = JSON.stringify(downloadData, null, 2); + setNewServiceTokenJSON(serviceTokenJSON); + + const blob = new Blob([serviceTokenJSON], { type: "application/json" }); const href = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = href; @@ -272,7 +330,6 @@ export const AddServiceTokenV3Modal = ({ }); reset(); - handlePopUpToggle("serviceTokenV3", false); } catch (err) { console.error(err); createNotification({ @@ -281,6 +338,8 @@ export const AddServiceTokenV3Modal = ({ }); } } + + const hasServiceTokenJSON = Boolean(newServiceTokenJSON); return ( { handlePopUpToggle("serviceTokenV3", isOpen); reset(); + setNewServiceTokenJSON(""); }} > -
- ( - - - - )} - /> - {tokenScopes.map(({ id }, index) => ( -
- ( - - - - )} - /> - ( - - - - )} - /> - ( - - - - )} - /> - remove(index)} - size="lg" - colorSchema="danger" - variant="plain" - ariaLabel="update" - className="p-3" + {!hasServiceTokenJSON ? ( + + + +
+ General + Advanced +
+
+ + - -
-
- ))} -
- -
- {tokenTrustedIps.map(({ id }, index) => ( -
- { - return ( - ( + + + + )} + /> + {tokenScopes.map(({ id }, index) => ( +
+ ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + remove(index)} + size="lg" + colorSchema="danger" + variant="plain" + ariaLabel="update" + className="p-3" + > + + +
+ ))} +
+ +
+ ( + + + + )} + /> + + + +
+ {tokenTrustedIps.map(({ id }, index) => ( +
+ { + return ( + + { + if (subscription?.ipAllowlisting) { + field.onChange(e); + return; + } + + handlePopUpOpen("upgradePlan"); + }} + placeholder="123.456.789.0" + /> + + ); + }} + /> + { + if (subscription?.ipAllowlisting) { + removeTrustedIp(index); + return; + } + + handlePopUpOpen("upgradePlan"); + }} + size="lg" + colorSchema="danger" + variant="plain" + ariaLabel="update" + className="p-3" + > + + +
+ ))} +
+ +
+ ( + + + + )} + /> +
+ ( + onChange(isChecked)} + isChecked={value} + > + Refresh Token Rotation + + )} /> - - ); - }} - /> - { - if (subscription?.ipAllowlisting) { - removeTrustedIp(index); - return; - } - - handlePopUpOpen("upgradePlan"); - }} - size="lg" - colorSchema="danger" - variant="plain" - ariaLabel="update" - className="p-3" +

When enabled, as a result of exchanging a refresh token, a new refresh token will be issued and the existing token will be invalidated.

+
+
+
+ +
+ +
- ))} -
- + + + Click to copy + +
- ( - - - - )} - /> -
- - -
- + )} handlePopUpToggle("upgradePlan", isOpen)} diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/ServiceTokenV3Table.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/ServiceTokenV3Table.tsx index dd9268bf6..d41a0bd82 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/ServiceTokenV3Table.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/ServiceTokenV3Table.tsx @@ -34,6 +34,8 @@ type Props = { name?: string; scopes?: ServiceTokenV3Scope[]; trustedIps?: ServiceTokenV3TrustedIp[]; + accessTokenTTL?: number; + isRefreshTokenRotationEnabled?: boolean; } ) => void; }; @@ -81,10 +83,9 @@ export const ServiceTokenV3Table = ({ Status Scopes Trusted IPs - {/* # Times Used */} - Last Used + Access Token TTL Created At - Expires At + Valid Until @@ -97,12 +98,12 @@ export const ServiceTokenV3Table = ({ _id, name, isActive, - lastUsed, - // usageCount, scopes, trustedIps, createdAt, - expiresAt + expiresAt, + accessTokenTTL, + isRefreshTokenRotationEnabled }) => { return ( @@ -160,8 +161,7 @@ export const ServiceTokenV3Table = ({ ); })} - {/* {usageCount} */} - {lastUsed ? format(new Date(lastUsed), "yyyy-MM-dd") : "-"} + {accessTokenTTL} {format(new Date(createdAt), "yyyy-MM-dd")} {expiresAt ? format(new Date(expiresAt), "yyyy-MM-dd") : "-"} @@ -176,7 +176,9 @@ export const ServiceTokenV3Table = ({ serviceTokenDataId: _id, name, scopes, - trustedIps + trustedIps, + accessTokenTTL, + isRefreshTokenRotationEnabled }); }} size="lg" diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/index.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/index.tsx index 5f9e1013b..757dfc6b2 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/index.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/index.tsx @@ -2,7 +2,6 @@ export { AutoCapitalizationSection } from "./AutoCapitalizationSection"; export { DeleteProjectSection } from "./DeleteProjectSection"; export { E2EESection } from "./E2EESection"; export { EnvironmentSection } from "./EnvironmentSection"; -export { ProjectIndexSecretsSection } from "./ProjectIndexSecretsSection"; export { ProjectNameChangeSection } from "./ProjectNameChangeSection"; export { SecretTagsSection } from "./SecretTagsSection"; export { ServiceTokenSection } from "./ServiceTokenSection"; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index d5a16121a..42ea0e773 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -2,15 +2,16 @@ "compilerOptions": { "baseUrl": ".", "paths": { - "~/components/*": ["./src/components/*"], - "~/hooks/*": ["./src/hooks/*"], - "~/utilities/*": ["./src/components/utilities/*"], - "~/*": ["./src/const"], - "~/pages/*": ["./src/pages/*"], - "@app/*": ["./src/*"] + "@app/*": [ + "./src/*" + ] }, "target": "ESNext", - "lib": ["dom", "dom.iterable", "esnext"], + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -25,6 +26,14 @@ "jsx": "preserve", "incremental": true }, - "include": ["next-i18next.config.js", "next-env.d.ts", "./src/**/*.ts", "./src/**/*.tsx", "./.eslintrc.js"], - "exclude": ["node_modules"] + "include": [ + "next-i18next.config.js", + "next-env.d.ts", + "./src/**/*.ts", + "./src/**/*.tsx", + "./.eslintrc.js" + ], + "exclude": [ + "node_modules" + ] }