1
0
mirror of https://github.com/Infisical/infisical.git synced 2025-03-29 22:02:57 +00:00

Merge branch 'main' into import-export-secrets

This commit is contained in:
mv-turtle
2023-01-06 16:07:11 -08:00
committed by GitHub
177 changed files with 9049 additions and 4575 deletions
.github/workflows
README.md
backend
Dockerfilepackage-lock.jsonpackage.json
src
tsconfig.json
cli
docs
frontend
img

@ -24,7 +24,7 @@ jobs:
cache: "npm" cache: "npm"
cache-dependency-path: backend/package-lock.json cache-dependency-path: backend/package-lock.json
- name: 📦 Install dependencies - name: 📦 Install dependencies
run: npm ci --only-production --ignore-scripts run: npm ci --only-production
working-directory: backend working-directory: backend
- name: 🧪 Run tests - name: 🧪 Run tests
run: npm run test:ci run: npm run test:ci

@ -21,7 +21,7 @@
<a href="https://github.com/infisical/infisical/blob/main/CONTRIBUTING.md"> <a href="https://github.com/infisical/infisical/blob/main/CONTRIBUTING.md">
<img src="https://img.shields.io/badge/PRs-Welcome-brightgreen" alt="PRs welcome!" /> <img src="https://img.shields.io/badge/PRs-Welcome-brightgreen" alt="PRs welcome!" />
</a> </a>
<a href=""> <a href="https://github.com/Infisical/infisical/issues">
<img src="https://img.shields.io/github/commit-activity/m/infisical/infisical" alt="git commit activity" /> <img src="https://img.shields.io/github/commit-activity/m/infisical/infisical" alt="git commit activity" />
</a> </a>
<a href="https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g"> <a href="https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g">
@ -40,13 +40,15 @@
- **[Language-Agnostic CLI](https://infisical.com/docs/cli/overview)** that pulls and injects environment variables into your local workflow - **[Language-Agnostic CLI](https://infisical.com/docs/cli/overview)** that pulls and injects environment variables into your local workflow
- **[Complete control over your data](https://infisical.com/docs/self-hosting/overview)** - host it yourself on any infrastructure - **[Complete control over your data](https://infisical.com/docs/self-hosting/overview)** - host it yourself on any infrastructure
- **Navigate Multiple Environments** per project (e.g. development, staging, production, etc.) - **Navigate Multiple Environments** per project (e.g. development, staging, production, etc.)
- **Personal/Shared** scoping for environment variables - **Personal overrides** for environment variables
- **[Integrations](https://infisical.com/docs/integrations/overview)** with CI/CD and production infrastructure - **[Integrations](https://infisical.com/docs/integrations/overview)** with CI/CD and production infrastructure
- **[Secret Versioning](https://infisical.com/docs/getting-started/dashboard/versioning)** - check the history of change for any secret
- **[Activity Logs](https://infisical.com/docs/getting-started/dashboard/audit-logs)** - check what user in the project is performing what actions with secrets
- **[Point-in-time Secrets Recovery](https://infisical.com/docs/getting-started/dashboard/pit-recovery)** - roll back to any snapshot of you secrets
- 🔜 **1-Click Deploy** to Digital Ocean and Heroku - 🔜 **1-Click Deploy** to Digital Ocean and Heroku
- 🔜 **Authentication/Authorization** for projects (read/write controls soon) - 🔜 **Authentication/Authorization** for projects (read/write controls soon)
- 🔜 **Automatic Secret Rotation** - 🔜 **Automatic Secret Rotation**
- 🔜 **2FA** - 🔜 **2FA**
- 🔜 **Access Logs**
- 🔜 **Slack Integration & MS Teams** integrations - 🔜 **Slack Integration & MS Teams** integrations
And more. And more.
@ -65,7 +67,7 @@ To quickly get started, visit our [get started guide](https://infisical.com/docs
Infisical makes secret management simple and end-to-end encrypted by default. We're on a mission to make it more accessible to all developers, <i>not just security teams</i>. Infisical makes secret management simple and end-to-end encrypted by default. We're on a mission to make it more accessible to all developers, <i>not just security teams</i>.
According to a [report](https://www.ekransystem.com/en/blog/secrets-management) in 2019, only 10% of organizations use secret management solutions despite all using digital secrets to some extent. According to a [report](https://www.ekransystem.com/en/blog/secrets-management), only 10% of organizations use secret management solutions despite all using digital secrets to some extent.
If you care about efficiency and security, then Infisical is right for you. If you care about efficiency and security, then Infisical is right for you.
@ -319,7 +321,7 @@ Looking to report a security vulnerability? Please don't post about it in GitHub
## 🚨 Stay Up-to-Date ## 🚨 Stay Up-to-Date
Infisical officially launched as v.1.0 on November 21st, 2022. However, a lot of new features are coming very quickly. Watch **releases** of this repository to be notified about future updates: Infisical officially launched as v.1.0 on November 21st, 2022. There are a lot of new features coming very frequently. Watch **releases** of this repository to be notified about future updates:
![infisical-star-github](https://github.com/Infisical/infisical/blob/main/.github/images/star-infisical.gif?raw=true) ![infisical-star-github](https://github.com/Infisical/infisical/blob/main/.github/images/star-infisical.gif?raw=true)

@ -4,7 +4,10 @@ WORKDIR /app
COPY package.json package-lock.json ./ COPY package.json package-lock.json ./
RUN npm ci --only-production --ignore-scripts # RUN npm ci --only-production --ignore-scripts
# "prepare": "cd .. && npm install"
RUN npm ci --only-production
COPY . . COPY . .

@ -15,7 +15,9 @@
"@sentry/tracing": "^7.19.0", "@sentry/tracing": "^7.19.0",
"@types/crypto-js": "^4.1.1", "@types/crypto-js": "^4.1.1",
"@types/libsodium-wrappers": "^0.7.10", "@types/libsodium-wrappers": "^0.7.10",
"await-to-js": "^3.0.0",
"axios": "^1.1.3", "axios": "^1.1.3",
"bcrypt": "^5.1.0",
"bigint-conversion": "^2.2.2", "bigint-conversion": "^2.2.2",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"cors": "^2.8.5", "cors": "^2.8.5",
@ -38,12 +40,15 @@
"tweetnacl": "^1.0.3", "tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1", "tweetnacl-util": "^0.15.1",
"typescript": "^4.9.3", "typescript": "^4.9.3",
"utility-types": "^3.10.0",
"winston": "^3.8.2", "winston": "^3.8.2",
"winston-loki": "^6.0.6" "winston-loki": "^6.0.6"
}, },
"devDependencies": { "devDependencies": {
"@jest/globals": "^29.3.1", "@jest/globals": "^29.3.1",
"@posthog/plugin-scaffold": "^1.3.4", "@posthog/plugin-scaffold": "^1.3.4",
"@types/bcrypt": "^5.0.0",
"@types/bcryptjs": "^2.4.2",
"@types/cookie-parser": "^1.4.3", "@types/cookie-parser": "^1.4.3",
"@types/cors": "^2.8.12", "@types/cors": "^2.8.12",
"@types/express": "^4.17.14", "@types/express": "^4.17.14",
@ -2576,6 +2581,39 @@
"resolved": "https://registry.npmjs.org/@juanelas/base64/-/base64-1.0.5.tgz", "resolved": "https://registry.npmjs.org/@juanelas/base64/-/base64-1.0.5.tgz",
"integrity": "sha512-gTIElNo4ohMcYUZzol/Hb6DYJzphxl0b1B4egJJ+JiqxqcOcWx4XLMAB+lhWuMsMX3uR1oc5hwPusU3lgc1FkQ==" "integrity": "sha512-gTIElNo4ohMcYUZzol/Hb6DYJzphxl0b1B4egJJ+JiqxqcOcWx4XLMAB+lhWuMsMX3uR1oc5hwPusU3lgc1FkQ=="
}, },
"node_modules/@mapbox/node-pre-gyp": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz",
"integrity": "sha512-4ySo4CjzStuprMwk35H5pPbkymjv1SF3jGLj6rAHp/xT/RF7TL7bd9CTm1xDY49K2qF7jmR/g7k+SkLETP6opA==",
"dependencies": {
"detect-libc": "^2.0.0",
"https-proxy-agent": "^5.0.0",
"make-dir": "^3.1.0",
"node-fetch": "^2.6.7",
"nopt": "^5.0.0",
"npmlog": "^5.0.1",
"rimraf": "^3.0.2",
"semver": "^7.3.5",
"tar": "^6.1.11"
},
"bin": {
"node-pre-gyp": "bin/node-pre-gyp"
}
},
"node_modules/@mapbox/node-pre-gyp/node_modules/nopt": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
"integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
"dependencies": {
"abbrev": "1"
},
"bin": {
"nopt": "bin/nopt.js"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@maxmind/geoip2-node": { "node_modules/@maxmind/geoip2-node": {
"version": "3.5.0", "version": "3.5.0",
"resolved": "https://registry.npmjs.org/@maxmind/geoip2-node/-/geoip2-node-3.5.0.tgz", "resolved": "https://registry.npmjs.org/@maxmind/geoip2-node/-/geoip2-node-3.5.0.tgz",
@ -3033,6 +3071,21 @@
"@babel/types": "^7.3.0" "@babel/types": "^7.3.0"
} }
}, },
"node_modules/@types/bcrypt": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.0.tgz",
"integrity": "sha512-agtcFKaruL8TmcvqbndlqHPSJgsolhf/qPWchFlgnW1gECTN/nKbFcoFnvKAQRFfKbh+BO6A3SWdJu9t+xF3Lw==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/bcryptjs": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.2.tgz",
"integrity": "sha512-LiMQ6EOPob/4yUL66SZzu6Yh77cbzJFYll+ZfaPiPPFswtIlA/Fs1MzdKYA7JApHU49zQTbJGX3PDmCpIdDBRQ==",
"dev": true
},
"node_modules/@types/body-parser": { "node_modules/@types/body-parser": {
"version": "1.19.2", "version": "1.19.2",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz",
@ -3483,8 +3536,7 @@
"node_modules/abbrev": { "node_modules/abbrev": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
"dev": true
}, },
"node_modules/accepts": { "node_modules/accepts": {
"version": "1.3.8", "version": "1.3.8",
@ -3586,7 +3638,6 @@
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"engines": { "engines": {
"node": ">=8" "node": ">=8"
} }
@ -3619,6 +3670,23 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/aproba": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
"integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ=="
},
"node_modules/are-we-there-yet": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz",
"integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==",
"dependencies": {
"delegates": "^1.0.0",
"readable-stream": "^3.6.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/arg": { "node_modules/arg": {
"version": "4.1.3", "version": "4.1.3",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
@ -3678,6 +3746,14 @@
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
}, },
"node_modules/await-to-js": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/await-to-js/-/await-to-js-3.0.0.tgz",
"integrity": "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g==",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/axios": { "node_modules/axios": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.1.3.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.1.3.tgz",
@ -3803,6 +3879,19 @@
} }
] ]
}, },
"node_modules/bcrypt": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.0.tgz",
"integrity": "sha512-RHBS7HI5N5tEnGTmtR/pppX0mmDSBpQ4aCBsj7CEQfYXDcO74A8sIBYcJMuCsis2E81zDxeENYhv66oZwLiA+Q==",
"hasInstallScript": true,
"dependencies": {
"@mapbox/node-pre-gyp": "^1.0.10",
"node-addon-api": "^5.0.0"
},
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/before-after-hook": { "node_modules/before-after-hook": {
"version": "2.2.3", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz",
@ -4132,6 +4221,14 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/chownr": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
"integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
"engines": {
"node": ">=10"
}
},
"node_modules/ci-info": { "node_modules/ci-info": {
"version": "3.5.0", "version": "3.5.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.5.0.tgz", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.5.0.tgz",
@ -4218,6 +4315,14 @@
"simple-swizzle": "^0.2.2" "simple-swizzle": "^0.2.2"
} }
}, },
"node_modules/color-support": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
"integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
"bin": {
"color-support": "bin.js"
}
},
"node_modules/color/node_modules/color-convert": { "node_modules/color/node_modules/color-convert": {
"version": "1.9.3", "version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@ -4262,6 +4367,11 @@
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
}, },
"node_modules/console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
"integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ=="
},
"node_modules/content-disposition": { "node_modules/content-disposition": {
"version": "0.5.4", "version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@ -4452,6 +4562,11 @@
"node": ">=0.4.0" "node": ">=0.4.0"
} }
}, },
"node_modules/delegates": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
"integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ=="
},
"node_modules/denque": { "node_modules/denque": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
@ -4482,6 +4597,14 @@
"npm": "1.2.8000 || >= 1.4.16" "npm": "1.2.8000 || >= 1.4.16"
} }
}, },
"node_modules/detect-libc": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz",
"integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==",
"engines": {
"node": ">=8"
}
},
"node_modules/detect-newline": { "node_modules/detect-newline": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
@ -4585,8 +4708,7 @@
"node_modules/emoji-regex": { "node_modules/emoji-regex": {
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
"dev": true
}, },
"node_modules/enabled": { "node_modules/enabled": {
"version": "2.0.0", "version": "2.0.0",
@ -4601,6 +4723,29 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/encoding": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
"optional": true,
"peer": true,
"dependencies": {
"iconv-lite": "^0.6.2"
}
},
"node_modules/encoding/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==",
"optional": true,
"peer": true,
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/error-ex": { "node_modules/error-ex": {
"version": "1.3.2", "version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
@ -5259,6 +5404,28 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/fs-minipass": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
"integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
"dependencies": {
"minipass": "^3.0.0"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/fs-minipass/node_modules/minipass": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/fs.realpath": { "node_modules/fs.realpath": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@ -5283,6 +5450,25 @@
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
}, },
"node_modules/gauge": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
"integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==",
"dependencies": {
"aproba": "^1.0.3 || ^2.0.0",
"color-support": "^1.1.2",
"console-control-strings": "^1.0.0",
"has-unicode": "^2.0.1",
"object-assign": "^4.1.1",
"signal-exit": "^3.0.0",
"string-width": "^4.2.3",
"strip-ansi": "^6.0.1",
"wide-align": "^1.1.2"
},
"engines": {
"node": ">=10"
}
},
"node_modules/gensync": { "node_modules/gensync": {
"version": "1.0.0-beta.2", "version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@ -5476,6 +5662,11 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/has-unicode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
"integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ=="
},
"node_modules/hash-base": { "node_modules/hash-base": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz",
@ -5726,7 +5917,6 @@
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true,
"engines": { "engines": {
"node": ">=8" "node": ">=8"
} }
@ -6497,9 +6687,9 @@
"dev": true "dev": true
}, },
"node_modules/json5": { "node_modules/json5": {
"version": "2.2.1", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true, "dev": true,
"bin": { "bin": {
"json5": "lib/cli.js" "json5": "lib/cli.js"
@ -6696,7 +6886,6 @@
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
"dev": true,
"dependencies": { "dependencies": {
"semver": "^6.0.0" "semver": "^6.0.0"
}, },
@ -6711,7 +6900,6 @@
"version": "6.3.0", "version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true,
"bin": { "bin": {
"semver": "bin/semver.js" "semver": "bin/semver.js"
} }
@ -6880,11 +7068,44 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/minipass": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-4.0.0.tgz",
"integrity": "sha512-g2Uuh2jEKoht+zvO6vJqXmYpflPqzRBT+Th2h01DKh5z7wbY/AZ2gCQ78cP70YoHPyFdY30YBV5WxgLOEwOykw==",
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/minizlib": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
"integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
"dependencies": {
"minipass": "^3.0.0",
"yallist": "^4.0.0"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/minizlib/node_modules/minipass": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/mkdirp": { "node_modules/mkdirp": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"dev": true,
"bin": { "bin": {
"mkdirp": "bin/cmd.js" "mkdirp": "bin/cmd.js"
}, },
@ -7004,6 +7225,11 @@
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="
}, },
"node_modules/node-addon-api": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.0.0.tgz",
"integrity": "sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA=="
},
"node_modules/node-fetch": { "node_modules/node-fetch": {
"version": "2.6.7", "version": "2.6.7",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
@ -9731,6 +9957,17 @@
"inBundle": true, "inBundle": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/npmlog": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
"integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==",
"dependencies": {
"are-we-there-yet": "^2.0.0",
"console-control-strings": "^1.1.0",
"gauge": "^3.0.0",
"set-blocking": "^2.0.0"
}
},
"node_modules/object-assign": { "node_modules/object-assign": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@ -10514,6 +10751,11 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
},
"node_modules/setprototypeof": { "node_modules/setprototypeof": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@ -10573,8 +10815,7 @@
"node_modules/signal-exit": { "node_modules/signal-exit": {
"version": "3.0.7", "version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
"dev": true
}, },
"node_modules/simple-swizzle": { "node_modules/simple-swizzle": {
"version": "0.2.2", "version": "0.2.2",
@ -10795,7 +11036,6 @@
"version": "4.2.3", "version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"dependencies": { "dependencies": {
"emoji-regex": "^8.0.0", "emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0", "is-fullwidth-code-point": "^3.0.0",
@ -10809,7 +11049,6 @@
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"dependencies": { "dependencies": {
"ansi-regex": "^5.0.1" "ansi-regex": "^5.0.1"
}, },
@ -10935,6 +11174,22 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/tar": {
"version": "6.1.13",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.13.tgz",
"integrity": "sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw==",
"dependencies": {
"chownr": "^2.0.0",
"fs-minipass": "^2.0.0",
"minipass": "^4.0.0",
"minizlib": "^2.1.1",
"mkdirp": "^1.0.3",
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/test-exclude": { "node_modules/test-exclude": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
@ -11276,6 +11531,14 @@
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
}, },
"node_modules/utility-types": {
"version": "3.10.0",
"resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.10.0.tgz",
"integrity": "sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==",
"engines": {
"node": ">= 4"
}
},
"node_modules/utils-merge": { "node_modules/utils-merge": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
@ -11403,6 +11666,14 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/wide-align": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
"integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
"dependencies": {
"string-width": "^1.0.2 || 2 || 3 || 4"
}
},
"node_modules/winston": { "node_modules/winston": {
"version": "3.8.2", "version": "3.8.2",
"resolved": "https://registry.npmjs.org/winston/-/winston-3.8.2.tgz", "resolved": "https://registry.npmjs.org/winston/-/winston-3.8.2.tgz",
@ -13768,6 +14039,32 @@
"resolved": "https://registry.npmjs.org/@juanelas/base64/-/base64-1.0.5.tgz", "resolved": "https://registry.npmjs.org/@juanelas/base64/-/base64-1.0.5.tgz",
"integrity": "sha512-gTIElNo4ohMcYUZzol/Hb6DYJzphxl0b1B4egJJ+JiqxqcOcWx4XLMAB+lhWuMsMX3uR1oc5hwPusU3lgc1FkQ==" "integrity": "sha512-gTIElNo4ohMcYUZzol/Hb6DYJzphxl0b1B4egJJ+JiqxqcOcWx4XLMAB+lhWuMsMX3uR1oc5hwPusU3lgc1FkQ=="
}, },
"@mapbox/node-pre-gyp": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz",
"integrity": "sha512-4ySo4CjzStuprMwk35H5pPbkymjv1SF3jGLj6rAHp/xT/RF7TL7bd9CTm1xDY49K2qF7jmR/g7k+SkLETP6opA==",
"requires": {
"detect-libc": "^2.0.0",
"https-proxy-agent": "^5.0.0",
"make-dir": "^3.1.0",
"node-fetch": "^2.6.7",
"nopt": "^5.0.0",
"npmlog": "^5.0.1",
"rimraf": "^3.0.2",
"semver": "^7.3.5",
"tar": "^6.1.11"
},
"dependencies": {
"nopt": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
"integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
"requires": {
"abbrev": "1"
}
}
}
},
"@maxmind/geoip2-node": { "@maxmind/geoip2-node": {
"version": "3.5.0", "version": "3.5.0",
"resolved": "https://registry.npmjs.org/@maxmind/geoip2-node/-/geoip2-node-3.5.0.tgz", "resolved": "https://registry.npmjs.org/@maxmind/geoip2-node/-/geoip2-node-3.5.0.tgz",
@ -14152,6 +14449,21 @@
"@babel/types": "^7.3.0" "@babel/types": "^7.3.0"
} }
}, },
"@types/bcrypt": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.0.tgz",
"integrity": "sha512-agtcFKaruL8TmcvqbndlqHPSJgsolhf/qPWchFlgnW1gECTN/nKbFcoFnvKAQRFfKbh+BO6A3SWdJu9t+xF3Lw==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/bcryptjs": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.2.tgz",
"integrity": "sha512-LiMQ6EOPob/4yUL66SZzu6Yh77cbzJFYll+ZfaPiPPFswtIlA/Fs1MzdKYA7JApHU49zQTbJGX3PDmCpIdDBRQ==",
"dev": true
},
"@types/body-parser": { "@types/body-parser": {
"version": "1.19.2", "version": "1.19.2",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz",
@ -14513,8 +14825,7 @@
"abbrev": { "abbrev": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
"dev": true
}, },
"accepts": { "accepts": {
"version": "1.3.8", "version": "1.3.8",
@ -14584,8 +14895,7 @@
"ansi-regex": { "ansi-regex": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
"dev": true
}, },
"ansi-styles": { "ansi-styles": {
"version": "4.3.0", "version": "4.3.0",
@ -14606,6 +14916,20 @@
"picomatch": "^2.0.4" "picomatch": "^2.0.4"
} }
}, },
"aproba": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
"integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ=="
},
"are-we-there-yet": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz",
"integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==",
"requires": {
"delegates": "^1.0.0",
"readable-stream": "^3.6.0"
}
},
"arg": { "arg": {
"version": "4.1.3", "version": "4.1.3",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
@ -14656,6 +14980,11 @@
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
}, },
"await-to-js": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/await-to-js/-/await-to-js-3.0.0.tgz",
"integrity": "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g=="
},
"axios": { "axios": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.1.3.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.1.3.tgz",
@ -14746,6 +15075,15 @@
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
}, },
"bcrypt": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.0.tgz",
"integrity": "sha512-RHBS7HI5N5tEnGTmtR/pppX0mmDSBpQ4aCBsj7CEQfYXDcO74A8sIBYcJMuCsis2E81zDxeENYhv66oZwLiA+Q==",
"requires": {
"@mapbox/node-pre-gyp": "^1.0.10",
"node-addon-api": "^5.0.0"
}
},
"before-after-hook": { "before-after-hook": {
"version": "2.2.3", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz",
@ -14973,6 +15311,11 @@
} }
} }
}, },
"chownr": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
"integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="
},
"ci-info": { "ci-info": {
"version": "3.5.0", "version": "3.5.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.5.0.tgz", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.5.0.tgz",
@ -15064,6 +15407,11 @@
"simple-swizzle": "^0.2.2" "simple-swizzle": "^0.2.2"
} }
}, },
"color-support": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
"integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg=="
},
"colorspace": { "colorspace": {
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz",
@ -15092,6 +15440,11 @@
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
}, },
"console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
"integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ=="
},
"content-disposition": { "content-disposition": {
"version": "0.5.4", "version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@ -15237,6 +15590,11 @@
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
}, },
"delegates": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
"integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ=="
},
"denque": { "denque": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
@ -15257,6 +15615,11 @@
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="
}, },
"detect-libc": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz",
"integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w=="
},
"detect-newline": { "detect-newline": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
@ -15336,8 +15699,7 @@
"emoji-regex": { "emoji-regex": {
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
"dev": true
}, },
"enabled": { "enabled": {
"version": "2.0.0", "version": "2.0.0",
@ -15349,6 +15711,28 @@
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="
}, },
"encoding": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
"optional": true,
"peer": true,
"requires": {
"iconv-lite": "^0.6.2"
},
"dependencies": {
"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==",
"optional": true,
"peer": true,
"requires": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
}
}
}
},
"error-ex": { "error-ex": {
"version": "1.3.2", "version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
@ -15856,6 +16240,24 @@
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="
}, },
"fs-minipass": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
"integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
"requires": {
"minipass": "^3.0.0"
},
"dependencies": {
"minipass": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
"requires": {
"yallist": "^4.0.0"
}
}
}
},
"fs.realpath": { "fs.realpath": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@ -15873,6 +16275,22 @@
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
}, },
"gauge": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
"integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==",
"requires": {
"aproba": "^1.0.3 || ^2.0.0",
"color-support": "^1.1.2",
"console-control-strings": "^1.0.0",
"has-unicode": "^2.0.1",
"object-assign": "^4.1.1",
"signal-exit": "^3.0.0",
"string-width": "^4.2.3",
"strip-ansi": "^6.0.1",
"wide-align": "^1.1.2"
}
},
"gensync": { "gensync": {
"version": "1.0.0-beta.2", "version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@ -16003,6 +16421,11 @@
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="
}, },
"has-unicode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
"integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ=="
},
"hash-base": { "hash-base": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz",
@ -16181,8 +16604,7 @@
"is-fullwidth-code-point": { "is-fullwidth-code-point": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
"dev": true
}, },
"is-generator-fn": { "is-generator-fn": {
"version": "2.1.0", "version": "2.1.0",
@ -16776,9 +17198,9 @@
"dev": true "dev": true
}, },
"json5": { "json5": {
"version": "2.2.1", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true "dev": true
}, },
"jsonwebtoken": { "jsonwebtoken": {
@ -16944,7 +17366,6 @@
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
"dev": true,
"requires": { "requires": {
"semver": "^6.0.0" "semver": "^6.0.0"
}, },
@ -16952,8 +17373,7 @@
"semver": { "semver": {
"version": "6.3.0", "version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
"dev": true
} }
} }
}, },
@ -17078,11 +17498,37 @@
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz",
"integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==" "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g=="
}, },
"minipass": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-4.0.0.tgz",
"integrity": "sha512-g2Uuh2jEKoht+zvO6vJqXmYpflPqzRBT+Th2h01DKh5z7wbY/AZ2gCQ78cP70YoHPyFdY30YBV5WxgLOEwOykw==",
"requires": {
"yallist": "^4.0.0"
}
},
"minizlib": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
"integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
"requires": {
"minipass": "^3.0.0",
"yallist": "^4.0.0"
},
"dependencies": {
"minipass": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
"requires": {
"yallist": "^4.0.0"
}
}
}
},
"mkdirp": { "mkdirp": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
"dev": true
}, },
"mmdb-lib": { "mmdb-lib": {
"version": "2.0.2", "version": "2.0.2",
@ -17173,6 +17619,11 @@
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="
}, },
"node-addon-api": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.0.0.tgz",
"integrity": "sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA=="
},
"node-fetch": { "node-fetch": {
"version": "2.6.7", "version": "2.6.7",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
@ -19082,6 +19533,17 @@
"path-key": "^3.0.0" "path-key": "^3.0.0"
} }
}, },
"npmlog": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
"integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==",
"requires": {
"are-we-there-yet": "^2.0.0",
"console-control-strings": "^1.1.0",
"gauge": "^3.0.0",
"set-blocking": "^2.0.0"
}
},
"object-assign": { "object-assign": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@ -19637,6 +20099,11 @@
"send": "0.18.0" "send": "0.18.0"
} }
}, },
"set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
},
"setprototypeof": { "setprototypeof": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@ -19684,8 +20151,7 @@
"signal-exit": { "signal-exit": {
"version": "3.0.7", "version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
"dev": true
}, },
"simple-swizzle": { "simple-swizzle": {
"version": "0.2.2", "version": "0.2.2",
@ -19860,7 +20326,6 @@
"version": "4.2.3", "version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"requires": { "requires": {
"emoji-regex": "^8.0.0", "emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0", "is-fullwidth-code-point": "^3.0.0",
@ -19871,7 +20336,6 @@
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"requires": { "requires": {
"ansi-regex": "^5.0.1" "ansi-regex": "^5.0.1"
} }
@ -19960,6 +20424,19 @@
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true "dev": true
}, },
"tar": {
"version": "6.1.13",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.13.tgz",
"integrity": "sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw==",
"requires": {
"chownr": "^2.0.0",
"fs-minipass": "^2.0.0",
"minipass": "^4.0.0",
"minizlib": "^2.1.1",
"mkdirp": "^1.0.3",
"yallist": "^4.0.0"
}
},
"test-exclude": { "test-exclude": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
@ -20178,6 +20655,11 @@
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
}, },
"utility-types": {
"version": "3.10.0",
"resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.10.0.tgz",
"integrity": "sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg=="
},
"utils-merge": { "utils-merge": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
@ -20277,6 +20759,14 @@
"isexe": "^2.0.0" "isexe": "^2.0.0"
} }
}, },
"wide-align": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
"integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
"requires": {
"string-width": "^1.0.2 || 2 || 3 || 4"
}
},
"winston": { "winston": {
"version": "3.8.2", "version": "3.8.2",
"resolved": "https://registry.npmjs.org/winston/-/winston-3.8.2.tgz", "resolved": "https://registry.npmjs.org/winston/-/winston-3.8.2.tgz",

@ -6,7 +6,9 @@
"@sentry/tracing": "^7.19.0", "@sentry/tracing": "^7.19.0",
"@types/crypto-js": "^4.1.1", "@types/crypto-js": "^4.1.1",
"@types/libsodium-wrappers": "^0.7.10", "@types/libsodium-wrappers": "^0.7.10",
"await-to-js": "^3.0.0",
"axios": "^1.1.3", "axios": "^1.1.3",
"bcrypt": "^5.1.0",
"bigint-conversion": "^2.2.2", "bigint-conversion": "^2.2.2",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"cors": "^2.8.5", "cors": "^2.8.5",
@ -29,6 +31,7 @@
"tweetnacl": "^1.0.3", "tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1", "tweetnacl-util": "^0.15.1",
"typescript": "^4.9.3", "typescript": "^4.9.3",
"utility-types": "^3.10.0",
"winston": "^3.8.2", "winston": "^3.8.2",
"winston-loki": "^6.0.6" "winston-loki": "^6.0.6"
}, },
@ -36,7 +39,6 @@
"version": "1.0.0", "version": "1.0.0",
"main": "src/index.js", "main": "src/index.js",
"scripts": { "scripts": {
"prepare": "cd .. && npm install",
"start": "npm run build && node build/index.js", "start": "npm run build && node build/index.js",
"dev": "nodemon", "dev": "nodemon",
"build": "rimraf ./build && tsc && cp -R ./src/templates ./build", "build": "rimraf ./build && tsc && cp -R ./src/templates ./build",
@ -62,6 +64,8 @@
"devDependencies": { "devDependencies": {
"@jest/globals": "^29.3.1", "@jest/globals": "^29.3.1",
"@posthog/plugin-scaffold": "^1.3.4", "@posthog/plugin-scaffold": "^1.3.4",
"@types/bcrypt": "^5.0.0",
"@types/bcryptjs": "^2.4.2",
"@types/cookie-parser": "^1.4.3", "@types/cookie-parser": "^1.4.3",
"@types/cors": "^2.8.12", "@types/cors": "^2.8.12",
"@types/express": "^4.17.14", "@types/express": "^4.17.14",

@ -1,7 +1,7 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const { patchRouterParam } = require('./utils/patchAsyncRoutes'); const { patchRouterParam } = require('./utils/patchAsyncRoutes');
import express from 'express'; import express, { Request, Response } from 'express';
import helmet from 'helmet'; import helmet from 'helmet';
import cors from 'cors'; import cors from 'cors';
import cookieParser from 'cookie-parser'; import cookieParser from 'cookie-parser';
@ -13,7 +13,9 @@ import { apiLimiter } from './helpers/rateLimiter';
import { import {
workspace as eeWorkspaceRouter, workspace as eeWorkspaceRouter,
secret as eeSecretRouter secret as eeSecretRouter,
secretSnapshot as eeSecretSnapshotRouter,
action as eeActionRouter
} from './ee/routes/v1'; } from './ee/routes/v1';
import { import {
signup as v1SignupRouter, signup as v1SignupRouter,
@ -36,9 +38,13 @@ import {
} from './routes/v1'; } from './routes/v1';
import { import {
secret as v2SecretRouter, secret as v2SecretRouter,
workspace as v2WorkspaceRouter workspace as v2WorkspaceRouter,
serviceTokenData as v2ServiceTokenDataRouter,
apiKeyData as v2APIKeyDataRouter,
} from './routes/v2'; } from './routes/v2';
import { healthCheck } from './routes/status';
import { getLogger } from './utils/logger'; import { getLogger } from './utils/logger';
import { RouteNotFoundError } from './utils/errors'; import { RouteNotFoundError } from './utils/errors';
import { requestErrorHandler } from './middleware/requestErrorHandler'; import { requestErrorHandler } from './middleware/requestErrorHandler';
@ -68,7 +74,9 @@ if (NODE_ENV === 'production') {
// (EE) routes // (EE) routes
app.use('/api/v1/secret', eeSecretRouter); app.use('/api/v1/secret', eeSecretRouter);
app.use('/api/v1/secret-snapshot', eeSecretSnapshotRouter);
app.use('/api/v1/workspace', eeWorkspaceRouter); app.use('/api/v1/workspace', eeWorkspaceRouter);
app.use('/api/v1/action', eeActionRouter);
// v1 routes // v1 routes
app.use('/api/v1/signup', v1SignupRouter); app.use('/api/v1/signup', v1SignupRouter);
@ -83,7 +91,7 @@ app.use('/api/v1/membership', v1MembershipRouter);
app.use('/api/v1/key', v1KeyRouter); app.use('/api/v1/key', v1KeyRouter);
app.use('/api/v1/invite-org', v1InviteOrgRouter); app.use('/api/v1/invite-org', v1InviteOrgRouter);
app.use('/api/v1/secret', v1SecretRouter); app.use('/api/v1/secret', v1SecretRouter);
app.use('/api/v1/service-token', v1ServiceTokenRouter); app.use('/api/v1/service-token', v1ServiceTokenRouter); // deprecate
app.use('/api/v1/password', v1PasswordRouter); app.use('/api/v1/password', v1PasswordRouter);
app.use('/api/v1/stripe', v1StripeRouter); app.use('/api/v1/stripe', v1StripeRouter);
app.use('/api/v1/integration', v1IntegrationRouter); app.use('/api/v1/integration', v1IntegrationRouter);
@ -92,12 +100,17 @@ app.use('/api/v1/integration-auth', v1IntegrationAuthRouter);
// v2 routes // v2 routes
app.use('/api/v2/workspace', v2WorkspaceRouter); app.use('/api/v2/workspace', v2WorkspaceRouter);
app.use('/api/v2/secret', v2SecretRouter); app.use('/api/v2/secret', v2SecretRouter);
app.use('/api/v2/service-token', v2ServiceTokenDataRouter);
app.use('/api/v2/api-key-data', v2APIKeyDataRouter);
// Server status
app.use('/api', healthCheck)
//* Handle unrouted requests and respond with proper error message as well as status code //* Handle unrouted requests and respond with proper error message as well as status code
app.use((req, res, next)=>{ app.use((req, res, next) => {
if(res.headersSent) return next(); if (res.headersSent) return next();
next(RouteNotFoundError({message: `The requested source '(${req.method})${req.url}' was not found`})) next(RouteNotFoundError({ message: `The requested source '(${req.method})${req.url}' was not found` }))
}) })
//* Error Handling Middleware (must be after all routing logic) //* Error Handling Middleware (must be after all routing logic)

@ -1,6 +1,7 @@
const PORT = process.env.PORT || 4000; const PORT = process.env.PORT || 4000;
const EMAIL_TOKEN_LIFETIME = process.env.EMAIL_TOKEN_LIFETIME! || '86400'; const EMAIL_TOKEN_LIFETIME = process.env.EMAIL_TOKEN_LIFETIME! || '86400';
const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY!; const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY!;
const SALT_ROUNDS = parseInt(process.env.SALT_ROUNDS!) || 10;
const JWT_AUTH_LIFETIME = process.env.JWT_AUTH_LIFETIME! || '10d'; const JWT_AUTH_LIFETIME = process.env.JWT_AUTH_LIFETIME! || '10d';
const JWT_AUTH_SECRET = process.env.JWT_AUTH_SECRET!; const JWT_AUTH_SECRET = process.env.JWT_AUTH_SECRET!;
const JWT_REFRESH_LIFETIME = process.env.JWT_REFRESH_LIFETIME! || '90d'; const JWT_REFRESH_LIFETIME = process.env.JWT_REFRESH_LIFETIME! || '90d';
@ -47,6 +48,7 @@ export {
PORT, PORT,
EMAIL_TOKEN_LIFETIME, EMAIL_TOKEN_LIFETIME,
ENCRYPTION_KEY, ENCRYPTION_KEY,
SALT_ROUNDS,
JWT_AUTH_LIFETIME, JWT_AUTH_LIFETIME,
JWT_AUTH_SECRET, JWT_AUTH_SECRET,
JWT_REFRESH_LIFETIME, JWT_REFRESH_LIFETIME,

@ -2,7 +2,6 @@ import { Request, Response } from 'express';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { Key } from '../../models'; import { Key } from '../../models';
import { findMembership } from '../../helpers/membership'; import { findMembership } from '../../helpers/membership';
import { GRANTED } from '../../variables';
/** /**
* Add (encrypted) copy of workspace key for workspace with id [workspaceId] for user with * Add (encrypted) copy of workspace key for workspace with id [workspaceId] for user with
@ -26,9 +25,6 @@ export const uploadKey = async (req: Request, res: Response) => {
throw new Error('Failed receiver membership validation for workspace'); throw new Error('Failed receiver membership validation for workspace');
} }
receiverMembership.status = GRANTED;
await receiverMembership.save();
await new Key({ await new Key({
encryptedKey: key.encryptedKey, encryptedKey: key.encryptedKey,
nonce: key.nonce, nonce: key.nonce,

@ -7,7 +7,7 @@ import {
} from '../../helpers/membership'; } from '../../helpers/membership';
import { sendMail } from '../../helpers/nodemailer'; import { sendMail } from '../../helpers/nodemailer';
import { SITE_URL } from '../../config'; import { SITE_URL } from '../../config';
import { ADMIN, MEMBER, GRANTED, ACCEPTED } from '../../variables'; import { ADMIN, MEMBER, ACCEPTED } from '../../variables';
/** /**
* Check that user is a member of workspace with id [workspaceId] * Check that user is a member of workspace with id [workspaceId]
@ -175,8 +175,7 @@ export const inviteUserToWorkspace = async (req: Request, res: Response) => {
// already a member of the workspace // already a member of the workspace
const inviteeMembership = await Membership.findOne({ const inviteeMembership = await Membership.findOne({
user: invitee._id, user: invitee._id,
workspace: workspaceId, workspace: workspaceId
status: GRANTED
}); });
if (inviteeMembership) if (inviteeMembership)
@ -205,8 +204,7 @@ export const inviteUserToWorkspace = async (req: Request, res: Response) => {
const m = await new Membership({ const m = await new Membership({
user: invitee._id, user: invitee._id,
workspace: workspaceId, workspace: workspaceId,
role: MEMBER, role: MEMBER
status: GRANTED
}).save(); }).save();
await sendMail({ await sendMail({

@ -123,7 +123,9 @@ export const pullSecrets = async (req: Request, res: Response) => {
secrets = await pull({ secrets = await pull({
userId: req.user._id.toString(), userId: req.user._id.toString(),
workspaceId, workspaceId,
environment environment,
channel: channel ? channel : 'cli',
ipAddress: req.ip
}); });
key = await Key.findOne({ key = await Key.findOne({
@ -188,7 +190,9 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => {
secrets = await pull({ secrets = await pull({
userId: req.serviceToken.user._id.toString(), userId: req.serviceToken.user._id.toString(),
workspaceId, workspaceId,
environment environment,
channel: 'cli',
ipAddress: req.ip
}); });
key = { key = {

@ -11,7 +11,6 @@ import { JWT_SERVICE_SECRET } from '../../config';
* @returns * @returns
*/ */
export const getServiceToken = async (req: Request, res: Response) => { export const getServiceToken = async (req: Request, res: Response) => {
// get service token
return res.status(200).send({ return res.status(200).send({
serviceToken: req.serviceToken serviceToken: req.serviceToken
}); });
@ -73,4 +72,4 @@ export const createServiceToken = async (req: Request, res: Response) => {
return res.status(200).send({ return res.status(200).send({
token token
}); });
}; };

@ -8,13 +8,14 @@ import {
IntegrationAuth, IntegrationAuth,
IUser, IUser,
ServiceToken, ServiceToken,
ServiceTokenData
} from '../../models'; } from '../../models';
import { import {
createWorkspace as create, createWorkspace as create,
deleteWorkspace as deleteWork deleteWorkspace as deleteWork
} from '../../helpers/workspace'; } from '../../helpers/workspace';
import { addMemberships } from '../../helpers/membership'; import { addMemberships } from '../../helpers/membership';
import { ADMIN, COMPLETED, GRANTED } from '../../variables'; import { ADMIN } from '../../variables';
/** /**
* Return public keys of members of workspace with id [workspaceId] * Return public keys of members of workspace with id [workspaceId]
@ -32,13 +33,12 @@ export const getWorkspacePublicKeys = async (req: Request, res: Response) => {
workspace: workspaceId workspace: workspaceId
}).populate<{ user: IUser }>('user', 'publicKey') }).populate<{ user: IUser }>('user', 'publicKey')
) )
.filter((m) => m.status === COMPLETED || m.status === GRANTED) .map((member) => {
.map((member) => { return {
return { publicKey: member.user.publicKey,
publicKey: member.user.publicKey, userId: member.user._id
userId: member.user._id };
}; });
});
} catch (err) { } catch (err) {
Sentry.setUser({ email: req.user.email }); Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); Sentry.captureException(err);
@ -168,8 +168,7 @@ export const createWorkspace = async (req: Request, res: Response) => {
await addMemberships({ await addMemberships({
userIds: [req.user._id], userIds: [req.user._id],
workspaceId: workspace._id.toString(), workspaceId: workspace._id.toString(),
roles: [ADMIN], roles: [ADMIN]
statuses: [GRANTED]
}); });
} catch (err) { } catch (err) {
Sentry.setUser({ email: req.user.email }); Sentry.setUser({ email: req.user.email });

@ -0,0 +1,106 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import crypto from 'crypto';
import bcrypt from 'bcrypt';
import {
APIKeyData
} from '../../models';
import {
SALT_ROUNDS
} from '../../config';
/**
* Return API key data for user with id [req.user_id]
* @param req
* @param res
* @returns
*/
export const getAPIKeyData = async (req: Request, res: Response) => {
let apiKeyData;
try {
apiKeyData = await APIKeyData.find({
user: req.user._id
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get API key data'
});
}
return res.status(200).send({
apiKeyData
});
}
/**
* Create new API key data for user with id [req.user._id]
* @param req
* @param res
*/
export const createAPIKeyData = async (req: Request, res: Response) => {
let apiKey, apiKeyData;
try {
const { name, expiresIn } = req.body;
const secret = crypto.randomBytes(16).toString('hex');
const secretHash = await bcrypt.hash(secret, SALT_ROUNDS);
const expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
apiKeyData = await new APIKeyData({
name,
expiresAt,
user: req.user._id,
secretHash
}).save();
// return api key data without sensitive data
apiKeyData = await APIKeyData.findById(apiKeyData._id);
if (!apiKeyData) throw new Error('Failed to find API key data');
apiKey = `ak.${apiKeyData._id.toString()}.${secret}`;
} catch (err) {
console.error(err);
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to API key data'
});
}
return res.status(200).send({
apiKey,
apiKeyData
});
}
/**
* Delete API key data with id [apiKeyDataId].
* @param req
* @param res
* @returns
*/
export const deleteAPIKeyData = async (req: Request, res: Response) => {
let apiKeyData;
try {
const { apiKeyDataId } = req.params;
apiKeyData = await APIKeyData.findByIdAndDelete(apiKeyDataId);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete API key data'
});
}
return res.status(200).send({
apiKeyData
});
}

@ -1,5 +1,11 @@
import * as workspaceController from './workspaceController'; import * as workspaceController from './workspaceController';
import * as serviceTokenDataController from './serviceTokenDataController';
import * as apiKeyDataController from './apiKeyDataController';
import * as secretController from './secretController';
export { export {
workspaceController workspaceController,
serviceTokenDataController,
apiKeyDataController,
secretController
} }

@ -0,0 +1,276 @@
import to from "await-to-js";
import { Request, Response } from "express";
import mongoose, { Types } from "mongoose";
import Secret, { ISecret } from "../../models/secret";
import { CreateSecretRequestBody, ModifySecretRequestBody, SanitizedSecretForCreate, SanitizedSecretModify } from "../../types/secret";
const { ValidationError } = mongoose.Error;
import { BadRequestError, InternalServerError, UnauthorizedRequestError, ValidationError as RouteValidationError } from '../../utils/errors';
import { AnyBulkWriteOperation } from 'mongodb';
import { SECRET_PERSONAL, SECRET_SHARED } from "../../variables";
import { validateMembership } from "../../helpers/membership";
import { ADMIN, MEMBER } from '../../variables';
export const createSingleSecret = async (req: Request, res: Response) => {
const secretToCreate: CreateSecretRequestBody = req.body.secret;
const { workspaceId, environmentName } = req.params
const sanitizedSecret: SanitizedSecretForCreate = {
secretKeyCiphertext: secretToCreate.secretKeyCiphertext,
secretKeyIV: secretToCreate.secretKeyIV,
secretKeyTag: secretToCreate.secretKeyTag,
secretKeyHash: secretToCreate.secretKeyHash,
secretValueCiphertext: secretToCreate.secretValueCiphertext,
secretValueIV: secretToCreate.secretValueIV,
secretValueTag: secretToCreate.secretValueTag,
secretValueHash: secretToCreate.secretValueHash,
secretCommentCiphertext: secretToCreate.secretCommentCiphertext,
secretCommentIV: secretToCreate.secretCommentIV,
secretCommentTag: secretToCreate.secretCommentTag,
secretCommentHash: secretToCreate.secretCommentHash,
workspace: new Types.ObjectId(workspaceId),
environment: environmentName,
type: secretToCreate.type,
user: new Types.ObjectId(req.user._id)
}
const [error, newlyCreatedSecret] = await to(Secret.create(sanitizedSecret).then())
if (error instanceof ValidationError) {
throw RouteValidationError({ message: error.message, stack: error.stack })
}
res.status(200).send()
}
export const batchCreateSecrets = async (req: Request, res: Response) => {
const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets;
const { workspaceId, environmentName } = req.params
const sanitizedSecretesToCreate: SanitizedSecretForCreate[] = []
secretsToCreate.forEach(rawSecret => {
const safeUpdateFields: SanitizedSecretForCreate = {
secretKeyCiphertext: rawSecret.secretKeyCiphertext,
secretKeyIV: rawSecret.secretKeyIV,
secretKeyTag: rawSecret.secretKeyTag,
secretKeyHash: rawSecret.secretKeyHash,
secretValueCiphertext: rawSecret.secretValueCiphertext,
secretValueIV: rawSecret.secretValueIV,
secretValueTag: rawSecret.secretValueTag,
secretValueHash: rawSecret.secretValueHash,
secretCommentCiphertext: rawSecret.secretCommentCiphertext,
secretCommentIV: rawSecret.secretCommentIV,
secretCommentTag: rawSecret.secretCommentTag,
secretCommentHash: rawSecret.secretCommentHash,
workspace: new Types.ObjectId(workspaceId),
environment: environmentName,
type: rawSecret.type,
user: new Types.ObjectId(req.user._id)
}
sanitizedSecretesToCreate.push(safeUpdateFields)
})
const [bulkCreateError, newlyCreatedSecrets] = await to(Secret.insertMany(sanitizedSecretesToCreate).then())
if (bulkCreateError) {
if (bulkCreateError instanceof ValidationError) {
throw RouteValidationError({ message: bulkCreateError.message, stack: bulkCreateError.stack })
}
throw InternalServerError({ message: "Unable to process your batch create request. Please try again", stack: bulkCreateError.stack })
}
res.status(200).send()
}
export const batchDeleteSecrets = async (req: Request, res: Response) => {
const { workspaceId, environmentName } = req.params
const secretIdsToDelete: string[] = req.body.secretIds
const [secretIdsUserCanDeleteError, secretIdsUserCanDelete] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then())
if (secretIdsUserCanDeleteError) {
throw InternalServerError({ message: `Unable to fetch secrets you own: [error=${secretIdsUserCanDeleteError.message}]` })
}
const secretsUserCanDeleteSet: Set<string> = new Set(secretIdsUserCanDelete.map(objectId => objectId._id.toString()));
const deleteOperationsToPerform: AnyBulkWriteOperation<ISecret>[] = []
secretIdsToDelete.forEach(secretIdToDelete => {
if (secretsUserCanDeleteSet.has(secretIdToDelete)) {
const deleteOperation = { deleteOne: { filter: { _id: new Types.ObjectId(secretIdToDelete) } } }
deleteOperationsToPerform.push(deleteOperation)
} else {
throw RouteValidationError({ message: "You cannot delete secrets that you do not have access to" })
}
})
const [bulkDeleteError, bulkDelete] = await to(Secret.bulkWrite(deleteOperationsToPerform).then())
if (bulkDeleteError) {
if (bulkDeleteError instanceof ValidationError) {
throw RouteValidationError({ message: "Unable to apply modifications, please try again", stack: bulkDeleteError.stack })
}
throw InternalServerError()
}
res.status(200).send()
}
export const deleteSingleSecret = async (req: Request, res: Response) => {
const { secretId } = req.params;
const [error, singleSecretRetrieved] = await to(Secret.findById(secretId).then())
if (error instanceof ValidationError) {
throw RouteValidationError({ message: "Unable to get secret, please try again", stack: error.stack })
}
if (singleSecretRetrieved) {
const [membershipValidationError, membership] = await to(validateMembership({
userId: req.user._id,
workspaceId: singleSecretRetrieved.workspace._id.toString(),
acceptedRoles: [ADMIN, MEMBER]
}))
if (membershipValidationError || !membership) {
throw UnauthorizedRequestError()
}
await Secret.findByIdAndDelete(secretId)
res.status(200).send()
} else {
throw BadRequestError()
}
}
export const batchModifySecrets = async (req: Request, res: Response) => {
const { workspaceId, environmentName } = req.params
const secretsModificationsRequested: ModifySecretRequestBody[] = req.body.secrets;
const [secretIdsUserCanModifyError, secretIdsUserCanModify] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then())
if (secretIdsUserCanModifyError) {
throw InternalServerError({ message: "Unable to fetch secrets you own" })
}
const secretsUserCanModifySet: Set<string> = new Set(secretIdsUserCanModify.map(objectId => objectId._id.toString()));
const updateOperationsToPerform: any = []
secretsModificationsRequested.forEach(userModifiedSecret => {
if (secretsUserCanModifySet.has(userModifiedSecret._id.toString())) {
const sanitizedSecret: SanitizedSecretModify = {
secretKeyCiphertext: userModifiedSecret.secretKeyCiphertext,
secretKeyIV: userModifiedSecret.secretKeyIV,
secretKeyTag: userModifiedSecret.secretKeyTag,
secretKeyHash: userModifiedSecret.secretKeyHash,
secretValueCiphertext: userModifiedSecret.secretValueCiphertext,
secretValueIV: userModifiedSecret.secretValueIV,
secretValueTag: userModifiedSecret.secretValueTag,
secretValueHash: userModifiedSecret.secretValueHash,
secretCommentCiphertext: userModifiedSecret.secretCommentCiphertext,
secretCommentIV: userModifiedSecret.secretCommentIV,
secretCommentTag: userModifiedSecret.secretCommentTag,
secretCommentHash: userModifiedSecret.secretCommentHash,
}
const updateOperation = { updateOne: { filter: { _id: userModifiedSecret._id, workspace: workspaceId }, update: { $inc: { version: 1 }, $set: sanitizedSecret } } }
updateOperationsToPerform.push(updateOperation)
} else {
throw UnauthorizedRequestError({ message: "You do not have permission to modify one or more of the requested secrets" })
}
})
const [bulkModificationInfoError, bulkModificationInfo] = await to(Secret.bulkWrite(updateOperationsToPerform).then())
if (bulkModificationInfoError) {
if (bulkModificationInfoError instanceof ValidationError) {
throw RouteValidationError({ message: "Unable to apply modifications, please try again", stack: bulkModificationInfoError.stack })
}
throw InternalServerError()
}
return res.status(200).send()
}
export const modifySingleSecrets = async (req: Request, res: Response) => {
const { workspaceId, environmentName } = req.params
const secretModificationsRequested: ModifySecretRequestBody = req.body.secret;
const [secretIdUserCanModifyError, secretIdUserCanModify] = await to(Secret.findOne({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then())
if (secretIdUserCanModifyError && !secretIdUserCanModify) {
throw BadRequestError()
}
const sanitizedSecret: SanitizedSecretModify = {
secretKeyCiphertext: secretModificationsRequested.secretKeyCiphertext,
secretKeyIV: secretModificationsRequested.secretKeyIV,
secretKeyTag: secretModificationsRequested.secretKeyTag,
secretKeyHash: secretModificationsRequested.secretKeyHash,
secretValueCiphertext: secretModificationsRequested.secretValueCiphertext,
secretValueIV: secretModificationsRequested.secretValueIV,
secretValueTag: secretModificationsRequested.secretValueTag,
secretValueHash: secretModificationsRequested.secretValueHash,
secretCommentCiphertext: secretModificationsRequested.secretCommentCiphertext,
secretCommentIV: secretModificationsRequested.secretCommentIV,
secretCommentTag: secretModificationsRequested.secretCommentTag,
secretCommentHash: secretModificationsRequested.secretCommentHash,
}
const [error, singleModificationUpdate] = await to(Secret.updateOne({ _id: secretModificationsRequested._id, workspace: workspaceId }, { $inc: { version: 1 }, $set: sanitizedSecret }).then())
if (error instanceof ValidationError) {
throw RouteValidationError({ message: "Unable to apply modifications, please try again", stack: error.stack })
}
return res.status(200).send(singleModificationUpdate)
}
export const fetchAllSecrets = async (req: Request, res: Response) => {
const { environment } = req.query;
const { workspaceId } = req.params;
let userId: string | undefined = undefined // Used for choosing the personal secrets to fetch in
if (req.user) {
userId = req.user._id.toString();
}
if (req.serviceTokenData) {
userId = req.serviceTokenData.user._id
}
const [retriveAllSecretsError, allSecrets] = await to(Secret.find(
{
workspace: workspaceId,
environment,
$or: [{ user: userId }, { user: { $exists: false } }],
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
}
).then())
if (retriveAllSecretsError instanceof ValidationError) {
throw RouteValidationError({ message: "Unable to get secrets, please try again", stack: retriveAllSecretsError.stack })
}
return res.json(allSecrets)
}
export const fetchSingleSecret = async (req: Request, res: Response) => {
const { secretId } = req.params;
const [error, singleSecretRetrieved] = await to(Secret.findById(secretId).then())
if (error instanceof ValidationError) {
throw RouteValidationError({ message: "Unable to get secret, please try again", stack: error.stack })
}
if (singleSecretRetrieved) {
const [membershipValidationError, membership] = await to(validateMembership({
userId: req.user._id,
workspaceId: singleSecretRetrieved.workspace._id.toString(),
acceptedRoles: [ADMIN, MEMBER]
}))
if (membershipValidationError || !membership) {
throw UnauthorizedRequestError()
}
res.json(singleSecretRetrieved)
} else {
throw BadRequestError()
}
}

@ -0,0 +1,103 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import crypto from 'crypto';
import bcrypt from 'bcrypt';
import {
ServiceTokenData
} from '../../models';
import {
SALT_ROUNDS
} from '../../config';
/**
* Return service token data associated with service token on request
* @param req
* @param res
* @returns
*/
export const getServiceTokenData = async (req: Request, res: Response) => res.status(200).json(req.serviceTokenData);
/**
* Create new service token data for workspace with id [workspaceId] and
* environment [environment].
* @param req
* @param res
* @returns
*/
export const createServiceTokenData = async (req: Request, res: Response) => {
let serviceToken, serviceTokenData;
try {
const {
name,
workspaceId,
environment,
encryptedKey,
iv,
tag,
expiresIn
} = req.body;
const secret = crypto.randomBytes(16).toString('hex');
const secretHash = await bcrypt.hash(secret, SALT_ROUNDS);
const expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
serviceTokenData = await new ServiceTokenData({
name,
workspace: workspaceId,
environment,
user: req.user._id,
expiresAt,
secretHash,
encryptedKey,
iv,
tag
}).save();
// return service token data without sensitive data
serviceTokenData = await ServiceTokenData.findById(serviceTokenData._id);
if (!serviceTokenData) throw new Error('Failed to find service token data');
serviceToken = `st.${serviceTokenData._id.toString()}.${secret}`;
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to create service token data'
});
}
return res.status(200).send({
serviceToken,
serviceTokenData
});
}
/**
* Delete service token data with id [serviceTokenDataId].
* @param req
* @param res
* @returns
*/
export const deleteServiceTokenData = async (req: Request, res: Response) => {
let serviceTokenData;
try {
const { serviceTokenDataId } = req.params;
serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete service token data'
});
}
return res.status(200).send({
serviceTokenData
});
}

@ -6,24 +6,21 @@ import {
MembershipOrg, MembershipOrg,
Integration, Integration,
IntegrationAuth, IntegrationAuth,
Key, Key,
IUser, IUser,
ServiceToken, ServiceToken,
ServiceTokenData
} from '../../models'; } from '../../models';
import {
createWorkspace as create,
deleteWorkspace as deleteWork
} from '../../helpers/workspace';
import { import {
v2PushSecrets as push, v2PushSecrets as push,
pullSecrets as pull, pullSecrets as pull,
reformatPullSecrets reformatPullSecrets
} from '../../helpers/secret'; } from '../../helpers/secret';
import { pushKeys } from '../../helpers/key'; import { pushKeys } from '../../helpers/key';
import { addMemberships } from '../../helpers/membership';
import { postHogClient, EventService } from '../../services'; import { postHogClient, EventService } from '../../services';
import { eventPushSecrets } from '../../events'; import { eventPushSecrets } from '../../events';
import { ADMIN, COMPLETED, GRANTED, ENV_SET } from '../../variables'; import { ENV_SET } from '../../variables';
interface V2PushSecret { interface V2PushSecret {
type: string; // personal or shared type: string; // personal or shared
secretKeyCiphertext: string; secretKeyCiphertext: string;
@ -40,326 +37,6 @@ interface V2PushSecret {
secretCommentHash?: string; secretCommentHash?: string;
} }
/**
* Return public keys of members of workspace with id [workspaceId]
* @param req
* @param res
* @returns
*/
export const getWorkspacePublicKeys = async (req: Request, res: Response) => {
let publicKeys;
try {
const { workspaceId } = req.params;
publicKeys = (
await Membership.find({
workspace: workspaceId
}).populate<{ user: IUser }>('user', 'publicKey')
)
.filter((m) => m.status === COMPLETED || m.status === GRANTED)
.map((member) => {
return {
publicKey: member.user.publicKey,
userId: member.user._id
};
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get workspace member public keys'
});
}
return res.status(200).send({
publicKeys
});
};
/**
* Return memberships for workspace with id [workspaceId]
* @param req
* @param res
* @returns
*/
export const getWorkspaceMemberships = async (req: Request, res: Response) => {
let users;
try {
const { workspaceId } = req.params;
users = await Membership.find({
workspace: workspaceId
}).populate('user', '+publicKey');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get workspace members'
});
}
return res.status(200).send({
users
});
};
/**
* Return workspaces that user is part of
* @param req
* @param res
* @returns
*/
export const getWorkspaces = async (req: Request, res: Response) => {
let workspaces;
try {
workspaces = (
await Membership.find({
user: req.user._id
}).populate('workspace')
).map((m) => m.workspace);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get workspaces'
});
}
return res.status(200).send({
workspaces
});
};
/**
* Return workspace with id [workspaceId]
* @param req
* @param res
* @returns
*/
export const getWorkspace = async (req: Request, res: Response) => {
let workspace;
try {
const { workspaceId } = req.params;
workspace = await Workspace.findOne({
_id: workspaceId
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get workspace'
});
}
return res.status(200).send({
workspace
});
};
/**
* Create new workspace named [workspaceName] under organization with id
* [organizationId] and add user as admin
* @param req
* @param res
* @returns
*/
export const createWorkspace = async (req: Request, res: Response) => {
let workspace;
try {
const { workspaceName, organizationId } = req.body;
// validate organization membership
const membershipOrg = await MembershipOrg.findOne({
user: req.user._id,
organization: organizationId
});
if (!membershipOrg) {
throw new Error('Failed to validate organization membership');
}
if (workspaceName.length < 1) {
throw new Error('Workspace names must be at least 1-character long');
}
// create workspace and add user as member
workspace = await create({
name: workspaceName,
organizationId
});
await addMemberships({
userIds: [req.user._id],
workspaceId: workspace._id.toString(),
roles: [ADMIN],
statuses: [GRANTED]
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to create workspace'
});
}
return res.status(200).send({
workspace
});
};
/**
* Delete workspace with id [workspaceId]
* @param req
* @param res
* @returns
*/
export const deleteWorkspace = async (req: Request, res: Response) => {
try {
const { workspaceId } = req.params;
// delete workspace
await deleteWork({
id: workspaceId
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete workspace'
});
}
return res.status(200).send({
message: 'Successfully deleted workspace'
});
};
/**
* Change name of workspace with id [workspaceId] to [name]
* @param req
* @param res
* @returns
*/
export const changeWorkspaceName = async (req: Request, res: Response) => {
let workspace;
try {
const { workspaceId } = req.params;
const { name } = req.body;
workspace = await Workspace.findOneAndUpdate(
{
_id: workspaceId
},
{
name
},
{
new: true
}
);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to change workspace name'
});
}
return res.status(200).send({
message: 'Successfully changed workspace name',
workspace
});
};
/**
* Return integrations for workspace with id [workspaceId]
* @param req
* @param res
* @returns
*/
export const getWorkspaceIntegrations = async (req: Request, res: Response) => {
let integrations;
try {
const { workspaceId } = req.params;
integrations = await Integration.find({
workspace: workspaceId
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get workspace integrations'
});
}
return res.status(200).send({
integrations
});
};
/**
* Return (integration) authorizations for workspace with id [workspaceId]
* @param req
* @param res
* @returns
*/
export const getWorkspaceIntegrationAuthorizations = async (
req: Request,
res: Response
) => {
let authorizations;
try {
const { workspaceId } = req.params;
authorizations = await IntegrationAuth.find({
workspace: workspaceId
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get workspace integration authorizations'
});
}
return res.status(200).send({
authorizations
});
};
/**
* Return service service tokens for workspace [workspaceId] belonging to user
* @param req
* @param res
* @returns
*/
export const getWorkspaceServiceTokens = async (
req: Request,
res: Response
) => {
let serviceTokens;
try {
const { workspaceId } = req.params;
serviceTokens = await ServiceToken.find({
user: req.user._id,
workspace: workspaceId
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get workspace service tokens'
});
}
return res.status(200).send({
serviceTokens
});
}
/** /**
* Upload (encrypted) secrets to workspace with id [workspaceId] * Upload (encrypted) secrets to workspace with id [workspaceId]
* for environment [environment] * for environment [environment]
@ -369,7 +46,6 @@ export const getWorkspaceServiceTokens = async (
*/ */
export const pushWorkspaceSecrets = async (req: Request, res: Response) => { export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
// upload (encrypted) secrets to workspace with id [workspaceId] // upload (encrypted) secrets to workspace with id [workspaceId]
try { try {
let { secrets }: { secrets: V2PushSecret[] } = req.body; let { secrets }: { secrets: V2PushSecret[] } = req.body;
const { keys, environment, channel } = req.body; const { keys, environment, channel } = req.body;
@ -389,7 +65,9 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
userId: req.user._id, userId: req.user._id,
workspaceId, workspaceId,
environment, environment,
secrets secrets,
channel: channel ? channel : 'cli',
ipAddress: req.ip
}); });
await pushKeys({ await pushKeys({
@ -397,8 +75,7 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
workspaceId, workspaceId,
keys keys
}); });
if (postHogClient) { if (postHogClient) {
postHogClient.capture({ postHogClient.capture({
event: 'secrets pushed', event: 'secrets pushed',
@ -434,39 +111,33 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
/** /**
* Return (encrypted) secrets for workspace with id [workspaceId] * Return (encrypted) secrets for workspace with id [workspaceId]
* for environment [environment] and (encrypted) workspace key * for environment [environment]
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const pullSecrets = async (req: Request, res: Response) => { export const pullSecrets = async (req: Request, res: Response) => {
// TODO: only return secrets, do not return workspace key
let secrets; let secrets;
let key;
try { try {
const environment: string = req.query.environment as string; const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string; const channel: string = req.query.channel as string;
const { workspaceId } = req.params; const { workspaceId } = req.params;
// validate environment let userId;
if (!ENV_SET.has(environment)) { if (req.user) {
throw new Error('Failed to validate environment'); userId = req.user._id.toString();
} else if (req.serviceTokenData) {
userId = req.serviceTokenData.user._id
} }
secrets = await pull({ secrets = await pull({
userId: req.user._id.toString(), userId,
workspaceId, workspaceId,
environment environment,
channel: channel ? channel : 'cli',
ipAddress: req.ip
}); });
key = await Key.findOne({
workspace: workspaceId,
receiver: req.user._id
})
.sort({ createdAt: -1 })
.populate('sender', '+publicKey');
if (channel !== 'cli') { if (channel !== 'cli') {
secrets = reformatPullSecrets({ secrets }); secrets = reformatPullSecrets({ secrets });
} }
@ -493,73 +164,54 @@ export const pullSecrets = async (req: Request, res: Response) => {
} }
return res.status(200).send({ return res.status(200).send({
secrets, secrets
key
}); });
}; };
// TODO: modify based on upcoming serviceTokenData changes export const getWorkspaceKey = async (req: Request, res: Response) => {
/**
* Return (encrypted) secrets for workspace with id [workspaceId]
* for environment [environment] and (encrypted) workspace key
* via service token
* @param req
* @param res
* @returns
*/
export const pullSecretsServiceToken = async (req: Request, res: Response) => {
let secrets;
let key; let key;
try { try {
const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string;
const { workspaceId } = req.params; const { workspaceId } = req.params;
// validate environment key = await Key.findOne({
if (!ENV_SET.has(environment)) { workspace: workspaceId,
throw new Error('Failed to validate environment'); receiver: req.user._id
} }).populate('sender', '+publicKey');
secrets = await pull({ if (!key) throw new Error('Failed to find workspace key');
userId: req.serviceToken.user._id.toString(),
workspaceId,
environment
});
key = {
encryptedKey: req.serviceToken.encryptedKey,
nonce: req.serviceToken.nonce,
sender: {
publicKey: req.serviceToken.publicKey
},
receiver: req.serviceToken.user,
workspace: req.serviceToken.workspace
};
if (postHogClient) {
// capture secrets pulled event in production
postHogClient.capture({
distinctId: req.serviceToken.user.email,
event: 'secrets pulled',
properties: {
numberOfSecrets: secrets.length,
environment,
workspaceId,
channel: channel ? channel : 'cli'
}
});
}
} catch (err) { } catch (err) {
Sentry.setUser({ email: req.serviceToken.user.email }); Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); Sentry.captureException(err);
return res.status(400).send({ return res.status(400).send({
message: 'Failed to pull workspace secrets' message: 'Failed to get workspace key'
});
}
return res.status(200).json(key);
}
export const getWorkspaceServiceTokenData = async (
req: Request,
res: Response
) => {
let serviceTokenData;
try {
const { workspaceId } = req.params;
serviceTokenData = await ServiceTokenData
.find({
workspace: workspaceId
})
.select('+encryptedKey +iv +tag');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get workspace service token data'
}); });
} }
return res.status(200).send({ return res.status(200).send({
secrets: reformatPullSecrets({ secrets }), serviceTokenData
key
}); });
}; }

@ -0,0 +1,31 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Action, SecretVersion } from '../../models';
import { ActionNotFoundError } from '../../../utils/errors';
export const getAction = async (req: Request, res: Response) => {
let action;
try {
const { actionId } = req.params;
action = await Action
.findById(actionId)
.populate([
'payload.secretVersions.oldSecretVersion',
'payload.secretVersions.newSecretVersion'
]);
if (!action) throw ActionNotFoundError({
message: 'Failed to find action'
});
} catch (err) {
throw ActionNotFoundError({
message: 'Failed to find action'
});
}
return res.status(200).send({
action
});
}

@ -1,9 +1,13 @@
import * as stripeController from './stripeController'; import * as stripeController from './stripeController';
import * as secretController from './secretController'; import * as secretController from './secretController';
import * as secretSnapshotController from './secretSnapshotController';
import * as workspaceController from './workspaceController'; import * as workspaceController from './workspaceController';
import * as actionController from './actionController';
export { export {
stripeController, stripeController,
secretController, secretController,
workspaceController secretSnapshotController,
workspaceController,
actionController
} }

@ -18,6 +18,7 @@ import { SecretVersion } from '../../models';
secretVersions = await SecretVersion.find({ secretVersions = await SecretVersion.find({
secret: secretId secret: secretId
}) })
.sort({ createdAt: -1 })
.skip(offset) .skip(offset)
.limit(limit); .limit(limit);

@ -0,0 +1,27 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { SecretSnapshot } from '../../models';
export const getSecretSnapshot = async (req: Request, res: Response) => {
let secretSnapshot;
try {
const { secretSnapshotId } = req.params;
secretSnapshot = await SecretSnapshot
.findById(secretSnapshotId)
.populate('secretVersions');
if (!secretSnapshot) throw new Error('Failed to find secret snapshot');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get secret snapshot'
});
}
return res.status(200).send({
secretSnapshot
});
}

@ -1,6 +1,9 @@
import { Request, Response } from 'express'; import e, { Request, Response } from 'express';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { SecretSnapshot } from '../../models'; import {
SecretSnapshot,
Log
} from '../../models';
/** /**
* Return secret snapshots for workspace with id [workspaceId] * Return secret snapshots for workspace with id [workspaceId]
@ -18,6 +21,7 @@ import { SecretSnapshot } from '../../models';
secretSnapshots = await SecretSnapshot.find({ secretSnapshots = await SecretSnapshot.find({
workspace: workspaceId workspace: workspaceId
}) })
.sort({ createdAt: -1 })
.skip(offset) .skip(offset)
.limit(limit); .limit(limit);
@ -32,4 +36,77 @@ import { SecretSnapshot } from '../../models';
return res.status(200).send({ return res.status(200).send({
secretSnapshots secretSnapshots
}); });
}
/**
* Return count of secret snapshots for workspace with id [workspaceId]
* @param req
* @param res
*/
export const getWorkspaceSecretSnapshotsCount = async (req: Request, res: Response) => {
let count;
try {
const { workspaceId } = req.params;
count = await SecretSnapshot.countDocuments({
workspace: workspaceId
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to count number of secret snapshots'
});
}
return res.status(200).send({
count
});
}
/**
* Return (audit) logs for workspace with id [workspaceId]
* @param req
* @param res
* @returns
*/
export const getWorkspaceLogs = async (req: Request, res: Response) => {
let logs
try {
const { workspaceId } = req.params;
const offset: number = parseInt(req.query.offset as string);
const limit: number = parseInt(req.query.limit as string);
const sortBy: string = req.query.sortBy as string;
const userId: string = req.query.userId as string;
const actionNames: string = req.query.actionNames as string;
logs = await Log.find({
workspace: workspaceId,
...( userId ? { user: userId } : {}),
...(
actionNames
? {
actionNames: {
$in: actionNames.split(',')
}
} : {}
)
})
.sort({ createdAt: sortBy === 'recent' ? -1 : 1 })
.skip(offset)
.limit(limit)
.populate('actions')
.populate('user');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get workspace logs'
});
}
return res.status(200).send({
logs
});
} }

@ -0,0 +1,112 @@
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import { Secret } from '../../models';
import { SecretVersion, Action } from '../models';
import { ACTION_UPDATE_SECRETS } from '../../variables';
/**
* Create an (audit) action for secrets including
* add, delete, update, and read actions.
* @param {Object} obj
* @param {String} obj.name - name of action
* @param {ObjectId[]} obj.secretIds - ids of relevant secrets
* @returns {Action} action - new action
*/
const createActionSecretHelper = async ({
name,
userId,
workspaceId,
secretIds
}: {
name: string;
userId: string;
workspaceId: string;
secretIds: Types.ObjectId[];
}) => {
let action;
let latestSecretVersions;
try {
if (name === ACTION_UPDATE_SECRETS) {
// case: action is updating secrets
// -> add old and new secret versions
// TODO: make query more efficient
latestSecretVersions = (await SecretVersion.aggregate([
{
$match: {
secret: {
$in: secretIds,
},
},
},
{
$sort: { version: -1 },
},
{
$group: {
_id: "$secret",
versions: { $push: "$$ROOT" },
},
},
{
$project: {
_id: 0,
secret: "$_id",
versions: { $slice: ["$versions", 2] },
},
}
]))
.map((s) => ({
oldSecretVersion: s.versions[0]._id,
newSecretVersion: s.versions[1]._id
}));
} else {
// case: action is adding, deleting, or reading secrets
// -> add new secret versions
latestSecretVersions = (await SecretVersion.aggregate([
{
$match: {
secret: {
$in: secretIds
}
}
},
{
$group: {
_id: '$secret',
version: { $max: '$version' },
versionId: { $max: '$_id' } // secret version id
}
},
{
$sort: { version: -1 }
}
])
.exec())
.map((s) => ({
newSecretVersion: s.versionId
}));
}
action = await new Action({
name,
user: userId,
workspace: workspaceId,
payload: {
secretVersions: latestSecretVersions
}
}).save();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to create action');
}
return action;
}
export { createActionSecretHelper };

@ -0,0 +1,41 @@
import * as Sentry from '@sentry/node';
import {
Log,
IAction
} from '../models';
const createLogHelper = async ({
userId,
workspaceId,
actions,
channel,
ipAddress
}: {
userId: string;
workspaceId: string;
actions: IAction[];
channel: string;
ipAddress: string;
}) => {
let log;
try {
log = await new Log({
user: userId,
workspace: workspaceId,
actionNames: actions.map((a) => a.name),
actions,
channel,
ipAddress
}).save();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to create log');
}
return log;
}
export {
createLogHelper
}

@ -1,6 +1,8 @@
import { Types } from 'mongoose';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { import {
Secret Secret,
ISecret
} from '../../models'; } from '../../models';
import { import {
SecretSnapshot, SecretSnapshot,
@ -9,66 +11,159 @@ import {
} from '../models'; } from '../models';
/** /**
* Save a copy of the current state of secrets in workspace with id * Save a secret snapshot that is a copy of the current state of secrets in workspace with id
* [workspaceId] under a new snapshot with incremented version under the * [workspaceId] under a new snapshot with incremented version under the
* secretsnapshots collection. * secretsnapshots collection.
* @param {Object} obj * @param {Object} obj
* @param {String} obj.workspaceId * @param {String} obj.workspaceId
* @returns {SecretSnapshot} secretSnapshot - new secret snapshot
*/ */
const takeSecretSnapshotHelper = async ({ const takeSecretSnapshotHelper = async ({
workspaceId workspaceId
}: { }: {
workspaceId: string; workspaceId: string;
}) => { }) => {
let secretSnapshot;
try { try {
const secrets = await Secret.find({ const secretIds = (await Secret.find({
workspace: workspaceId workspace: workspaceId
}); }, '_id')).map((s) => s._id);
const latestSecretVersions = (await SecretVersion.aggregate([
{
$match: {
secret: {
$in: secretIds
}
}
},
{
$group: {
_id: '$secret',
version: { $max: '$version' },
versionId: { $max: '$_id' } // secret version id
}
},
{
$sort: { version: -1 }
}
])
.exec())
.map((s) => s.versionId);
const latestSecretSnapshot = await SecretSnapshot.findOne({ const latestSecretSnapshot = await SecretSnapshot.findOne({
workspace: workspaceId workspace: workspaceId
}).sort({ version: -1 }); }).sort({ version: -1 });
if (!latestSecretSnapshot) { secretSnapshot = await new SecretSnapshot({
// case: no snapshots exist for workspace -> create first snapshot
await new SecretSnapshot({
workspace: workspaceId,
version: 1,
secrets
}).save();
return;
}
// case: snapshots exist for workspace
await new SecretSnapshot({
workspace: workspaceId, workspace: workspaceId,
version: latestSecretSnapshot.version + 1, version: latestSecretSnapshot ? latestSecretSnapshot.version + 1 : 1,
secrets secretVersions: latestSecretVersions
}).save(); }).save();
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
throw new Error('Failed to take a secret snapshot'); throw new Error('Failed to take a secret snapshot');
} }
return secretSnapshot;
} }
/**
* Add secret versions [secretVersions] to the SecretVersion collection.
* @param {Object} obj
* @param {Object[]} obj.secretVersions
* @returns {SecretVersion[]} newSecretVersions - new secret versions
*/
const addSecretVersionsHelper = async ({ const addSecretVersionsHelper = async ({
secretVersions secretVersions
}: { }: {
secretVersions: ISecretVersion[] secretVersions: ISecretVersion[]
}) => { }) => {
let newSecretVersions;
try { try {
await SecretVersion.insertMany(secretVersions); newSecretVersions = await SecretVersion.insertMany(secretVersions);
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
throw new Error('Failed to add secret versions'); throw new Error('Failed to add secret versions');
} }
return newSecretVersions;
}
const markDeletedSecretVersionsHelper = async ({
secretIds
}: {
secretIds: Types.ObjectId[];
}) => {
try {
await SecretVersion.updateMany({
secret: { $in: secretIds }
}, {
isDeleted: true
}, {
new: true
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to mark secret versions as deleted');
}
}
/**
* Initialize secret versioning by setting previously unversioned
* secrets to version 1 and begin populating secret versions.
*/
const initSecretVersioningHelper = async () => {
try {
await Secret.updateMany(
{ version: { $exists: false } },
{ $set: { version: 1 } }
);
const unversionedSecrets: ISecret[] = await Secret.aggregate([
{
$lookup: {
from: 'secretversions',
localField: '_id',
foreignField: 'secret',
as: 'versions',
},
},
{
$match: {
versions: { $size: 0 },
},
},
]);
if (unversionedSecrets.length > 0) {
await addSecretVersionsHelper({
secretVersions: unversionedSecrets.map((s, idx) => ({
...s,
secret: s._id,
version: s.version ? s.version : 1,
isDeleted: false,
workspace: s.workspace,
environment: s.environment
}))
});
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to ensure that secrets are versioned');
}
} }
export { export {
takeSecretSnapshotHelper, takeSecretSnapshotHelper,
addSecretVersionsHelper addSecretVersionsHelper,
markDeletedSecretVersionsHelper,
initSecretVersioningHelper
} }

@ -0,0 +1,7 @@
import requireLicenseAuth from './requireLicenseAuth';
import requireSecretSnapshotAuth from './requireSecretSnapshotAuth';
export {
requireLicenseAuth,
requireSecretSnapshotAuth
}

@ -0,0 +1,47 @@
import { Request, Response, NextFunction } from 'express';
import { UnauthorizedRequestError, SecretSnapshotNotFoundError } from '../../utils/errors';
import { SecretSnapshot } from '../models';
import {
validateMembership
} from '../../helpers/membership';
/**
* Validate if user on request has proper membership for secret snapshot
* @param {Object} obj
* @param {String[]} obj.acceptedRoles - accepted workspace roles
* @param {String[]} obj.acceptedStatuses - accepted workspace statuses
* @param {String[]} obj.location - location of [workspaceId] on request (e.g. params, body) for parsing
*/
const requireSecretSnapshotAuth = ({
acceptedRoles,
}: {
acceptedRoles: string[];
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
try {
const { secretSnapshotId } = req.params;
const secretSnapshot = await SecretSnapshot.findById(secretSnapshotId);
if (!secretSnapshot) {
return next(SecretSnapshotNotFoundError({
message: 'Failed to find secret snapshot'
}));
}
await validateMembership({
userId: req.user._id.toString(),
workspaceId: secretSnapshot.workspace.toString(),
acceptedRoles
});
req.secretSnapshot = secretSnapshot as any;
next();
} catch (err) {
return next(UnauthorizedRequestError({ message: 'Unable to authenticate secret snapshot' }));
}
}
}
export default requireSecretSnapshotAuth;

@ -0,0 +1,46 @@
import { Schema, model, Types } from 'mongoose';
export interface IAction {
name: string;
user?: Types.ObjectId,
workspace?: Types.ObjectId,
payload: {
secretVersions?: Types.ObjectId[]
}
}
const actionSchema = new Schema<IAction>(
{
name: {
type: String,
required: true
},
user: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true
},
workspace: {
type: Schema.Types.ObjectId,
ref: 'Workspace'
},
payload: {
secretVersions: [{
oldSecretVersion: {
type: Schema.Types.ObjectId,
ref: 'SecretVersion'
},
newSecretVersion: {
type: Schema.Types.ObjectId,
ref: 'SecretVersion'
}
}]
}
}, {
timestamps: true
}
);
const Action = model<IAction>('Action', actionSchema);
export default Action;

@ -1,9 +1,15 @@
import SecretSnapshot, { ISecretSnapshot } from "./secretSnapshot"; import SecretSnapshot, { ISecretSnapshot } from './secretSnapshot';
import SecretVersion, { ISecretVersion } from "./secretVersion"; import SecretVersion, { ISecretVersion } from './secretVersion';
import Log, { ILog } from './log';
import Action, { IAction } from './action';
export { export {
SecretSnapshot, SecretSnapshot,
ISecretSnapshot, ISecretSnapshot,
SecretVersion, SecretVersion,
ISecretVersion ISecretVersion,
Log,
ILog,
Action,
IAction
} }

@ -0,0 +1,59 @@
import { Schema, model, Types } from 'mongoose';
import {
ACTION_ADD_SECRETS,
ACTION_UPDATE_SECRETS,
ACTION_READ_SECRETS,
ACTION_DELETE_SECRETS
} from '../../variables';
export interface ILog {
_id: Types.ObjectId;
user?: Types.ObjectId;
workspace?: Types.ObjectId;
actionNames: string[];
actions: Types.ObjectId[];
channel: string;
ipAddress?: string;
}
const logSchema = new Schema<ILog>(
{
user: {
type: Schema.Types.ObjectId,
ref: 'User'
},
workspace: {
type: Schema.Types.ObjectId,
ref: 'Workspace'
},
actionNames: {
type: [String],
enum: [
ACTION_ADD_SECRETS,
ACTION_UPDATE_SECRETS,
ACTION_READ_SECRETS,
ACTION_DELETE_SECRETS
],
required: true
},
actions: [{
type: Schema.Types.ObjectId,
ref: 'Action',
required: true
}],
channel: {
type: String,
enum: ['web', 'cli', 'auto'],
required: true
},
ipAddress: {
type: String
}
}, {
timestamps: true
}
);
const Log = model<ILog>('Log', logSchema);
export default Log;

@ -1,31 +1,9 @@
import { Schema, model, Types } from 'mongoose'; import { Schema, model, Types } from 'mongoose';
import {
SECRET_SHARED,
SECRET_PERSONAL,
ENV_DEV,
ENV_TESTING,
ENV_STAGING,
ENV_PROD
} from '../../variables';
export interface ISecretSnapshot { export interface ISecretSnapshot {
workspace: Types.ObjectId; workspace: Types.ObjectId;
version: number; version: number;
secrets: { secretVersions: Types.ObjectId[];
version: number;
workspace: Types.ObjectId;
type: string;
user: Types.ObjectId;
environment: string;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretKeyHash: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretValueHash: string;
}[]
} }
const secretSnapshotSchema = new Schema<ISecretSnapshot>( const secretSnapshotSchema = new Schema<ISecretSnapshot>(
@ -39,64 +17,10 @@ const secretSnapshotSchema = new Schema<ISecretSnapshot>(
type: Number, type: Number,
required: true required: true
}, },
secrets: [{ secretVersions: [{
version: { type: Schema.Types.ObjectId,
type: Number, ref: 'SecretVersion',
default: 1, required: true
required: true
},
workspace: {
type: Schema.Types.ObjectId,
ref: 'Workspace',
required: true
},
type: {
type: String,
enum: [SECRET_SHARED, SECRET_PERSONAL],
required: true
},
user: {
// user associated with the personal secret
type: Schema.Types.ObjectId,
ref: 'User'
},
environment: {
type: String,
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD],
required: true
},
secretKeyCiphertext: {
type: String,
required: true
},
secretKeyIV: {
type: String, // symmetric
required: true
},
secretKeyTag: {
type: String, // symmetric
required: true
},
secretKeyHash: {
type: String,
required: true
},
secretValueCiphertext: {
type: String,
required: true
},
secretValueIV: {
type: String, // symmetric
required: true
},
secretValueTag: {
type: String, // symmetric
required: true
},
secretValueHash: {
type: String,
required: true
}
}] }]
}, },
{ {

@ -1,9 +1,30 @@
import { Schema, model, Types } from 'mongoose'; import { Schema, model, Types } from 'mongoose';
import {
SECRET_SHARED,
SECRET_PERSONAL,
ENV_DEV,
ENV_TESTING,
ENV_STAGING,
ENV_PROD
} from '../../variables';
/**
* TODO:
* 1. Modify SecretVersion to also contain XX
* - type
* - user
* - environment
* 2. Modify SecretSnapshot to point to arrays of SecretVersion
*/
export interface ISecretVersion { export interface ISecretVersion {
_id?: Types.ObjectId; _id?: Types.ObjectId;
secret: Types.ObjectId; secret: Types.ObjectId;
version: number; version: number;
workspace: Types.ObjectId; // new
type: string; // new
user: Types.ObjectId; // new
environment: string; // new
isDeleted: boolean; isDeleted: boolean;
secretKeyCiphertext: string; secretKeyCiphertext: string;
secretKeyIV: string; secretKeyIV: string;
@ -27,6 +48,26 @@ const secretVersionSchema = new Schema<ISecretVersion>(
default: 1, default: 1,
required: true required: true
}, },
workspace: {
type: Schema.Types.ObjectId,
ref: 'Workspace',
required: true
},
type: {
type: String,
enum: [SECRET_SHARED, SECRET_PERSONAL],
required: true
},
user: {
// user associated with the personal secret
type: Schema.Types.ObjectId,
ref: 'User'
},
environment: {
type: String,
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD],
required: true
},
isDeleted: { isDeleted: {
type: Boolean, type: Boolean,
default: false, default: false,

@ -0,0 +1,17 @@
import express from 'express';
const router = express.Router();
import {
validateRequest
} from '../../../middleware';
import { param } from 'express-validator';
import { actionController } from '../../controllers/v1';
// TODO: put into action controller
router.get(
'/:actionId',
param('actionId').exists().trim(),
validateRequest,
actionController.getAction
);
export default router;

@ -1,7 +1,11 @@
import secret from './secret'; import secret from './secret';
import secretSnapshot from './secretSnapshot';
import workspace from './workspace'; import workspace from './workspace';
import action from './action';
export { export {
secret, secret,
workspace secretSnapshot,
workspace,
action
} }

@ -2,19 +2,20 @@ import express from 'express';
const router = express.Router(); const router = express.Router();
import { import {
requireAuth, requireAuth,
requireWorkspaceAuth, requireSecretAuth,
validateRequest validateRequest
} from '../../../middleware'; } from '../../../middleware';
import { body, query, param } from 'express-validator'; import { query, param } from 'express-validator';
import { secretController } from '../../controllers/v1'; import { secretController } from '../../controllers/v1';
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../../variables'; import { ADMIN, MEMBER } from '../../../variables';
router.get( router.get(
'/:secretId/secret-versions', '/:secretId/secret-versions',
requireAuth, requireAuth({
requireWorkspaceAuth({ acceptedAuthModes: ['jwt']
acceptedRoles: [ADMIN, MEMBER], }),
acceptedStatuses: [COMPLETED, GRANTED] requireSecretAuth({
acceptedRoles: [ADMIN, MEMBER]
}), }),
param('secretId').exists().trim(), param('secretId').exists().trim(),
query('offset').exists().isInt(), query('offset').exists().isInt(),

@ -0,0 +1,27 @@
import express from 'express';
const router = express.Router();
import {
requireSecretSnapshotAuth
} from '../../middleware';
import {
requireAuth,
validateRequest
} from '../../../middleware';
import { param } from 'express-validator';
import { ADMIN, MEMBER } from '../../../variables';
import { secretSnapshotController } from '../../controllers/v1';
router.get(
'/:secretSnapshotId',
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireSecretSnapshotAuth({
acceptedRoles: [ADMIN, MEMBER]
}),
param('secretSnapshotId').exists().trim(),
validateRequest,
secretSnapshotController.getSecretSnapshot
);
export default router;

@ -6,15 +6,16 @@ import {
validateRequest validateRequest
} from '../../../middleware'; } from '../../../middleware';
import { param, query } from 'express-validator'; import { param, query } from 'express-validator';
import { ADMIN, MEMBER, GRANTED } from '../../../variables'; import { ADMIN, MEMBER } from '../../../variables';
import { workspaceController } from '../../controllers/v1'; import { workspaceController } from '../../controllers/v1';
router.get( router.get(
'/:workspaceId/secret-snapshots', '/:workspaceId/secret-snapshots',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER]
acceptedStatuses: [GRANTED]
}), }),
param('workspaceId').exists().trim(), param('workspaceId').exists().trim(),
query('offset').exists().isInt(), query('offset').exists().isInt(),
@ -23,5 +24,35 @@ router.get(
workspaceController.getWorkspaceSecretSnapshots workspaceController.getWorkspaceSecretSnapshots
); );
router.get(
'/:workspaceId/secret-snapshots/count',
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
}),
param('workspaceId').exists().trim(),
validateRequest,
workspaceController.getWorkspaceSecretSnapshotsCount
);
router.get(
'/:workspaceId/logs',
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
}),
param('workspaceId').exists().trim(),
query('offset').exists().isInt(),
query('limit').exists().isInt(),
query('sortBy'),
query('userId'),
query('actionNames'),
validateRequest,
workspaceController.getWorkspaceLogs
);
export default router; export default router;

@ -0,0 +1,81 @@
import { Types } from 'mongoose';
import {
Log,
Action,
IAction
} from '../models';
import {
createLogHelper
} from '../helpers/log';
import {
createActionSecretHelper
} from '../helpers/action';
import EELicenseService from './EELicenseService';
/**
* Class to handle Enterprise Edition log actions
*/
class EELogService {
/**
* Create an (audit) log
* @param {Object} obj
* @param {String} obj.userId - id of user associated with the log
* @param {String} obj.workspaceId - id of workspace associated with the log
* @param {Action} obj.actions - actions to include in log
* @param {String} obj.channel - channel (web/cli/auto) associated with the log
* @param {String} obj.ipAddress - ip address associated with the log
* @returns {Log} log - new audit log
*/
static async createLog({
userId,
workspaceId,
actions,
channel,
ipAddress
}: {
userId: string;
workspaceId: string;
actions: IAction[];
channel: string;
ipAddress: string;
}) {
if (!EELicenseService.isLicenseValid) return null;
return await createLogHelper({
userId,
workspaceId,
actions,
channel,
ipAddress
})
}
/**
* Create an (audit) action for secrets including
* add, delete, update, and read actions.
* @param {Object} obj
* @param {String} obj.name - name of action
* @param {ObjectId[]} obj.secretIds - secret ids
* @returns {Action} action - new action
*/
static async createActionSecret({
name,
userId,
workspaceId,
secretIds
}: {
name: string;
userId: string;
workspaceId: string;
secretIds: Types.ObjectId[];
}) {
if (!EELicenseService.isLicenseValid) return null;
return await createActionSecretHelper({
name,
userId,
workspaceId,
secretIds
});
}
}
export default EELogService;

@ -1,7 +1,10 @@
import { Types } from 'mongoose';
import { ISecretVersion } from '../models'; import { ISecretVersion } from '../models';
import { import {
takeSecretSnapshotHelper, takeSecretSnapshotHelper,
addSecretVersionsHelper addSecretVersionsHelper,
markDeletedSecretVersionsHelper,
initSecretVersioningHelper
} from '../helpers/secret'; } from '../helpers/secret';
import EELicenseService from './EELicenseService'; import EELicenseService from './EELicenseService';
@ -11,12 +14,13 @@ import EELicenseService from './EELicenseService';
class EESecretService { class EESecretService {
/** /**
* Save a copy of the current state of secrets in workspace with id * Save a secret snapshot that is a copy of the current state of secrets in workspace with id
* [workspaceId] under a new snapshot with incremented version under the * [workspaceId] under a new snapshot with incremented version under the
* SecretSnapshot collection. * SecretSnapshot collection.
* Requires a valid license key [licenseKey] * Requires a valid license key [licenseKey]
* @param {Object} obj * @param {Object} obj
* @param {String} obj.workspaceId * @param {String} obj.workspaceId
* @returns {SecretSnapshot} secretSnapshot - new secret snpashot
*/ */
static async takeSecretSnapshot({ static async takeSecretSnapshot({
workspaceId workspaceId
@ -24,13 +28,14 @@ class EESecretService {
workspaceId: string; workspaceId: string;
}) { }) {
if (!EELicenseService.isLicenseValid) return; if (!EELicenseService.isLicenseValid) return;
await takeSecretSnapshotHelper({ workspaceId }); return await takeSecretSnapshotHelper({ workspaceId });
} }
/** /**
* Adds secret versions [secretVersions] to the SecretVersion collection. * Add secret versions [secretVersions] to the SecretVersion collection.
* @param {Object} obj * @param {Object} obj
* @param {SecretVersion} obj.secretVersions * @param {Object[]} obj.secretVersions
* @returns {SecretVersion[]} newSecretVersions - new secret versions
*/ */
static async addSecretVersions({ static async addSecretVersions({
secretVersions secretVersions
@ -38,10 +43,36 @@ class EESecretService {
secretVersions: ISecretVersion[]; secretVersions: ISecretVersion[];
}) { }) {
if (!EELicenseService.isLicenseValid) return; if (!EELicenseService.isLicenseValid) return;
await addSecretVersionsHelper({ return await addSecretVersionsHelper({
secretVersions secretVersions
}); });
} }
/**
* Mark secret versions associated with secrets with ids [secretIds]
* as deleted.
* @param {Object} obj
* @param {ObjectId[]} obj.secretIds - secret ids
*/
static async markDeletedSecretVersions({
secretIds
}: {
secretIds: Types.ObjectId[];
}) {
if (!EELicenseService.isLicenseValid) return;
await markDeletedSecretVersionsHelper({
secretIds
});
}
/**
* Initialize secret versioning by setting previously unversioned
* secrets to version 1 and begin populating secret versions.
*/
static async initSecretVersioning() {
if (!EELicenseService.isLicenseValid) return;
await initSecretVersioningHelper();
}
} }
export default EESecretService; export default EESecretService;

@ -1,7 +1,9 @@
import EELicenseService from "./EELicenseService"; import EELicenseService from "./EELicenseService";
import EESecretService from "./EESecretService"; import EESecretService from "./EESecretService";
import EELogService from "./EELogService";
export { export {
EELicenseService, EELicenseService,
EESecretService EESecretService,
EELogService
} }

@ -1,4 +1,7 @@
import { EVENT_PUSH_SECRETS } from '../variables'; import {
EVENT_PUSH_SECRETS,
EVENT_PULL_SECRETS
} from '../variables';
interface PushSecret { interface PushSecret {
ciphertextKey: string; ciphertextKey: string;
@ -19,7 +22,7 @@ interface PushSecret {
* @returns * @returns
*/ */
const eventPushSecrets = ({ const eventPushSecrets = ({
workspaceId, workspaceId
}: { }: {
workspaceId: string; workspaceId: string;
}) => { }) => {
@ -32,6 +35,26 @@ const eventPushSecrets = ({
}); });
} }
/**
* Return event for pulling secrets
* @param {Object} obj
* @param {String} obj.workspaceId - id of workspace to pull secrets from
* @returns
*/
const eventPullSecrets = ({
workspaceId,
}: {
workspaceId: string;
}) => {
return ({
name: EVENT_PULL_SECRETS,
workspaceId,
payload: {
}
});
}
export { export {
eventPushSecrets eventPushSecrets
} }

@ -1,7 +1,10 @@
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import bcrypt from 'bcrypt';
import { import {
User User,
ServiceTokenData,
APIKeyData
} from '../models'; } from '../models';
import { import {
JWT_AUTH_LIFETIME, JWT_AUTH_LIFETIME,
@ -9,6 +12,179 @@ import {
JWT_REFRESH_LIFETIME, JWT_REFRESH_LIFETIME,
JWT_REFRESH_SECRET JWT_REFRESH_SECRET
} from '../config'; } from '../config';
import {
AccountNotFoundError,
ServiceTokenDataNotFoundError,
APIKeyDataNotFoundError,
UnauthorizedRequestError
} from '../utils/errors';
// TODO 1: check if API key works
// TODO 2: optimize middleware
/**
* Validate that auth token value [authTokenValue] falls under one of
* accepted auth modes [acceptedAuthModes].
* @param {Object} obj
* @param {String} obj.authTokenValue - auth token value (e.g. JWT or service token value)
* @param {String[]} obj.acceptedAuthModes - accepted auth modes (e.g. jwt, serviceToken)
* @returns {String} authMode - auth mode
*/
const validateAuthMode = ({
authTokenValue,
acceptedAuthModes
}: {
authTokenValue: string;
acceptedAuthModes: string[];
}) => {
let authMode;
try {
switch (authTokenValue.split('.', 1)[0]) {
case 'st':
authMode = 'serviceToken';
break;
case 'ak':
authMode = 'apiKey';
break;
default:
authMode = 'jwt';
break;
}
if (!acceptedAuthModes.includes(authMode))
throw UnauthorizedRequestError({ message: 'Failed to authenticated auth mode' });
} catch (err) {
throw UnauthorizedRequestError({ message: 'Failed to authenticated auth mode' });
}
return authMode;
}
/**
* Return user payload corresponding to JWT token [authTokenValue]
* @param {Object} obj
* @param {String} obj.authTokenValue - JWT token value
* @returns {User} user - user corresponding to JWT token
*/
const getAuthUserPayload = async ({
authTokenValue
}: {
authTokenValue: string;
}) => {
let user;
try {
const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(authTokenValue, JWT_AUTH_SECRET)
);
user = await User.findOne({
_id: 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' });
} catch (err) {
throw UnauthorizedRequestError({
message: 'Failed to authenticate JWT token'
});
}
return user;
}
/**
* 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
*/
const getAuthSTDPayload = async ({
authTokenValue
}: {
authTokenValue: string;
}) => {
let serviceTokenData;
try {
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3);
// TODO: optimize double query
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'
});
serviceTokenData = await ServiceTokenData
.findById(TOKEN_IDENTIFIER)
.select('+encryptedKey +iv +tag');
} catch (err) {
throw UnauthorizedRequestError({
message: 'Failed to authenticate service token'
});
}
return serviceTokenData;
}
/**
* 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
*/
const getAuthAPIKeyPayload = async ({
authTokenValue
}: {
authTokenValue: string;
}) => {
let user;
try {
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3);
const apiKeyData = await APIKeyData
.findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt')
.populate('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'
});
user = apiKeyData.user;
} catch (err) {
throw UnauthorizedRequestError({
message: 'Failed to authenticate API key'
});
}
return user;
}
/** /**
* Return newly issued (JWT) auth and refresh tokens to user with id [userId] * Return newly issued (JWT) auth and refresh tokens to user with id [userId]
@ -99,4 +275,12 @@ const createToken = ({
} }
}; };
export { createToken, issueTokens, clearTokens }; export {
validateAuthMode,
getAuthUserPayload,
getAuthSTDPayload,
getAuthAPIKeyPayload,
createToken,
issueTokens,
clearTokens
};

@ -0,0 +1,31 @@
import mongoose from 'mongoose';
import { ISecret, Secret } from '../models';
import { EESecretService } from '../ee/services';
import { getLogger } from '../utils/logger';
/**
* Initialize database connection
* @param {Object} obj
* @param {String} obj.mongoURL - mongo connection string
* @returns
*/
const initDatabaseHelper = async ({
mongoURL
}: {
mongoURL: string;
}) => {
try {
await mongoose.connect(mongoURL);
getLogger("database").info("Database connection established");
await EESecretService.initSecretVersioning();
} catch (err) {
getLogger("database").error(`Unable to establish Database connection due to the error.\n${err}`);
}
return mongoose.connection;
}
export {
initDatabaseHelper
}

@ -3,7 +3,7 @@ import { Membership, Key } from '../models';
/** /**
* Validate that user with id [userId] is a member of workspace with id [workspaceId] * Validate that user with id [userId] is a member of workspace with id [workspaceId]
* and has at least one of the roles in [acceptedRoles] and statuses in [acceptedStatuses] * and has at least one of the roles in [acceptedRoles]
* @param {Object} obj * @param {Object} obj
* @param {String} obj.userId - id of user to validate * @param {String} obj.userId - id of user to validate
* @param {String} obj.workspaceId - id of workspace * @param {String} obj.workspaceId - id of workspace
@ -12,12 +12,10 @@ const validateMembership = async ({
userId, userId,
workspaceId, workspaceId,
acceptedRoles, acceptedRoles,
acceptedStatuses
}: { }: {
userId: string; userId: string;
workspaceId: string; workspaceId: string;
acceptedRoles: string[]; acceptedRoles: string[];
acceptedStatuses: string[];
}) => { }) => {
let membership; let membership;
@ -33,11 +31,6 @@ const validateMembership = async ({
if (!acceptedRoles.includes(membership.role)) { if (!acceptedRoles.includes(membership.role)) {
throw new Error('Failed to validate membership role'); throw new Error('Failed to validate membership role');
} }
if (!acceptedStatuses.includes(membership.status)) {
throw new Error('Failed to validate membership status');
}
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
@ -72,18 +65,15 @@ const findMembership = async (queryObj: any) => {
* @param {String[]} obj.userIds - id of users. * @param {String[]} obj.userIds - id of users.
* @param {String} obj.workspaceId - id of workspace. * @param {String} obj.workspaceId - id of workspace.
* @param {String[]} obj.roles - roles of users. * @param {String[]} obj.roles - roles of users.
* @param {String[]} obj.statuses - statuses of users.
*/ */
const addMemberships = async ({ const addMemberships = async ({
userIds, userIds,
workspaceId, workspaceId,
roles, roles
statuses
}: { }: {
userIds: string[]; userIds: string[];
workspaceId: string; workspaceId: string;
roles: string[]; roles: string[];
statuses: string[];
}): Promise<void> => { }): Promise<void> => {
try { try {
const operations = userIds.map((userId, idx) => { const operations = userIds.map((userId, idx) => {
@ -92,14 +82,12 @@ const addMemberships = async ({
filter: { filter: {
user: userId, user: userId,
workspace: workspaceId, workspace: workspaceId,
role: roles[idx], role: roles[idx]
status: statuses[idx]
}, },
update: { update: {
user: userId, user: userId,
workspace: workspaceId, workspace: workspaceId,
role: roles[idx], role: roles[idx]
status: statuses[idx]
}, },
upsert: true upsert: true
} }

@ -3,10 +3,12 @@ import rateLimit from 'express-rate-limit';
// 300 requests per 15 minutes // 300 requests per 15 minutes
const apiLimiter = rateLimit({ const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, windowMs: 15 * 60 * 1000,
max: 400, max: 450,
standardHeaders: true, standardHeaders: true,
legacyHeaders: false, legacyHeaders: false,
skip: (request) => request.path === '/healthcheck' skip: (request) => {
return request.path === '/healthcheck' || request.path === '/api/status'
}
}); });
// 5 requests per hour // 5 requests per hour
@ -20,7 +22,7 @@ const signupLimiter = rateLimit({
// 10 requests per hour // 10 requests per hour
const loginLimiter = rateLimit({ const loginLimiter = rateLimit({
windowMs: 60 * 60 * 1000, windowMs: 60 * 60 * 1000,
max: 20, max: 25,
standardHeaders: true, standardHeaders: true,
legacyHeaders: false legacyHeaders: false
}); });

@ -1,19 +1,24 @@
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import { import {
Secret, Secret,
ISecret, ISecret,
} from '../models'; } from '../models';
import { import {
EESecretService EESecretService,
EELogService
} from '../ee/services'; } from '../ee/services';
import { import {
SecretVersion IAction
} from '../ee/models'; } from '../ee/models';
import { import {
takeSecretSnapshotHelper SECRET_SHARED,
} from '../ee/helpers/secret'; SECRET_PERSONAL,
import { decryptSymmetric } from '../utils/crypto'; ACTION_ADD_SECRETS,
import { SECRET_SHARED, SECRET_PERSONAL } from '../variables'; ACTION_UPDATE_SECRETS,
ACTION_DELETE_SECRETS,
ACTION_READ_SECRETS
} from '../variables';
interface V1PushSecret { interface V1PushSecret {
ciphertextKey: string; ciphertextKey: string;
@ -51,8 +56,6 @@ interface Update {
[index: string]: any; [index: string]: any;
} }
type DecryptSecretType = 'text' | 'object' | 'expanded';
/** /**
* Push secrets for user with id [userId] to workspace * Push secrets for user with id [userId] to workspace
* with id [workspaceId] with environment [environment]. Follow steps: * with id [workspaceId] with environment [environment]. Follow steps:
@ -68,7 +71,7 @@ const v1PushSecrets = async ({
userId, userId,
workspaceId, workspaceId,
environment, environment,
secrets secrets,
}: { }: {
userId: string; userId: string;
workspaceId: string; workspaceId: string;
@ -78,7 +81,7 @@ const v1PushSecrets = async ({
// TODO: clean up function and fix up types // TODO: clean up function and fix up types
try { try {
// construct useful data structures // construct useful data structures
const oldSecrets = await pullSecrets({ const oldSecrets = await getSecrets({
userId, userId,
workspaceId, workspaceId,
environment environment
@ -101,11 +104,9 @@ const v1PushSecrets = async ({
await Secret.deleteMany({ await Secret.deleteMany({
_id: { $in: toDelete } _id: { $in: toDelete }
}); });
await SecretVersion.updateMany({ await EESecretService.markDeletedSecretVersions({
secret: { $in: toDelete } secretIds: toDelete
}, {
isDeleted: true
}); });
} }
@ -188,6 +189,10 @@ const v1PushSecrets = async ({
return ({ return ({
secret: _id, secret: _id,
version: version ? version + 1 : 1, version: version ? version + 1 : 1,
workspace: new Types.ObjectId(workspaceId),
type: newSecret.type,
user: new Types.ObjectId(userId),
environment,
isDeleted: false, isDeleted: false,
secretKeyCiphertext: newSecret.ciphertextKey, secretKeyCiphertext: newSecret.ciphertextKey,
secretKeyIV: newSecret.ivKey, secretKeyIV: newSecret.ivKey,
@ -239,6 +244,11 @@ const v1PushSecrets = async ({
EESecretService.addSecretVersions({ EESecretService.addSecretVersions({
secretVersions: newSecrets.map(({ secretVersions: newSecrets.map(({
_id, _id,
version,
workspace,
type,
user,
environment,
secretKeyCiphertext, secretKeyCiphertext,
secretKeyIV, secretKeyIV,
secretKeyTag, secretKeyTag,
@ -249,7 +259,11 @@ const v1PushSecrets = async ({
secretValueHash secretValueHash
}) => ({ }) => ({
secret: _id, secret: _id,
version: 1, version,
workspace,
type,
user,
environment,
isDeleted: false, isDeleted: false,
secretKeyCiphertext, secretKeyCiphertext,
secretKeyIV, secretKeyIV,
@ -284,22 +298,30 @@ const v1PushSecrets = async ({
* @param {String} obj.workspaceId - id of workspace to push to * @param {String} obj.workspaceId - id of workspace to push to
* @param {String} obj.environment - environment for secrets * @param {String} obj.environment - environment for secrets
* @param {Object[]} obj.secrets - secrets to push * @param {Object[]} obj.secrets - secrets to push
* @param {String} obj.channel - channel (web/cli/auto)
* @param {String} obj.ipAddress - ip address of request to push secrets
*/ */
const v2PushSecrets = async ({ const v2PushSecrets = async ({
userId, userId,
workspaceId, workspaceId,
environment, environment,
secrets secrets,
channel,
ipAddress
}: { }: {
userId: string; userId: string;
workspaceId: string; workspaceId: string;
environment: string; environment: string;
secrets: V2PushSecret[]; secrets: V2PushSecret[];
channel: string;
ipAddress: string;
}): Promise<void> => { }): Promise<void> => {
// TODO: clean up function and fix up types // TODO: clean up function and fix up types
try { try {
const actions: IAction[] = [];
// construct useful data structures // construct useful data structures
const oldSecrets = await pullSecrets({ const oldSecrets = await getSecrets({
userId, userId,
workspaceId, workspaceId,
environment environment
@ -322,12 +344,19 @@ const v1PushSecrets = async ({
await Secret.deleteMany({ await Secret.deleteMany({
_id: { $in: toDelete } _id: { $in: toDelete }
}); });
await SecretVersion.updateMany({ await EESecretService.markDeletedSecretVersions({
secret: { $in: toDelete } secretIds: toDelete
}, {
isDeleted: true
}); });
const deleteAction = await EELogService.createActionSecret({
name: ACTION_DELETE_SECRETS,
userId,
workspaceId,
secretIds: toDelete
});
deleteAction && actions.push(deleteAction);
} }
const toUpdate = oldSecrets const toUpdate = oldSecrets
@ -348,118 +377,10 @@ const v1PushSecrets = async ({
return false; return false;
}); });
const operations = toUpdate if (toUpdate.length > 0) {
.map((s) => { const operations = toUpdate
const { .map((s) => {
secretValueCiphertext, const {
secretValueIV,
secretValueTag,
secretValueHash,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
secretCommentHash,
} = newSecretsObj[`${s.type}-${s.secretKeyHash}`];
const update: Update = {
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
secretCommentHash,
}
if (!s.version) {
// case: (legacy) secret was not versioned
update.version = 1;
} else {
update['$inc'] = {
version: 1
}
}
if (s.type === SECRET_PERSONAL) {
// attach user associated with the personal secret
update['user'] = userId;
}
return {
updateOne: {
filter: {
_id: oldSecretsObj[`${s.type}-${s.secretKeyHash}`]._id
},
update
}
};
});
await Secret.bulkWrite(operations as any);
// (EE) add secret versions for updated secrets
await EESecretService.addSecretVersions({
secretVersions: toUpdate.map((s) => {
const {
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretKeyHash,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
secretCommentHash,
} = newSecretsObj[`${s.type}-${s.secretKeyHash}`];
return ({
secret: s._id,
version: s.version ? s.version + 1 : 1,
isDeleted: false,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretKeyHash,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash
})
})
});
// handle adding new secrets
const toAdd = secrets.filter((s) => !(`${s.type}-${s.secretKeyHash}` in oldSecretsObj));
if (toAdd.length > 0) {
// add secrets
const newSecrets = await Secret.insertMany(
toAdd.map(({
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretKeyHash,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
secretCommentHash,
}, idx) => {
const obj: any = {
version: 1,
workspace: workspaceId,
type: toAdd[idx].type,
environment,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretKeyHash,
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
@ -467,49 +388,120 @@ const v1PushSecrets = async ({
secretCommentCiphertext, secretCommentCiphertext,
secretCommentIV, secretCommentIV,
secretCommentTag, secretCommentTag,
secretCommentHash secretCommentHash,
}; } = newSecretsObj[`${s.type}-${s.secretKeyHash}`];
if (toAdd[idx].type === 'personal') { const update: Update = {
obj['user' as keyof typeof obj] = userId; secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
secretCommentHash,
} }
return obj; if (!s.version) {
}) // case: (legacy) secret was not versioned
update.version = 1;
} else {
update['$inc'] = {
version: 1
}
}
if (s.type === SECRET_PERSONAL) {
// attach user associated with the personal secret
update['user'] = userId;
}
return {
updateOne: {
filter: {
_id: oldSecretsObj[`${s.type}-${s.secretKeyHash}`]._id
},
update
}
};
});
await Secret.bulkWrite(operations as any);
// (EE) add secret versions for updated secrets
await EESecretService.addSecretVersions({
secretVersions: toUpdate.map((s) => {
return ({
...newSecretsObj[`${s.type}-${s.secretKeyHash}`],
secret: s._id,
version: s.version ? s.version + 1 : 1,
workspace: new Types.ObjectId(workspaceId),
user: s.user,
environment: s.environment,
isDeleted: false
})
})
});
const updateAction = await EELogService.createActionSecret({
name: ACTION_UPDATE_SECRETS,
userId,
workspaceId,
secretIds: toUpdate.map((u) => u._id)
});
updateAction && actions.push(updateAction);
}
// handle adding new secrets
const toAdd = secrets.filter((s) => !(`${s.type}-${s.secretKeyHash}` in oldSecretsObj));
if (toAdd.length > 0) {
// add secrets
const newSecrets = await Secret.insertMany(
toAdd.map((s, idx) => ({
...s,
version: 1,
workspace: workspaceId,
type: toAdd[idx].type,
environment,
...( toAdd[idx].type === 'personal' ? { user: userId } : {})
}))
); );
// (EE) add secret versions for new secrets // (EE) add secret versions for new secrets
EESecretService.addSecretVersions({ EESecretService.addSecretVersions({
secretVersions: newSecrets.map(({ secretVersions: newSecrets.map((secretDocument) => {
_id, return {
secretKeyCiphertext, ...secretDocument.toObject(),
secretKeyIV, secret: secretDocument._id,
secretKeyTag, isDeleted: false
secretKeyHash, }})
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash
}) => ({
secret: _id,
version: 1,
isDeleted: false,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretKeyHash,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash
}))
}); });
const addAction = await EELogService.createActionSecret({
name: ACTION_ADD_SECRETS,
userId,
workspaceId,
secretIds: newSecrets.map((n) => n._id)
});
addAction && actions.push(addAction);
} }
// (EE) take a secret snapshot // (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({ await EESecretService.takeSecretSnapshot({
workspaceId workspaceId
}) })
// (EE) create (audit) log
if (actions.length > 0) {
await EELogService.createLog({
userId,
workspaceId,
actions,
channel,
ipAddress
});
}
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
@ -518,15 +510,14 @@ const v1PushSecrets = async ({
}; };
/** /**
* Pull secrets for user with id [userId] for workspace * Get secrets for user with id [userId] for workspace
* with id [workspaceId] with environment [environment] * with id [workspaceId] with environment [environment]
* @param {Object} obj * @param {Object} obj
* @param {String} obj.userId -id of user to pull secrets for * @param {String} obj.userId -id of user to pull secrets for
* @param {String} obj.workspaceId - id of workspace to pull from * @param {String} obj.workspaceId - id of workspace to pull from
* @param {String} obj.environment - environment for secrets * @param {String} obj.environment - environment for secrets
*
*/ */
const pullSecrets = async ({ const getSecrets = async ({
userId, userId,
workspaceId, workspaceId,
environment environment
@ -563,9 +554,64 @@ const pullSecrets = async ({
return secrets; return secrets;
}; };
/**
* Pull secrets for user with id [userId] for workspace
* with id [workspaceId] with environment [environment]
* @param {Object} obj
* @param {String} obj.userId -id of user to pull secrets for
* @param {String} obj.workspaceId - id of workspace to pull from
* @param {String} obj.environment - environment for secrets
* @param {String} obj.channel - channel (web/cli/auto)
* @param {String} obj.ipAddress - ip address of request to push secrets
*/
const pullSecrets = async ({
userId,
workspaceId,
environment,
channel,
ipAddress
}: {
userId: string;
workspaceId: string;
environment: string;
channel: string;
ipAddress: string;
}): Promise<ISecret[]> => {
let secrets: any;
try {
secrets = await getSecrets({
userId,
workspaceId,
environment
})
const readAction = await EELogService.createActionSecret({
name: ACTION_READ_SECRETS,
userId,
workspaceId,
secretIds: secrets.map((n: any) => n._id)
});
readAction && await EELogService.createLog({
userId,
workspaceId,
actions: [readAction],
channel,
ipAddress
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to pull shared and personal secrets');
}
return secrets;
};
/** /**
* Reformat output of pullSecrets() to be compatible with how existing * Reformat output of pullSecrets() to be compatible with how existing
* clients handle secrets * web client handle secrets
* @param {Object} obj * @param {Object} obj
* @param {Object} obj.secrets * @param {Object} obj.secrets
*/ */

@ -5,7 +5,7 @@ import { createOrganization } from './organization';
import { addMembershipsOrg } from './membershipOrg'; import { addMembershipsOrg } from './membershipOrg';
import { createWorkspace } from './workspace'; import { createWorkspace } from './workspace';
import { addMemberships } from './membership'; import { addMemberships } from './membership';
import { OWNER, ADMIN, ACCEPTED, GRANTED } from '../variables'; import { OWNER, ADMIN, ACCEPTED } from '../variables';
import { sendMail } from '../helpers/nodemailer'; import { sendMail } from '../helpers/nodemailer';
/** /**
@ -113,8 +113,7 @@ const initializeDefaultOrg = async ({
await addMemberships({ await addMemberships({
userIds: [user._id.toString()], userIds: [user._id.toString()],
workspaceId: workspace._id.toString(), workspaceId: workspace._id.toString(),
roles: [ADMIN], roles: [ADMIN]
statuses: [GRANTED]
}); });
} catch (err) { } catch (err) {
throw new Error('Failed to initialize default organization and workspace'); throw new Error('Failed to initialize default organization and workspace');

@ -4,12 +4,12 @@ dotenv.config();
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { SENTRY_DSN, NODE_ENV, MONGO_URL } from './config'; import { SENTRY_DSN, NODE_ENV, MONGO_URL } from './config';
import { server } from './app'; import { server } from './app';
import { initDatabase } from './services/database'; import { DatabaseService } from './services';
import { setUpHealthEndpoint } from './services/health'; import { setUpHealthEndpoint } from './services/health';
import { initSmtp } from './services/smtp'; import { initSmtp } from './services/smtp';
import { setTransporter } from './helpers/nodemailer'; import { setTransporter } from './helpers/nodemailer';
initDatabase(MONGO_URL); DatabaseService.initDatabase(MONGO_URL);
setUpHealthEndpoint(server); setUpHealthEndpoint(server);

@ -6,6 +6,8 @@ import requireOrganizationAuth from './requireOrganizationAuth';
import requireIntegrationAuth from './requireIntegrationAuth'; import requireIntegrationAuth from './requireIntegrationAuth';
import requireIntegrationAuthorizationAuth from './requireIntegrationAuthorizationAuth'; import requireIntegrationAuthorizationAuth from './requireIntegrationAuthorizationAuth';
import requireServiceTokenAuth from './requireServiceTokenAuth'; import requireServiceTokenAuth from './requireServiceTokenAuth';
import requireServiceTokenDataAuth from './requireServiceTokenDataAuth';
import requireSecretAuth from './requireSecretAuth';
import validateRequest from './validateRequest'; import validateRequest from './validateRequest';
export { export {
@ -17,5 +19,7 @@ export {
requireIntegrationAuth, requireIntegrationAuth,
requireIntegrationAuthorizationAuth, requireIntegrationAuthorizationAuth,
requireServiceTokenAuth, requireServiceTokenAuth,
requireServiceTokenDataAuth,
requireSecretAuth,
validateRequest validateRequest
}; };

@ -4,26 +4,33 @@ import * as Sentry from '@sentry/node';
import { InternalServerError } from "../utils/errors"; import { InternalServerError } from "../utils/errors";
import { getLogger } from "../utils/logger"; import { getLogger } from "../utils/logger";
import RequestError, { LogLevel } from "../utils/requestError"; import RequestError, { LogLevel } from "../utils/requestError";
import { NODE_ENV } from "../config";
export const requestErrorHandler: ErrorRequestHandler = (error: RequestError|Error, req, res, next) => { export const requestErrorHandler: ErrorRequestHandler = (error: RequestError | Error, req, res, next) => {
if(res.headersSent) return next(); if (res.headersSent) return next();
if (NODE_ENV !== "production") {
/* eslint-disable no-console */
console.log(error)
/* eslint-enable no-console */
}
//TODO: Find better way to type check for error. In current setting you need to cast type to get the functions and variables from RequestError //TODO: Find better way to type check for error. In current setting you need to cast type to get the functions and variables from RequestError
if(!(error instanceof RequestError)){ if (!(error instanceof RequestError)) {
error = InternalServerError({context: {exception: error.message}, stack: error.stack}) error = InternalServerError({ context: { exception: error.message }, stack: error.stack })
getLogger('backend-main').log((<RequestError>error).levelName.toLowerCase(), (<RequestError>error).message) getLogger('backend-main').log((<RequestError>error).levelName.toLowerCase(), (<RequestError>error).message)
} }
//* Set Sentry user identification if req.user is populated //* Set Sentry user identification if req.user is populated
if(req.user !== undefined && req.user !== null){ if (req.user !== undefined && req.user !== null) {
Sentry.setUser({ email: req.user.email }) Sentry.setUser({ email: req.user.email })
} }
//* Only sent error to Sentry if LogLevel is one of the following level 'ERROR', 'EMERGENCY' or 'CRITICAL' //* Only sent error to Sentry if LogLevel is one of the following level 'ERROR', 'EMERGENCY' or 'CRITICAL'
//* with this we will eliminate false-positive errors like 'BadRequestError', 'UnauthorizedRequestError' and so on //* with this we will eliminate false-positive errors like 'BadRequestError', 'UnauthorizedRequestError' and so on
if([LogLevel.ERROR, LogLevel.EMERGENCY, LogLevel.CRITICAL].includes((<RequestError>error).level)){ if ([LogLevel.ERROR, LogLevel.EMERGENCY, LogLevel.CRITICAL].includes((<RequestError>error).level)) {
Sentry.captureException(error) Sentry.captureException(error)
} }
res.status((<RequestError>error).statusCode).json((<RequestError>error).format(req)) res.status((<RequestError>error).statusCode).json((<RequestError>error).format(req))
next() next()
} }

@ -1,8 +1,13 @@
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { User } from '../models'; import { User, ServiceTokenData } from '../models';
import { JWT_AUTH_SECRET } from '../config'; import {
import { AccountNotFoundError, BadRequestError, UnauthorizedRequestError } from '../utils/errors'; validateAuthMode,
getAuthUserPayload,
getAuthSTDPayload,
getAuthAPIKeyPayload
} from '../helpers/auth';
import { BadRequestError } from '../utils/errors';
declare module 'jsonwebtoken' { declare module 'jsonwebtoken' {
export interface UserIDJwtPayload extends jwt.JwtPayload { export interface UserIDJwtPayload extends jwt.JwtPayload {
@ -11,34 +16,58 @@ declare module 'jsonwebtoken' {
} }
/** /**
* Validate if JWT (auth) token on request is valid (e.g. not expired), * Validate if token on request is valid (e.g. not expired) for various auth modes:
* if there is an associated user, and if that user is fully setup. * - If token is a JWT token, then check if there is an associated user
* @param req - express request object * and if user is fully setup.
* @param res - express response object * - If token is a service token (st), then check if there is associated
* @param next - express next function * service token data.
* @param {Object} obj
* @param {String[]} obj.acceptedAuthModes - accepted modes of authentication (jwt/st)
* @returns * @returns
*/ */
const requireAuth = async (req: Request, res: Response, next: NextFunction) => { const requireAuth = ({
// JWT authentication middleware acceptedAuthModes = ['jwt']
const [ AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE ] = <[string, string]>req.headers['authorization']?.split(' ', 2) ?? [null, null] }: {
if(AUTH_TOKEN_TYPE === null) return next(BadRequestError({message: `Missing Authorization Header in the request header.`})) acceptedAuthModes: string[];
if(AUTH_TOKEN_TYPE.toLowerCase() !== 'bearer') return next(BadRequestError({message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.`})) }) => {
if(AUTH_TOKEN_VALUE === null) return next(BadRequestError({message: 'Missing Authorization Body in the request header'})) return async (req: Request, res: Response, next: NextFunction) => {
const [AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE] = <[string, string]>req.headers['authorization']?.split(' ', 2) ?? [null, null]
if (AUTH_TOKEN_TYPE === null)
return next(BadRequestError({ message: `Missing Authorization Header in the request header.` }))
if (AUTH_TOKEN_TYPE.toLowerCase() !== 'bearer')
return next(BadRequestError({ message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.` }))
if (AUTH_TOKEN_VALUE === null)
return next(BadRequestError({ message: 'Missing Authorization Body in the request header' }))
const decodedToken = <jwt.UserIDJwtPayload>( // validate auth token against
jwt.verify(AUTH_TOKEN_VALUE, JWT_AUTH_SECRET) const authMode = validateAuthMode({
); authTokenValue: AUTH_TOKEN_VALUE,
acceptedAuthModes
});
const user = await User.findOne({ if (!acceptedAuthModes.includes(authMode)) throw new Error('Failed to validate auth mode');
_id: decodedToken.userId
}).select('+publicKey');
if (!user) return next(AccountNotFoundError({message: 'Failed to locate User account'})) // attach auth payloads
if (!user?.publicKey) switch (authMode) {
return next(UnauthorizedRequestError({message: 'Unable to authenticate due to partially set up account'})) case 'serviceToken':
req.serviceTokenData = await getAuthSTDPayload({
authTokenValue: AUTH_TOKEN_VALUE
});
break;
case 'apiKey':
req.user = await getAuthAPIKeyPayload({
authTokenValue: AUTH_TOKEN_VALUE
});
break;
default:
req.user = await getAuthUserPayload({
authTokenValue: AUTH_TOKEN_VALUE
});
break;
}
req.user = user; return next();
return next(); }
}; }
export default requireAuth; export default requireAuth;

@ -7,15 +7,13 @@ type req = 'params' | 'body' | 'query';
const requireBotAuth = ({ const requireBotAuth = ({
acceptedRoles, acceptedRoles,
acceptedStatuses,
location = 'params' location = 'params'
}: { }: {
acceptedRoles: string[]; acceptedRoles: string[];
acceptedStatuses: string[];
location?: req; location?: req;
}) => { }) => {
return async (req: Request, res: Response, next: NextFunction) => { return async (req: Request, res: Response, next: NextFunction) => {
const bot = await Bot.findOne({ _id: req[location].botId }); const bot = await Bot.findById(req[location].botId);
if (!bot) { if (!bot) {
return next(AccountNotFoundError({message: 'Failed to locate Bot account'})) return next(AccountNotFoundError({message: 'Failed to locate Bot account'}))
@ -24,8 +22,7 @@ const requireBotAuth = ({
await validateMembership({ await validateMembership({
userId: req.user._id.toString(), userId: req.user._id.toString(),
workspaceId: bot.workspace.toString(), workspaceId: bot.workspace.toString(),
acceptedRoles, acceptedRoles
acceptedStatuses
}); });
req.bot = bot; req.bot = bot;

@ -9,14 +9,11 @@ import { IntegrationNotFoundError, UnauthorizedRequestError } from '../utils/err
* with the integration on request params. * with the integration on request params.
* @param {Object} obj * @param {Object} obj
* @param {String[]} obj.acceptedRoles - accepted workspace roles * @param {String[]} obj.acceptedRoles - accepted workspace roles
* @param {String[]} obj.acceptedStatuses - accepted workspace statuses
*/ */
const requireIntegrationAuth = ({ const requireIntegrationAuth = ({
acceptedRoles, acceptedRoles
acceptedStatuses
}: { }: {
acceptedRoles: string[]; acceptedRoles: string[];
acceptedStatuses: string[];
}) => { }) => {
return async (req: Request, res: Response, next: NextFunction) => { return async (req: Request, res: Response, next: NextFunction) => {
// integration authorization middleware // integration authorization middleware
@ -35,8 +32,7 @@ const requireIntegrationAuth = ({
await validateMembership({ await validateMembership({
userId: req.user._id.toString(), userId: req.user._id.toString(),
workspaceId: integration.workspace.toString(), workspaceId: integration.workspace.toString(),
acceptedRoles, acceptedRoles
acceptedStatuses
}); });
const integrationAuth = await IntegrationAuth.findOne({ const integrationAuth = await IntegrationAuth.findOne({

@ -10,16 +10,13 @@ import { UnauthorizedRequestError } from '../utils/errors';
* with the integration authorization on request params. * with the integration authorization on request params.
* @param {Object} obj * @param {Object} obj
* @param {String[]} obj.acceptedRoles - accepted workspace roles * @param {String[]} obj.acceptedRoles - accepted workspace roles
* @param {String[]} obj.acceptedStatuses - accepted workspace statuses
* @param {Boolean} obj.attachAccessToken - whether or not to decrypt and attach integration authorization access token onto request * @param {Boolean} obj.attachAccessToken - whether or not to decrypt and attach integration authorization access token onto request
*/ */
const requireIntegrationAuthorizationAuth = ({ const requireIntegrationAuthorizationAuth = ({
acceptedRoles, acceptedRoles,
acceptedStatuses,
attachAccessToken = true attachAccessToken = true
}: { }: {
acceptedRoles: string[]; acceptedRoles: string[];
acceptedStatuses: string[];
attachAccessToken?: boolean; attachAccessToken?: boolean;
}) => { }) => {
return async (req: Request, res: Response, next: NextFunction) => { return async (req: Request, res: Response, next: NextFunction) => {
@ -38,8 +35,7 @@ const requireIntegrationAuthorizationAuth = ({
await validateMembership({ await validateMembership({
userId: req.user._id.toString(), userId: req.user._id.toString(),
workspaceId: integrationAuth.workspace.toString(), workspaceId: integrationAuth.workspace.toString(),
acceptedRoles, acceptedRoles
acceptedStatuses
}); });
req.integrationAuth = integrationAuth; req.integrationAuth = integrationAuth;

@ -0,0 +1,46 @@
import { Request, Response, NextFunction } from 'express';
import { UnauthorizedRequestError, SecretNotFoundError } from '../utils/errors';
import { Secret } from '../models';
import {
validateMembership
} from '../helpers/membership';
/**
* Validate if user on request has proper membership to modify secret.
* @param {Object} obj
* @param {String[]} obj.acceptedRoles - accepted workspace roles
* @param {String[]} obj.location - location of [workspaceId] on request (e.g. params, body) for parsing
*/
const requireSecretAuth = ({
acceptedRoles
}: {
acceptedRoles: string[];
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
try {
const { secretId } = req.params;
const secret = await Secret.findById(secretId);
if (!secret) {
return next(SecretNotFoundError({
message: 'Failed to find secret'
}));
}
await validateMembership({
userId: req.user._id.toString(),
workspaceId: secret.workspace.toString(),
acceptedRoles
});
req.secret = secret as any;
next();
} catch (err) {
return next(UnauthorizedRequestError({ message: 'Unable to authenticate secret' }));
}
}
}
export default requireSecretAuth;

@ -4,6 +4,7 @@ import { ServiceToken } from '../models';
import { JWT_SERVICE_SECRET } from '../config'; import { JWT_SERVICE_SECRET } from '../config';
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors'; import { BadRequestError, UnauthorizedRequestError } from '../utils/errors';
// TODO: deprecate
declare module 'jsonwebtoken' { declare module 'jsonwebtoken' {
export interface UserIDJwtPayload extends jwt.JwtPayload { export interface UserIDJwtPayload extends jwt.JwtPayload {
userId: string; userId: string;

@ -0,0 +1,41 @@
import { Request, Response, NextFunction } from 'express';
import { ServiceToken, ServiceTokenData } from '../models';
import { validateMembership } from '../helpers/membership';
import { AccountNotFoundError, UnauthorizedRequestError } from '../utils/errors';
type req = 'params' | 'body' | 'query';
const requireServiceTokenDataAuth = ({
acceptedRoles,
location = 'params'
}: {
acceptedRoles: string[];
location?: req;
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
const { serviceTokenDataId } = req[location];
const serviceTokenData = await ServiceTokenData
.findById(req[location].serviceTokenDataId)
.select('+encryptedKey +iv +tag');
if (!serviceTokenData) {
return next(AccountNotFoundError({message: 'Failed to locate service token data'}));
}
if (req.user) {
// case: jwt auth
await validateMembership({
userId: req.user._id.toString(),
workspaceId: serviceTokenData.workspace.toString(),
acceptedRoles
});
}
req.serviceTokenData = serviceTokenData;
next();
}
}
export default requireServiceTokenDataAuth;

@ -8,31 +8,38 @@ type req = 'params' | 'body' | 'query';
* Validate if user on request is a member with proper roles for workspace * Validate if user on request is a member with proper roles for workspace
* on request params. * on request params.
* @param {Object} obj * @param {Object} obj
* @param {String[]} obj.acceptedRoles - accepted workspace roles * @param {String[]} obj.acceptedRoles - accepted workspace roles for JWT auth
* @param {String[]} obj.acceptedStatuses - accepted workspace statuses
* @param {String[]} obj.location - location of [workspaceId] on request (e.g. params, body) for parsing * @param {String[]} obj.location - location of [workspaceId] on request (e.g. params, body) for parsing
*/ */
const requireWorkspaceAuth = ({ const requireWorkspaceAuth = ({
acceptedRoles, acceptedRoles,
acceptedStatuses,
location = 'params' location = 'params'
}: { }: {
acceptedRoles: string[]; acceptedRoles: string[];
acceptedStatuses: string[];
location?: req; location?: req;
}) => { }) => {
return async (req: Request, res: Response, next: NextFunction) => { return async (req: Request, res: Response, next: NextFunction) => {
// workspace authorization middleware
try { try {
const membership = await validateMembership({ const { workspaceId } = req[location];
userId: req.user._id.toString(),
workspaceId: req[location].workspaceId,
acceptedRoles,
acceptedStatuses
});
req.membership = membership; if (req.user) {
// case: jwt auth
const membership = await validateMembership({
userId: req.user._id.toString(),
workspaceId,
acceptedRoles
});
req.membership = membership;
}
if (
req.serviceTokenData
&& req.serviceTokenData.workspace !== workspaceId
&& req.serviceTokenData.environment !== req.query.environment
) {
next(UnauthorizedRequestError({message: 'Unable to authenticate workspace'}))
}
return next(); return next();
} catch (err) { } catch (err) {

@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { validationResult } from 'express-validator'; import { validationResult } from 'express-validator';
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors'; import { BadRequestError, UnauthorizedRequestError, ValidationError } from '../utils/errors';
/** /**
* Validate intended inputs on [req] via express-validator * Validate intended inputs on [req] via express-validator
@ -15,12 +15,12 @@ const validate = (req: Request, res: Response, next: NextFunction) => {
try { try {
const errors = validationResult(req); const errors = validationResult(req);
if (!errors.isEmpty()) { if (!errors.isEmpty()) {
return next(BadRequestError({context: {errors: errors.array}})) return next(ValidationError({ context: { errors: `One or more of your parameters are invalid [error(s)=${(JSON.stringify(errors))}]` } }))
} }
return next(); return next();
} catch (err) { } catch (err) {
return next(UnauthorizedRequestError({message: 'Unauthenticated requests are not allowed. Try logging in'})) return next(UnauthorizedRequestError({ message: 'Unauthenticated requests are not allowed. Try logging in' }))
} }
}; };

@ -0,0 +1,37 @@
import { Schema, model, Types } from 'mongoose';
export interface IAPIKeyData {
name: string;
user: Types.ObjectId;
expiresAt: Date;
secretHash: string;
}
const apiKeyDataSchema = new Schema<IAPIKeyData>(
{
name: {
type: String,
required: true
},
user: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true
},
expiresAt: {
type: Date
},
secretHash: {
type: String,
required: true,
select: false
}
},
{
timestamps: true
}
);
const APIKeyData = model<IAPIKeyData>('APIKeyData', apiKeyDataSchema);
export default APIKeyData;

@ -14,6 +14,8 @@ import Token, { IToken } from './token';
import User, { IUser } from './user'; import User, { IUser } from './user';
import UserAction, { IUserAction } from './userAction'; import UserAction, { IUserAction } from './userAction';
import Workspace, { IWorkspace } from './workspace'; import Workspace, { IWorkspace } from './workspace';
import ServiceTokenData, { IServiceTokenData } from './serviceTokenData';
import APIKeyData, { IAPIKeyData } from './apiKeyData';
export { export {
BackupPrivateKey, BackupPrivateKey,
@ -47,5 +49,9 @@ export {
UserAction, UserAction,
IUserAction, IUserAction,
Workspace, Workspace,
IWorkspace IWorkspace,
ServiceTokenData,
IServiceTokenData,
APIKeyData,
IAPIKeyData
}; };

@ -1,5 +1,5 @@
import { Schema, model, Types } from 'mongoose'; import { Schema, model, Types } from 'mongoose';
import { ADMIN, MEMBER, INVITED, COMPLETED, GRANTED } from '../variables'; import { ADMIN, MEMBER } from '../variables';
export interface IMembership { export interface IMembership {
_id: Types.ObjectId; _id: Types.ObjectId;
@ -7,7 +7,6 @@ export interface IMembership {
inviteEmail?: string; inviteEmail?: string;
workspace: Types.ObjectId; workspace: Types.ObjectId;
role: 'admin' | 'member'; role: 'admin' | 'member';
status: 'invited' | 'completed' | 'granted';
} }
const membershipSchema = new Schema( const membershipSchema = new Schema(
@ -28,12 +27,6 @@ const membershipSchema = new Schema(
type: String, type: String,
enum: [ADMIN, MEMBER], enum: [ADMIN, MEMBER],
required: true required: true
},
status: {
// INVITED, COMPLETED, GRANTED
type: String,
enum: [INVITED, COMPLETED, GRANTED],
required: true
} }
}, },
{ {

@ -33,7 +33,8 @@ const secretSchema = new Schema<ISecret>(
{ {
version: { version: {
type: Number, type: Number,
required: true required: true,
default: 1
}, },
workspace: { workspace: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,

@ -1,6 +1,7 @@
import { Schema, model, Types } from 'mongoose'; import { Schema, model, Types } from 'mongoose';
import { ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD } from '../variables'; import { ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD } from '../variables';
// TODO: deprecate
export interface IServiceToken { export interface IServiceToken {
_id: Types.ObjectId; _id: Types.ObjectId;
name: string; name: string;

@ -0,0 +1,63 @@
import { Schema, model, Types } from 'mongoose';
export interface IServiceTokenData {
name: string;
workspace: Types.ObjectId;
environment: string; // TODO: adapt to upcoming environment id
user: Types.ObjectId;
expiresAt: Date;
secretHash: string;
encryptedKey: string;
iv: string;
tag: string;
}
const serviceTokenDataSchema = new Schema<IServiceTokenData>(
{
name: {
type: String,
required: true
},
workspace: {
type: Schema.Types.ObjectId,
ref: 'Workspace',
required: true
},
environment: { // TODO: adapt to upcoming environment id
type: String,
required: true
},
user: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true
},
expiresAt: {
type: Date
},
secretHash: {
type: String,
required: true,
select: false
},
encryptedKey: {
type: String,
select: false
},
iv: {
type: String,
select: false
},
tag: {
type: String,
select: false
}
},
{
timestamps: true
}
);
const ServiceTokenData = model<IServiceTokenData>('ServiceTokenData', serviceTokenDataSchema);
export default ServiceTokenData;

@ -11,7 +11,7 @@ export interface IUser {
tag?: string; tag?: string;
salt?: string; salt?: string;
verifier?: string; verifier?: string;
refreshVersion?: Number; refreshVersion?: number;
} }
const userSchema = new Schema<IUser>( const userSchema = new Schema<IUser>(
@ -52,7 +52,8 @@ const userSchema = new Schema<IUser>(
}, },
refreshVersion: { refreshVersion: {
type: Number, type: Number,
default: 0 default: 0,
select: false
} }
}, },
{ {

@ -0,0 +1,5 @@
import healthCheck from './status';
export {
healthCheck
}

@ -0,0 +1,15 @@
import express, { Request, Response } from 'express';
const router = express.Router();
router.get(
'/status',
(req: Request, res: Response) => {
res.status(200).json({
date: new Date(),
message: 'Ok',
})
}
);
export default router

@ -25,7 +25,19 @@ router.post(
authController.login2 authController.login2
); );
router.post('/logout', requireAuth, authController.logout); router.post(
router.post('/checkAuth', requireAuth, authController.checkAuth); '/logout',
requireAuth({
acceptedAuthModes: ['jwt']
}),
authController.logout
);
router.post(
'/checkAuth',
requireAuth({
acceptedAuthModes: ['jwt']
}),
authController.checkAuth
);
export default router; export default router;

@ -8,14 +8,15 @@ import {
validateRequest validateRequest
} from '../../middleware'; } from '../../middleware';
import { botController } from '../../controllers/v1'; import { botController } from '../../controllers/v1';
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables'; import { ADMIN, MEMBER } from '../../variables';
router.get( router.get(
'/:workspaceId', '/:workspaceId',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER]
acceptedStatuses: [COMPLETED, GRANTED]
}), }),
param('workspaceId').exists().trim().notEmpty(), param('workspaceId').exists().trim().notEmpty(),
validateRequest, validateRequest,
@ -24,10 +25,11 @@ router.get(
router.patch( router.patch(
'/:botId/active', '/:botId/active',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireBotAuth({ requireBotAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER]
acceptedStatuses: [COMPLETED, GRANTED]
}), }),
body('isActive').isBoolean(), body('isActive').isBoolean(),
body('botKey'), body('botKey'),

@ -5,16 +5,17 @@ import {
requireIntegrationAuth, requireIntegrationAuth,
validateRequest validateRequest
} from '../../middleware'; } from '../../middleware';
import { ADMIN, MEMBER, GRANTED } from '../../variables'; import { ADMIN, MEMBER } from '../../variables';
import { body, param } from 'express-validator'; import { body, param } from 'express-validator';
import { integrationController } from '../../controllers/v1'; import { integrationController } from '../../controllers/v1';
router.patch( router.patch(
'/:integrationId', '/:integrationId',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireIntegrationAuth({ requireIntegrationAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER]
acceptedStatuses: [GRANTED]
}), }),
param('integrationId').exists().trim(), param('integrationId').exists().trim(),
body('app').exists().trim(), body('app').exists().trim(),
@ -29,10 +30,11 @@ router.patch(
router.delete( router.delete(
'/:integrationId', '/:integrationId',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireIntegrationAuth({ requireIntegrationAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER]
acceptedStatuses: [GRANTED]
}), }),
param('integrationId').exists().trim(), param('integrationId').exists().trim(),
validateRequest, validateRequest,

@ -7,21 +7,24 @@ import {
requireIntegrationAuthorizationAuth, requireIntegrationAuthorizationAuth,
validateRequest validateRequest
} from '../../middleware'; } from '../../middleware';
import { ADMIN, MEMBER, GRANTED } from '../../variables'; import { ADMIN, MEMBER } from '../../variables';
import { integrationAuthController } from '../../controllers/v1'; import { integrationAuthController } from '../../controllers/v1';
router.get( router.get(
'/integration-options', '/integration-options',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
integrationAuthController.getIntegrationOptions integrationAuthController.getIntegrationOptions
); );
router.post( router.post(
'/oauth-token', '/oauth-token',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [GRANTED],
location: 'body' location: 'body'
}), }),
body('workspaceId').exists().trim().notEmpty(), body('workspaceId').exists().trim().notEmpty(),
@ -33,10 +36,11 @@ router.post(
router.get( router.get(
'/:integrationAuthId/apps', '/:integrationAuthId/apps',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireIntegrationAuthorizationAuth({ requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER]
acceptedStatuses: [GRANTED]
}), }),
param('integrationAuthId'), param('integrationAuthId'),
validateRequest, validateRequest,
@ -45,10 +49,11 @@ router.get(
router.delete( router.delete(
'/:integrationAuthId', '/:integrationAuthId',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireIntegrationAuthorizationAuth({ requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [GRANTED],
attachAccessToken: false attachAccessToken: false
}), }),
param('integrationAuthId'), param('integrationAuthId'),

@ -6,7 +6,9 @@ import { membershipOrgController } from '../../controllers/v1';
router.post( router.post(
'/signup', '/signup',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
body('inviteeEmail').exists().trim().notEmpty().isEmail(), body('inviteeEmail').exists().trim().notEmpty().isEmail(),
body('organizationId').exists().trim().notEmpty(), body('organizationId').exists().trim().notEmpty(),
validateRequest, validateRequest,

@ -6,15 +6,16 @@ import {
validateRequest validateRequest
} from '../../middleware'; } from '../../middleware';
import { body, param } from 'express-validator'; import { body, param } from 'express-validator';
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables'; import { ADMIN, MEMBER } from '../../variables';
import { keyController } from '../../controllers/v1'; import { keyController } from '../../controllers/v1';
router.post( router.post(
'/:workspaceId', '/:workspaceId',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER]
acceptedStatuses: [COMPLETED, GRANTED]
}), }),
param('workspaceId').exists().trim(), param('workspaceId').exists().trim(),
body('key').exists(), body('key').exists(),
@ -24,10 +25,11 @@ router.post(
router.get( router.get(
'/:workspaceId/latest', '/:workspaceId/latest',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER]
acceptedStatuses: [COMPLETED, GRANTED]
}), }),
param('workspaceId'), param('workspaceId'),
validateRequest, validateRequest,

@ -6,7 +6,9 @@ import { membershipController } from '../../controllers/v1';
router.get( // used for CLI (deprecate) router.get( // used for CLI (deprecate)
'/:workspaceId/connect', '/:workspaceId/connect',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
param('workspaceId').exists().trim(), param('workspaceId').exists().trim(),
validateRequest, validateRequest,
membershipController.validateMembership membershipController.validateMembership
@ -14,7 +16,9 @@ router.get( // used for CLI (deprecate)
router.delete( router.delete(
'/:membershipId', '/:membershipId',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
param('membershipId').exists().trim(), param('membershipId').exists().trim(),
validateRequest, validateRequest,
membershipController.deleteMembership membershipController.deleteMembership
@ -22,7 +26,9 @@ router.delete(
router.post( router.post(
'/:membershipId/change-role', '/:membershipId/change-role',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
body('role').exists().trim(), body('role').exists().trim(),
validateRequest, validateRequest,
membershipController.changeMembershipRole membershipController.changeMembershipRole

@ -7,7 +7,9 @@ import { membershipOrgController } from '../../controllers/v1';
router.post( router.post(
// TODO // TODO
'/membershipOrg/:membershipOrgId/change-role', '/membershipOrg/:membershipOrgId/change-role',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
param('membershipOrgId'), param('membershipOrgId'),
validateRequest, validateRequest,
membershipOrgController.changeMembershipOrgRole membershipOrgController.changeMembershipOrgRole
@ -15,7 +17,9 @@ router.post(
router.delete( router.delete(
'/:membershipOrgId', '/:membershipOrgId',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
param('membershipOrgId').exists().trim(), param('membershipOrgId').exists().trim(),
validateRequest, validateRequest,
membershipOrgController.deleteMembershipOrg membershipOrgController.deleteMembershipOrg

@ -11,13 +11,17 @@ import { organizationController } from '../../controllers/v1';
router.get( router.get(
'/', '/',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
organizationController.getOrganizations organizationController.getOrganizations
); );
router.post( // not used on frontend router.post( // not used on frontend
'/', '/',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
body('organizationName').exists().trim().notEmpty(), body('organizationName').exists().trim().notEmpty(),
validateRequest, validateRequest,
organizationController.createOrganization organizationController.createOrganization
@ -25,7 +29,9 @@ router.post( // not used on frontend
router.get( router.get(
'/:organizationId', '/:organizationId',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireOrganizationAuth({ requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED] acceptedStatuses: [ACCEPTED]
@ -37,7 +43,9 @@ router.get(
router.get( router.get(
'/:organizationId/users', '/:organizationId/users',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireOrganizationAuth({ requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED] acceptedStatuses: [ACCEPTED]
@ -49,7 +57,9 @@ router.get(
router.get( router.get(
'/:organizationId/my-workspaces', '/:organizationId/my-workspaces',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireOrganizationAuth({ requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED] acceptedStatuses: [ACCEPTED]
@ -61,7 +71,9 @@ router.get(
router.patch( router.patch(
'/:organizationId/name', '/:organizationId/name',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireOrganizationAuth({ requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED] acceptedStatuses: [ACCEPTED]
@ -74,7 +86,9 @@ router.patch(
router.get( router.get(
'/:organizationId/incidentContactOrg', '/:organizationId/incidentContactOrg',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireOrganizationAuth({ requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED] acceptedStatuses: [ACCEPTED]
@ -86,7 +100,9 @@ router.get(
router.post( router.post(
'/:organizationId/incidentContactOrg', '/:organizationId/incidentContactOrg',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireOrganizationAuth({ requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED] acceptedStatuses: [ACCEPTED]
@ -99,7 +115,9 @@ router.post(
router.delete( router.delete(
'/:organizationId/incidentContactOrg', '/:organizationId/incidentContactOrg',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireOrganizationAuth({ requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED] acceptedStatuses: [ACCEPTED]
@ -112,7 +130,9 @@ router.delete(
router.post( router.post(
'/:organizationId/customer-portal-session', '/:organizationId/customer-portal-session',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireOrganizationAuth({ requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED] acceptedStatuses: [ACCEPTED]
@ -124,7 +144,9 @@ router.post(
router.get( router.get(
'/:organizationId/subscriptions', '/:organizationId/subscriptions',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireOrganizationAuth({ requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED] acceptedStatuses: [ACCEPTED]

@ -7,7 +7,9 @@ import { passwordLimiter } from '../../helpers/rateLimiter';
router.post( router.post(
'/srp1', '/srp1',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
body('clientPublicKey').exists().trim().notEmpty(), body('clientPublicKey').exists().trim().notEmpty(),
validateRequest, validateRequest,
passwordController.srp1 passwordController.srp1
@ -16,7 +18,9 @@ router.post(
router.post( router.post(
'/change-password', '/change-password',
passwordLimiter, passwordLimiter,
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
body('clientProof').exists().trim().notEmpty(), body('clientProof').exists().trim().notEmpty(),
body('encryptedPrivateKey').exists().trim().notEmpty().notEmpty(), // private key encrypted under new pwd body('encryptedPrivateKey').exists().trim().notEmpty().notEmpty(), // private key encrypted under new pwd
body('iv').exists().trim().notEmpty(), // new iv for private key body('iv').exists().trim().notEmpty(), // new iv for private key
@ -54,7 +58,9 @@ router.get(
router.post( router.post(
'/backup-private-key', '/backup-private-key',
passwordLimiter, passwordLimiter,
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
body('clientProof').exists().trim().notEmpty(), body('clientProof').exists().trim().notEmpty(),
body('encryptedPrivateKey').exists().trim().notEmpty(), // (backup) private key encrypted under a strong key body('encryptedPrivateKey').exists().trim().notEmpty(), // (backup) private key encrypted under a strong key
body('iv').exists().trim().notEmpty(), // new iv for (backup) private key body('iv').exists().trim().notEmpty(), // new iv for (backup) private key

@ -8,14 +8,15 @@ import {
} from '../../middleware'; } from '../../middleware';
import { body, query, param } from 'express-validator'; import { body, query, param } from 'express-validator';
import { secretController } from '../../controllers/v1'; import { secretController } from '../../controllers/v1';
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables'; import { ADMIN, MEMBER } from '../../variables';
router.post( router.post(
'/:workspaceId', '/:workspaceId',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER]
acceptedStatuses: [COMPLETED, GRANTED]
}), }),
body('secrets').exists(), body('secrets').exists(),
body('keys').exists(), body('keys').exists(),
@ -28,10 +29,11 @@ router.post(
router.get( router.get(
'/:workspaceId', '/:workspaceId',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER]
acceptedStatuses: [COMPLETED, GRANTED]
}), }),
query('environment').exists().trim(), query('environment').exists().trim(),
query('channel'), query('channel'),

@ -7,10 +7,10 @@ import {
validateRequest validateRequest
} from '../../middleware'; } from '../../middleware';
import { body } from 'express-validator'; import { body } from 'express-validator';
import { ADMIN, MEMBER, GRANTED } from '../../variables'; import { ADMIN, MEMBER } from '../../variables';
import { serviceTokenController } from '../../controllers/v1'; import { serviceTokenController } from '../../controllers/v1';
// TODO: revoke service token // note: deprecate service-token routes in favor of service-token data routes/structure
router.get( router.get(
'/', '/',
@ -20,10 +20,11 @@ router.get(
router.post( router.post(
'/', '/',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [GRANTED],
location: 'body' location: 'body'
}), }),
body('name').exists().trim().notEmpty(), body('name').exists().trim().notEmpty(),

@ -3,6 +3,12 @@ const router = express.Router();
import { requireAuth } from '../../middleware'; import { requireAuth } from '../../middleware';
import { userController } from '../../controllers/v1'; import { userController } from '../../controllers/v1';
router.get('/', requireAuth, userController.getUser); router.get(
'/',
requireAuth({
acceptedAuthModes: ['jwt']
}),
userController.getUser
);
export default router; export default router;

@ -4,9 +4,12 @@ import { requireAuth, validateRequest } from '../../middleware';
import { body, query } from 'express-validator'; import { body, query } from 'express-validator';
import { userActionController } from '../../controllers/v1'; import { userActionController } from '../../controllers/v1';
// note: [userAction] will be deprecated in /v2 in favor of [action]
router.post( router.post(
'/', '/',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
body('action'), body('action'),
validateRequest, validateRequest,
userActionController.addUserAction userActionController.addUserAction
@ -14,7 +17,9 @@ router.post(
router.get( router.get(
'/', '/',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
query('action'), query('action'),
validateRequest, validateRequest,
userActionController.getUserAction userActionController.getUserAction

@ -6,15 +6,16 @@ import {
requireWorkspaceAuth, requireWorkspaceAuth,
validateRequest validateRequest
} from '../../middleware'; } from '../../middleware';
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables'; import { ADMIN, MEMBER } from '../../variables';
import { workspaceController, membershipController } from '../../controllers/v1'; import { workspaceController, membershipController } from '../../controllers/v1';
router.get( router.get(
'/:workspaceId/keys', '/:workspaceId/keys',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER]
acceptedStatuses: [COMPLETED, GRANTED]
}), }),
param('workspaceId').exists().trim(), param('workspaceId').exists().trim(),
validateRequest, validateRequest,
@ -23,24 +24,32 @@ router.get(
router.get( router.get(
'/:workspaceId/users', '/:workspaceId/users',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [COMPLETED, GRANTED]
}), }),
param('workspaceId').exists().trim(), param('workspaceId').exists().trim(),
validateRequest, validateRequest,
workspaceController.getWorkspaceMemberships workspaceController.getWorkspaceMemberships
); );
router.get('/', requireAuth, workspaceController.getWorkspaces); router.get(
'/',
requireAuth({
acceptedAuthModes: ['jwt']
}),
workspaceController.getWorkspaces
);
router.get( router.get(
'/:workspaceId', '/:workspaceId',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER]
acceptedStatuses: [COMPLETED, GRANTED]
}), }),
param('workspaceId').exists().trim(), param('workspaceId').exists().trim(),
validateRequest, validateRequest,
@ -49,7 +58,9 @@ router.get(
router.post( router.post(
'/', '/',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
body('workspaceName').exists().trim().notEmpty(), body('workspaceName').exists().trim().notEmpty(),
body('organizationId').exists().trim().notEmpty(), body('organizationId').exists().trim().notEmpty(),
validateRequest, validateRequest,
@ -58,10 +69,11 @@ router.post(
router.delete( router.delete(
'/:workspaceId', '/:workspaceId',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN], acceptedRoles: [ADMIN]
acceptedStatuses: [GRANTED]
}), }),
param('workspaceId').exists().trim(), param('workspaceId').exists().trim(),
validateRequest, validateRequest,
@ -70,10 +82,11 @@ router.delete(
router.post( router.post(
'/:workspaceId/name', '/:workspaceId/name',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER]
acceptedStatuses: [COMPLETED, GRANTED]
}), }),
param('workspaceId').exists().trim(), param('workspaceId').exists().trim(),
body('name').exists().trim().notEmpty(), body('name').exists().trim().notEmpty(),
@ -83,10 +96,11 @@ router.post(
router.post( router.post(
'/:workspaceId/invite-signup', '/:workspaceId/invite-signup',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER]
acceptedStatuses: [GRANTED]
}), }),
param('workspaceId').exists().trim(), param('workspaceId').exists().trim(),
body('email').exists().trim().notEmpty(), body('email').exists().trim().notEmpty(),
@ -96,10 +110,11 @@ router.post(
router.get( router.get(
'/:workspaceId/integrations', '/:workspaceId/integrations',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER]
acceptedStatuses: [GRANTED]
}), }),
param('workspaceId').exists().trim(), param('workspaceId').exists().trim(),
validateRequest, validateRequest,
@ -108,10 +123,11 @@ router.get(
router.get( router.get(
'/:workspaceId/authorizations', '/:workspaceId/authorizations',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER]
acceptedStatuses: [GRANTED]
}), }),
param('workspaceId').exists().trim(), param('workspaceId').exists().trim(),
validateRequest, validateRequest,
@ -119,11 +135,12 @@ router.get(
); );
router.get( router.get(
'/:workspaceId/service-tokens', '/:workspaceId/service-tokens', // deprecate
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER]
acceptedStatuses: [GRANTED]
}), }),
param('workspaceId').exists().trim(), param('workspaceId').exists().trim(),
validateRequest, validateRequest,

@ -0,0 +1,39 @@
import express from 'express';
const router = express.Router();
import {
requireAuth,
validateRequest
} from '../../middleware';
import { param, body } from 'express-validator';
import { apiKeyDataController } from '../../controllers/v2';
router.get(
'/',
requireAuth({
acceptedAuthModes: ['jwt']
}),
apiKeyDataController.getAPIKeyData
);
router.post(
'/',
requireAuth({
acceptedAuthModes: ['jwt']
}),
body('name').exists().trim(),
body('expiresIn'), // measured in ms
validateRequest,
apiKeyDataController.createAPIKeyData
);
router.delete(
'/:apiKeyDataId',
requireAuth({
acceptedAuthModes: ['jwt']
}),
param('apiKeyDataId').exists().trim(),
validateRequest,
apiKeyDataController.deleteAPIKeyData
);
export default router;

@ -1,7 +1,11 @@
import secret from './secret'; import secret from './secret';
import workspace from './workspace'; import workspace from './workspace';
import serviceTokenData from './serviceTokenData';
import apiKeyData from './apiKeyData';
export { export {
secret, secret,
workspace workspace,
serviceTokenData,
apiKeyData
} }

@ -1,4 +1,144 @@
import express from 'express'; import express, { Request, Response } from 'express';
import { requireAuth, requireWorkspaceAuth, validateRequest } from '../../middleware';
import { body, param, query } from 'express-validator';
import { ADMIN, MEMBER } from '../../variables';
import { CreateSecretRequestBody, ModifySecretRequestBody } from '../../types/secret';
import { secretController } from '../../controllers/v2';
import { fetchAllSecrets, fetchSingleSecret } from '../../controllers/v2/secretController';
const router = express.Router(); const router = express.Router();
/**
* Create many secrets for a given workspace and environmentName
*/
router.post(
'/batch-create/workspace/:workspaceId/environment/:environmentName',
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
}),
param('workspaceId').exists().isMongoId().trim(),
param('environmentName').exists().trim(),
body('secrets').exists().isArray().custom((value) => value.every((item: CreateSecretRequestBody) => typeof item === 'object')),
validateRequest,
secretController.batchCreateSecrets
);
/**
* Create single secret for a given workspace and environmentName
*/
router.post(
'/workspace/:workspaceId/environment/:environmentName',
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
}),
param('workspaceId').exists().isMongoId().trim(),
param('environmentName').exists().trim(),
body('secret').exists().isObject(),
validateRequest,
secretController.createSingleSecret
);
/**
* Get all secrets for a given environment and workspace id
*/
router.get(
'/workspace/:workspaceId',
param('workspaceId').exists().trim(),
query("environment").exists(),
requireAuth({
acceptedAuthModes: ['jwt', 'serviceToken']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
}),
validateRequest,
fetchAllSecrets
);
/**
* Get single secret by id
*/
router.get(
'/:secretId',
requireAuth({
acceptedAuthModes: ['jwt', 'serviceToken']
}),
validateRequest,
fetchSingleSecret
);
/**
* Batch delete secrets in a given workspace and environment name
*/
router.delete(
'/batch/workspace/:workspaceId/environment/:environmentName',
requireAuth({
acceptedAuthModes: ['jwt']
}),
param('workspaceId').exists().isMongoId().trim(),
param('environmentName').exists().trim(),
body('secretIds').exists().isArray().custom(array => array.length > 0),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
}),
validateRequest,
secretController.batchDeleteSecrets
);
/**
* delete single secret by id
*/
router.delete(
'/:secretId',
requireAuth({
acceptedAuthModes: ['jwt']
}),
param('secretId').isMongoId(),
validateRequest,
secretController.deleteSingleSecret
);
/**
* Apply modifications to many existing secrets in a given workspace and environment
*/
router.patch(
'/batch-modify/workspace/:workspaceId/environment/:environmentName',
requireAuth({
acceptedAuthModes: ['jwt']
}),
body('secrets').exists().isArray().custom((secrets: ModifySecretRequestBody[]) => secrets.length > 0),
param('workspaceId').exists().isMongoId().trim(),
param('environmentName').exists().trim(),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
}),
validateRequest,
secretController.batchModifySecrets
);
/**
* Apply modifications to single existing secret in a given workspace and environment
*/
router.patch(
'/workspace/:workspaceId/environment/:environmentName',
requireAuth({
acceptedAuthModes: ['jwt']
}),
body('secret').isObject(),
param('workspaceId').exists().isMongoId().trim(),
param('environmentName').exists().trim(),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
}),
validateRequest,
secretController.modifySingleSecrets
);
export default router; export default router;

@ -0,0 +1,57 @@
import express from 'express';
const router = express.Router();
import {
requireAuth,
requireWorkspaceAuth,
requireServiceTokenDataAuth,
validateRequest
} from '../../middleware';
import { param, body } from 'express-validator';
import {
ADMIN,
MEMBER,
} from '../../variables';
import { serviceTokenDataController } from '../../controllers/v2';
router.get(
'/',
requireAuth({
acceptedAuthModes: ['serviceToken']
}),
serviceTokenDataController.getServiceTokenData
);
router.post(
'/',
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
location: 'body'
}),
body('name').exists().trim(),
body('workspaceId'),
body('environment'),
body('encryptedKey'),
body('iv'),
body('tag'),
body('expiresIn'), // measured in ms
validateRequest,
serviceTokenDataController.createServiceTokenData
);
router.delete(
'/:serviceTokenDataId',
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireServiceTokenDataAuth({
acceptedRoles: [ADMIN, MEMBER]
}),
param('serviceTokenDataId').exists().trim(),
validateRequest,
serviceTokenDataController.deleteServiceTokenData
);
export default router;

@ -4,140 +4,18 @@ import { body, param, query } from 'express-validator';
import { import {
requireAuth, requireAuth,
requireWorkspaceAuth, requireWorkspaceAuth,
requireServiceTokenAuth,
validateRequest validateRequest
} from '../../middleware'; } from '../../middleware';
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables'; import { ADMIN, MEMBER } from '../../variables';
import { membershipController } from '../../controllers/v1';
import { workspaceController } from '../../controllers/v2'; import { workspaceController } from '../../controllers/v2';
router.get(
'/:workspaceId/keys',
requireAuth,
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [COMPLETED, GRANTED]
}),
param('workspaceId').exists().trim(),
validateRequest,
workspaceController.getWorkspacePublicKeys
);
router.get(
'/:workspaceId/users',
requireAuth,
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [COMPLETED, GRANTED]
}),
param('workspaceId').exists().trim(),
validateRequest,
workspaceController.getWorkspaceMemberships
);
router.get('/', requireAuth, workspaceController.getWorkspaces);
router.get(
'/:workspaceId',
requireAuth,
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [COMPLETED, GRANTED]
}),
param('workspaceId').exists().trim(),
validateRequest,
workspaceController.getWorkspace
);
router.post(
'/',
requireAuth,
body('workspaceName').exists().trim().notEmpty(),
body('organizationId').exists().trim().notEmpty(),
validateRequest,
workspaceController.createWorkspace
);
router.delete(
'/:workspaceId',
requireAuth,
requireWorkspaceAuth({
acceptedRoles: [ADMIN],
acceptedStatuses: [GRANTED]
}),
param('workspaceId').exists().trim(),
validateRequest,
workspaceController.deleteWorkspace
);
router.post(
'/:workspaceId/name',
requireAuth,
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [COMPLETED, GRANTED]
}),
param('workspaceId').exists().trim(),
body('name').exists().trim().notEmpty(),
validateRequest,
workspaceController.changeWorkspaceName
);
router.post(
'/:workspaceId/invite-signup',
requireAuth,
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [GRANTED]
}),
param('workspaceId').exists().trim(),
body('email').exists().trim().notEmpty(),
validateRequest,
membershipController.inviteUserToWorkspace
);
router.get(
'/:workspaceId/integrations',
requireAuth,
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [GRANTED]
}),
param('workspaceId').exists().trim(),
validateRequest,
workspaceController.getWorkspaceIntegrations
);
router.get(
'/:workspaceId/authorizations',
requireAuth,
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [GRANTED]
}),
param('workspaceId').exists().trim(),
validateRequest,
workspaceController.getWorkspaceIntegrationAuthorizations
);
router.get( // TODO: modify
'/:workspaceId/service-tokens',
requireAuth,
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [GRANTED]
}),
param('workspaceId').exists().trim(),
validateRequest,
workspaceController.getWorkspaceServiceTokens
);
router.post( router.post(
'/:workspaceId/secrets', '/:workspaceId/secrets',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER]
acceptedStatuses: [COMPLETED, GRANTED]
}), }),
body('secrets').exists(), body('secrets').exists(),
body('keys').exists(), body('keys').exists(),
@ -150,10 +28,11 @@ router.post(
router.get( router.get(
'/:workspaceId/secrets', '/:workspaceId/secrets',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt', 'serviceToken']
}),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER]
acceptedStatuses: [COMPLETED, GRANTED]
}), }),
query('environment').exists().trim(), query('environment').exists().trim(),
query('channel'), query('channel'),
@ -162,15 +41,30 @@ router.get(
workspaceController.pullSecrets workspaceController.pullSecrets
); );
router.get( // TODO: modify based on upcoming serviceTokenData changes router.get(
'/:workspaceId/secrets-service-token', '/:workspaceId/encrypted-key',
requireServiceTokenAuth, requireAuth({
query('environment').exists().trim(), acceptedAuthModes: ['jwt']
query('channel'), }),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
}),
param('workspaceId').exists().trim(), param('workspaceId').exists().trim(),
validateRequest, validateRequest,
workspaceController.pullSecretsServiceToken workspaceController.getWorkspaceKey
); );
router.get(
'/:workspaceId/service-token-data',
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
}),
param('workspaceId').exists().trim(),
validateRequest,
workspaceController.getWorkspaceServiceTokenData
);
export default router; export default router;

@ -0,0 +1,16 @@
import mongoose from 'mongoose';
import { getLogger } from '../utils/logger';
import { initDatabaseHelper } from '../helpers/database';
/**
* Class to handle database actions
*/
class DatabaseService {
static async initDatabase(MONGO_URL: string) {
return await initDatabaseHelper({
mongoURL: MONGO_URL
});
}
}
export default DatabaseService;

@ -1,10 +0,0 @@
import mongoose from 'mongoose';
import { getLogger } from '../utils/logger';
export const initDatabase = (MONGO_URL: string) => {
mongoose
.connect(MONGO_URL)
.then(() => getLogger("database").info("Database connection established"))
.catch((e) => getLogger("database").error(`Unable to establish Database connection due to the error.\n${e}`));
return mongoose.connection;
};

@ -1,9 +1,11 @@
import DatabaseService from './DatabaseService';
import postHogClient from './PostHogClient'; import postHogClient from './PostHogClient';
import BotService from './BotService'; import BotService from './BotService';
import EventService from './EventService'; import EventService from './EventService';
import IntegrationService from './IntegrationService'; import IntegrationService from './IntegrationService';
export { export {
DatabaseService,
postHogClient, postHogClient,
BotService, BotService,
EventService, EventService,

@ -1,6 +1,5 @@
import * as express from 'express'; import * as express from 'express';
// TODO: fix (any) types // TODO: fix (any) types
declare global { declare global {
namespace Express { namespace Express {
@ -8,13 +7,17 @@ declare global {
user: any; user: any;
workspace: any; workspace: any;
membership: any; membership: any;
organizationt: any; organization: any;
membershipOrg: any; membershipOrg: any;
integration: any; integration: any;
integrationAuth: any; integrationAuth: any;
bot: any; bot: any;
secret: any;
secretSnapshot: any;
serviceToken: any; serviceToken: any;
accessToken: any; accessToken: any;
serviceTokenData: any;
apiKeyData: any;
query?: any; query?: any;
} }
} }

14
backend/src/types/secret/index.d.ts vendored Normal file

@ -0,0 +1,14 @@
import { Assign, Omit } from 'utility-types';
import { ISecret } from '../../models';
// Everything is required, except the omitted types
export type CreateSecretRequestBody = Omit<ISecret, "user" | "version" | "environment" | "workspace">;
// Omit the listed properties, then make everything optional and then make _id required
export type ModifySecretRequestBody = Assign<Partial<Omit<ISecret, "user" | "version" | "environment" | "workspace">>, { _id: string }>;
// Used for modeling sanitized secrets before uplaod. To be used for converting user input for uploading
export type SanitizedSecretModify = Partial<Omit<ISecret, "user" | "version" | "environment" | "workspace">>;
// Everything is required, except the omitted types
export type SanitizedSecretForCreate = Omit<ISecret, "version" | "_id">;

@ -8,7 +8,7 @@ export const RouteNotFoundError = (error?: Partial<RequestErrorContext>) => new
message: error?.message ?? 'The requested source was not found', message: error?.message ?? 'The requested source was not found',
context: error?.context, context: error?.context,
stack: error?.stack stack: error?.stack
}) });
export const MethodNotAllowedError = (error?: Partial<RequestErrorContext>) => new RequestError({ export const MethodNotAllowedError = (error?: Partial<RequestErrorContext>) => new RequestError({
logLevel: error?.logLevel ?? LogLevel.INFO, logLevel: error?.logLevel ?? LogLevel.INFO,
@ -17,7 +17,7 @@ export const MethodNotAllowedError = (error?: Partial<RequestErrorContext>) => n
message: error?.message ?? 'The requested method is not allowed for the resource', message: error?.message ?? 'The requested method is not allowed for the resource',
context: error?.context, context: error?.context,
stack: error?.stack stack: error?.stack
}) });
export const UnauthorizedRequestError = (error?: Partial<RequestErrorContext>) => new RequestError({ export const UnauthorizedRequestError = (error?: Partial<RequestErrorContext>) => new RequestError({
logLevel: error?.logLevel ?? LogLevel.INFO, logLevel: error?.logLevel ?? LogLevel.INFO,
@ -26,7 +26,7 @@ export const UnauthorizedRequestError = (error?: Partial<RequestErrorContext>) =
message: error?.message ?? 'You are not authorized to access this resource', message: error?.message ?? 'You are not authorized to access this resource',
context: error?.context, context: error?.context,
stack: error?.stack stack: error?.stack
}) });
export const ForbiddenRequestError = (error?: Partial<RequestErrorContext>) => new RequestError({ export const ForbiddenRequestError = (error?: Partial<RequestErrorContext>) => new RequestError({
logLevel: error?.logLevel ?? LogLevel.INFO, logLevel: error?.logLevel ?? LogLevel.INFO,
@ -35,7 +35,7 @@ export const ForbiddenRequestError = (error?: Partial<RequestErrorContext>) => n
message: error?.message ?? 'You are not allowed to access this resource', message: error?.message ?? 'You are not allowed to access this resource',
context: error?.context, context: error?.context,
stack: error?.stack stack: error?.stack
}) });
export const BadRequestError = (error?: Partial<RequestErrorContext>) => new RequestError({ export const BadRequestError = (error?: Partial<RequestErrorContext>) => new RequestError({
logLevel: error?.logLevel ?? LogLevel.INFO, logLevel: error?.logLevel ?? LogLevel.INFO,
@ -44,7 +44,7 @@ export const BadRequestError = (error?: Partial<RequestErrorContext>) => new Req
message: error?.message ?? 'The request is invalid or cannot be served', message: error?.message ?? 'The request is invalid or cannot be served',
context: error?.context, context: error?.context,
stack: error?.stack stack: error?.stack
}) });
export const InternalServerError = (error?: Partial<RequestErrorContext>) => new RequestError({ export const InternalServerError = (error?: Partial<RequestErrorContext>) => new RequestError({
logLevel: error?.logLevel ?? LogLevel.ERROR, logLevel: error?.logLevel ?? LogLevel.ERROR,
@ -53,7 +53,7 @@ export const InternalServerError = (error?: Partial<RequestErrorContext>) => new
message: error?.message ?? 'The server encountered an error while processing the request', message: error?.message ?? 'The server encountered an error while processing the request',
context: error?.context, context: error?.context,
stack: error?.stack stack: error?.stack
}) });
export const ServiceUnavailableError = (error?: Partial<RequestErrorContext>) => new RequestError({ export const ServiceUnavailableError = (error?: Partial<RequestErrorContext>) => new RequestError({
logLevel: error?.logLevel ?? LogLevel.ERROR, logLevel: error?.logLevel ?? LogLevel.ERROR,
@ -62,7 +62,7 @@ export const ServiceUnavailableError = (error?: Partial<RequestErrorContext>) =>
message: error?.message ?? 'The service is currently unavailable. Please try again later.', message: error?.message ?? 'The service is currently unavailable. Please try again later.',
context: error?.context, context: error?.context,
stack: error?.stack stack: error?.stack
}) });
export const ValidationError = (error?: Partial<RequestErrorContext>) => new RequestError({ export const ValidationError = (error?: Partial<RequestErrorContext>) => new RequestError({
logLevel: error?.logLevel ?? LogLevel.ERROR, logLevel: error?.logLevel ?? LogLevel.ERROR,
@ -71,7 +71,7 @@ export const ValidationError = (error?: Partial<RequestErrorContext>) => new Req
message: error?.message ?? 'The request failed validation', message: error?.message ?? 'The request failed validation',
context: error?.context, context: error?.context,
stack: error?.stack stack: error?.stack
}) });
//* ----->[INTEGRATION ERRORS]<----- //* ----->[INTEGRATION ERRORS]<-----
export const IntegrationNotFoundError = (error?: Partial<RequestErrorContext>) => new RequestError({ export const IntegrationNotFoundError = (error?: Partial<RequestErrorContext>) => new RequestError({
@ -81,7 +81,7 @@ export const IntegrationNotFoundError = (error?: Partial<RequestErrorContext>) =
message: error?.message ?? 'The requested integration was not found', message: error?.message ?? 'The requested integration was not found',
context: error?.context, context: error?.context,
stack: error?.stack stack: error?.stack
}) });
//* ----->[WORKSPACE ERRORS]<----- //* ----->[WORKSPACE ERRORS]<-----
export const WorkspaceNotFoundError = (error?: Partial<RequestErrorContext>) => new RequestError({ export const WorkspaceNotFoundError = (error?: Partial<RequestErrorContext>) => new RequestError({
@ -91,7 +91,7 @@ export const WorkspaceNotFoundError = (error?: Partial<RequestErrorContext>) =>
message: error?.message ?? 'The requested workspace was not found', message: error?.message ?? 'The requested workspace was not found',
context: error?.context, context: error?.context,
stack: error?.stack stack: error?.stack
}) });
//* ----->[ORGANIZATION ERRORS]<----- //* ----->[ORGANIZATION ERRORS]<-----
export const OrganizationNotFoundError = (error?: Partial<RequestErrorContext>) => new RequestError({ export const OrganizationNotFoundError = (error?: Partial<RequestErrorContext>) => new RequestError({
@ -101,7 +101,7 @@ export const OrganizationNotFoundError = (error?: Partial<RequestErrorContext>)
message: error?.message ?? 'The requested organization was not found', message: error?.message ?? 'The requested organization was not found',
context: error?.context, context: error?.context,
stack: error?.stack stack: error?.stack
}) });
//* ----->[ACCOUNT ERRORS]<----- //* ----->[ACCOUNT ERRORS]<-----
export const AccountNotFoundError = (error?: Partial<RequestErrorContext>) => new RequestError({ export const AccountNotFoundError = (error?: Partial<RequestErrorContext>) => new RequestError({
@ -111,6 +111,56 @@ export const AccountNotFoundError = (error?: Partial<RequestErrorContext>) => ne
message: error?.message ?? 'The requested account was not found', message: error?.message ?? 'The requested account was not found',
context: error?.context, context: error?.context,
stack: error?.stack stack: error?.stack
});
//* ----->[SECRET ERRORS]<-----
export const SecretNotFoundError = (error?: Partial<RequestErrorContext>) => new RequestError({
logLevel: error?.logLevel ?? LogLevel.ERROR,
statusCode: error?.statusCode ?? 404,
type: error?.type ?? 'secret_not_found_error',
message: error?.message ?? 'The requested secret was not found',
context: error?.context,
stack: error?.stack
});
//* ----->[SECRET SNAPSHOT ERRORS]<-----
export const SecretSnapshotNotFoundError = (error?: Partial<RequestErrorContext>) => new RequestError({
logLevel: error?.logLevel ?? LogLevel.ERROR,
statusCode: error?.statusCode ?? 404,
type: error?.type ?? 'secret_snapshot_not_found_error',
message: error?.message ?? 'The requested secret snapshot was not found',
context: error?.context,
stack: error?.stack
});
//* ----->[ACTION ERRORS]<-----
export const ActionNotFoundError = (error?: Partial<RequestErrorContext>) => new RequestError({
logLevel: error?.logLevel ?? LogLevel.ERROR,
statusCode: error?.statusCode ?? 404,
type: error?.type ?? 'action_not_found_error',
message: error?.message ?? 'The requested action was not found',
context: error?.context,
stack: error?.stack
});
//* ----->[SERVICE TOKEN DATA ERRORS]<-----
export const ServiceTokenDataNotFoundError = (error?: Partial<RequestErrorContext>) => new RequestError({
logLevel: error?.logLevel ?? LogLevel.ERROR,
statusCode: error?.statusCode ?? 404,
type: error?.type ?? 'service_token_data_not_found_error',
message: error?.message ?? 'The requested service token data was not found',
context: error?.context,
stack: error?.stack
})
//* ----->[API KEY DATA ERRORS]<-----
export const APIKeyDataNotFoundError = (error?: Partial<RequestErrorContext>) => new RequestError({
logLevel: error?.logLevel ?? LogLevel.ERROR,
statusCode: error?.statusCode ?? 404,
type: error?.type ?? 'service_token_data_not_found_error',
message: error?.message ?? 'The requested service token data was not found',
context: error?.context,
stack: error?.stack
}) })
//* ----->[MISC ERRORS]<----- //* ----->[MISC ERRORS]<-----

@ -0,0 +1,11 @@
const ACTION_ADD_SECRETS = 'addSecrets';
const ACTION_DELETE_SECRETS = 'deleteSecrets';
const ACTION_UPDATE_SECRETS = 'updateSecrets';
const ACTION_READ_SECRETS = 'readSecrets';
export {
ACTION_ADD_SECRETS,
ACTION_DELETE_SECRETS,
ACTION_UPDATE_SECRETS,
ACTION_READ_SECRETS
}

@ -28,11 +28,15 @@ import {
MEMBER, MEMBER,
INVITED, INVITED,
ACCEPTED, ACCEPTED,
COMPLETED,
GRANTED
} from './organization'; } from './organization';
import { SECRET_SHARED, SECRET_PERSONAL } from './secret'; import { SECRET_SHARED, SECRET_PERSONAL } from './secret';
import { EVENT_PUSH_SECRETS, EVENT_PULL_SECRETS } from './event'; import { EVENT_PUSH_SECRETS, EVENT_PULL_SECRETS } from './event';
import {
ACTION_ADD_SECRETS,
ACTION_UPDATE_SECRETS,
ACTION_DELETE_SECRETS,
ACTION_READ_SECRETS
} from './action';
import { SMTP_HOST_SENDGRID, SMTP_HOST_MAILGUN } from './smtp'; import { SMTP_HOST_SENDGRID, SMTP_HOST_MAILGUN } from './smtp';
import { PLAN_STARTER, PLAN_PRO } from './stripe'; import { PLAN_STARTER, PLAN_PRO } from './stripe';
@ -42,8 +46,6 @@ export {
MEMBER, MEMBER,
INVITED, INVITED,
ACCEPTED, ACCEPTED,
COMPLETED,
GRANTED,
SECRET_SHARED, SECRET_SHARED,
SECRET_PERSONAL, SECRET_PERSONAL,
ENV_DEV, ENV_DEV,
@ -67,6 +69,10 @@ export {
INTEGRATION_GITHUB_API_URL, INTEGRATION_GITHUB_API_URL,
EVENT_PUSH_SECRETS, EVENT_PUSH_SECRETS,
EVENT_PULL_SECRETS, EVENT_PULL_SECRETS,
ACTION_ADD_SECRETS,
ACTION_UPDATE_SECRETS,
ACTION_DELETE_SECRETS,
ACTION_READ_SECRETS,
INTEGRATION_OPTIONS, INTEGRATION_OPTIONS,
SMTP_HOST_SENDGRID, SMTP_HOST_SENDGRID,
SMTP_HOST_MAILGUN, SMTP_HOST_MAILGUN,

@ -48,7 +48,7 @@ const INTEGRATION_OPTIONS = [
name: 'Vercel', name: 'Vercel',
slug: 'vercel', slug: 'vercel',
image: 'Vercel', image: 'Vercel',
isAvailable: true, isAvailable: false,
type: 'vercel', type: 'vercel',
clientId: '', clientId: '',
clientSlug: CLIENT_SLUG_VERCEL, clientSlug: CLIENT_SLUG_VERCEL,
@ -58,7 +58,7 @@ const INTEGRATION_OPTIONS = [
name: 'Netlify', name: 'Netlify',
slug: 'netlify', slug: 'netlify',
image: 'Netlify', image: 'Netlify',
isAvailable: true, isAvailable: false,
type: 'oauth2', type: 'oauth2',
clientId: CLIENT_ID_NETLIFY, clientId: CLIENT_ID_NETLIFY,
docsLink: '' docsLink: ''
@ -67,7 +67,7 @@ const INTEGRATION_OPTIONS = [
name: 'GitHub', name: 'GitHub',
slug: 'github', slug: 'github',
image: 'GitHub', image: 'GitHub',
isAvailable: true, isAvailable: false,
type: 'oauth2', type: 'oauth2',
clientId: CLIENT_ID_GITHUB, clientId: CLIENT_ID_GITHUB,
docsLink: '' docsLink: ''

@ -9,16 +9,10 @@ const INVITED = 'invited';
// -- organization // -- organization
const ACCEPTED = 'accepted'; const ACCEPTED = 'accepted';
// -- workspace
const COMPLETED = 'completed';
const GRANTED = 'granted';
export { export {
OWNER, OWNER,
ADMIN, ADMIN,
MEMBER, MEMBER,
INVITED, INVITED,
ACCEPTED, ACCEPTED
COMPLETED,
GRANTED
} }

@ -1,7 +1,9 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es2016", "target": "es2016",
"lib": ["es6"], "lib": [
"es6"
],
"module": "commonjs", "module": "commonjs",
"rootDir": "src", "rootDir": "src",
"resolveJsonModule": true, "resolveJsonModule": true,
@ -13,8 +15,15 @@
"strict": true, "strict": true,
"noImplicitAny": true, "noImplicitAny": true,
"skipLibCheck": true, "skipLibCheck": true,
"typeRoots": ["./src/types", "./node_modules/@types"] "typeRoots": [
"./src/types",
"./node_modules/@types"
]
}, },
"include": ["src/**/*"], "include": [
"exclude": ["node_modules"] "src/**/*"
} ],
"exclude": [
"node_modules"
]
}

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