1
0
mirror of https://github.com/Infisical/infisical.git synced 2025-03-21 11:55:08 +00:00

Compare commits

..

172 Commits

Author SHA1 Message Date
7152e16288 add message to avoid typing in file passphrase 2023-01-06 01:30:08 -05:00
0d8e1042ba update crud CLI docs and remove projectID from run command 2023-01-06 01:18:12 -05:00
85be609290 Remove comment from posthog fn 2023-01-06 00:59:54 -05:00
37998b84a9 Add logs for posthog in frontend 2023-01-05 23:15:30 -05:00
37c66c2499 update variable to check telemetry 2023-01-05 22:12:54 -05:00
b60f0c1556 Increase cli version and decode hex for service token encryption key 2023-01-05 17:55:49 -05:00
054b3e3450 Merge branch 'main' of https://github.com/Infisical/infisical 2023-01-05 14:52:07 -08:00
de9d832669 Temporarily remove integrations as available 2023-01-05 14:24:16 -08:00
68b99b9f00 central logging and 2v service token 2023-01-05 16:58:10 -05:00
2c8c7a1777 add helper functions to check workspace and login status 2023-01-05 16:58:10 -05:00
764636cd47 Add service token v2 api into cli 2023-01-05 16:58:10 -05:00
c921eb8781 Telemetry and graphic changes 2023-01-05 13:11:13 -08:00
5a19f8ed32 Hotfix: state updates for dashabord and service tokens 2023-01-05 09:46:04 -08:00
8ddcccabfa Merge pull request from Infisical/activity-logs
Activity logs
2023-01-05 19:50:32 +07:00
db36b81b0c Update package-lock.json 2023-01-05 19:47:31 +07:00
85cb3a11aa Fix frontend endpoints for service tokens and patch secret index.d.ts error 2023-01-05 19:25:33 +07:00
6e125b9e74 Merge remote-tracking branch 'origin' into activity-logs 2023-01-05 18:23:43 +07:00
4dce7e87dc Merge pull request from Infisical/dependabot/npm_and_yarn/backend/json5-2.2.3
Bump json5 from 2.2.1 to 2.2.3 in /backend
2023-01-04 21:43:31 -08:00
ca2f44be54 Bump json5 from 2.2.1 to 2.2.3 in /backend
Bumps [json5](https://github.com/json5/json5) from 2.2.1 to 2.2.3.
- [Release notes](https://github.com/json5/json5/releases)
- [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md)
- [Commits](https://github.com/json5/json5/compare/v2.2.1...v2.2.3)

---
updated-dependencies:
- dependency-name: json5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-05 05:41:35 +00:00
021250a58c Update dependencies 2023-01-04 21:40:37 -08:00
3f0eefb091 Merge branch 'main' into activity-logs 2023-01-04 21:12:24 -08:00
cc408d8908 Update channel in workspace v2 controller 2023-01-05 12:05:49 +07:00
8b48205881 Fixed the service token bugs 2023-01-04 20:22:51 -08:00
9cf28fef5f Service tokens update on frontend 2023-01-04 18:54:45 -08:00
6c88c4dc36 Updated the image for signup invites 2023-01-04 17:34:30 -08:00
5428766bf6 modify getServiceTokenData to return single json 2023-01-04 20:17:11 -05:00
347b7201de Finished secret snapshots 2023-01-04 17:11:07 -08:00
d75d9ec324 Add get call secrets route for service token and jwt 2023-01-04 20:05:00 -05:00
880f4d25a9 print all errors during backend dev 2023-01-04 17:35:40 -05:00
fba40b5d4b print requestError logs in backend when in dev mode 2023-01-04 15:32:45 -05:00
68c488b8ee Add acceptedAuthModes for v2 secrets 2023-01-04 14:58:16 -05:00
68a8471292 remove accepted roles from secrets v2 api 2023-01-04 10:58:00 -05:00
7e026e82bb Merge crud cli api and cli changes 2023-01-04 10:43:57 -05:00
fe05732c46 update host to prod host 2023-01-04 10:39:32 -05:00
df7340e440 Fix merge conflicts 2023-01-04 21:31:58 +07:00
e364094d0d Merge pull request from Infisical/cleaning
Remove accept statuses
2023-01-04 21:26:48 +07:00
136fda37f2 Merge remote-tracking branch 'origin' into cleaning 2023-01-04 21:24:38 +07:00
54676c630e Remove accept statuses 2023-01-04 21:14:25 +07:00
d7dd65b181 Merge pull request from Infisical/api-key
Add API Key auth mode
2023-01-04 20:44:38 +07:00
d3efe351f1 Add DELETE route to API keys 2023-01-04 20:38:37 +07:00
c7fb9209c4 Complete v1 support for API key auth mode 2023-01-04 20:27:16 +07:00
8c7c41e091 Merge remote-tracking branch 'origin' into api-key 2023-01-04 18:15:54 +07:00
58830eab79 Move get service token data to v2 routes 2023-01-04 18:15:32 +07:00
ff0b053d12 Begin API Key functionality 2023-01-04 18:04:53 +07:00
15db792058 Patch requireAuth middleware in getting secret snapshot by id 2023-01-04 15:04:09 +07:00
5967a5cdba Add endpoint to return count of secret snapshots for a workspace 2023-01-04 10:00:05 +07:00
078c67f27c Add crud cli docs 2023-01-03 17:43:40 -05:00
3e945dd552 move v2 secret api to controller 2023-01-03 16:40:08 -05:00
59f5ad7710 add expand flag to crud sli 2023-01-03 16:39:33 -05:00
7e71e3ca57 v1 crud secrets complete 2023-01-03 16:09:47 -05:00
fb394de428 Remove unecessary imports 2023-01-03 16:02:05 +07:00
9727075b0b Resolve merge conflicts 2023-01-03 15:59:37 +07:00
c7c5a947d2 Modify secret snapshots to point to secret versions 2023-01-03 15:53:06 +07:00
9d0e269a2a Moved project id from dashboard to settings 2023-01-02 20:41:20 -08:00
92ab29f746 Merge branch 'activity-logs' of https://github.com/Infisical/infisical into activity-logs 2023-01-02 20:17:36 -08:00
fe0c466523 Moved the delete button to the sidebar 2023-01-02 20:17:16 -08:00
679db32de9 Begin docs for secret versioning, snapshots, and audit logs 2023-01-03 10:49:58 +07:00
daf8a73529 add dynmaic workspace and user creds for secrets cmd 2023-01-02 22:41:15 -05:00
d0949b2e19 Fixed the sorting buf with version history 2023-01-02 18:53:56 -08:00
212ca72c7b Merge pull request from Infisical/service-token-v2
Infisical Token V2
2023-01-03 09:36:56 +07:00
48defca012 Merge remote-tracking branch 'origin' into service-token-v2 2023-01-03 09:34:48 +07:00
6845e9129a Updated icon for activity logs 2023-01-02 18:33:24 -08:00
e9601307ef Move service token data routes and controllers to v2 2023-01-03 09:33:00 +07:00
0ff8194cf8 Modify getWorkspaceLogs to accept sortBy query param 2023-01-03 09:19:07 +07:00
14286795e9 Merge branch 'activity-logs' of https://github.com/Infisical/infisical into activity-logs 2023-01-03 08:19:43 +07:00
ae5320e4fa Finished activity logs V1 2023-01-02 14:20:39 -08:00
03b7d3a5ce Wired frontend for logs 2023-01-02 09:57:02 -08:00
408eb482f1 remove --ignore-scripts for backend temporary 2023-01-02 11:26:27 -05:00
ccb1c31413 add set command for crud cli 2023-01-02 11:24:41 -05:00
a07d4e6dd1 update types name for secrets v2 api 2023-01-02 11:23:48 -05:00
72a9343a02 Fix merge conflicts 2023-01-02 22:51:25 +07:00
e99ee94a7b Modify service token format 2023-01-02 22:43:00 +07:00
4af839040e Patch actionNames on getWorkspacelogs 2023-01-02 15:43:21 +07:00
1c2a43ceea Clean unecessary imports 2023-01-02 15:24:28 +07:00
029443161f Modularize prepareDatabasse into initSecretVersioning 2023-01-02 14:52:08 +07:00
a8f0c391bc Finish v1 audit logs, secret versioning, version all unversioned secrets 2023-01-02 14:18:49 +07:00
0167342722 Improved frontend for activity logs 2023-01-01 18:27:31 -08:00
ac4b67d98e delete, get and create via cli 2023-01-01 19:22:09 -05:00
f2bd4aec39 Show full validation error 2023-01-01 13:13:03 -05:00
776b4c2922 update types for request body in secrets v2 api 2023-01-01 11:18:00 -05:00
939e9ba075 rename secret route with workspace and environment hierarchy 2023-01-01 10:39:23 -05:00
f015e6be6e Add batch delete api and batch create api 2023-01-01 02:12:24 -05:00
4576e8f6a7 Merge branch 'activity-logs' of https://github.com/Infisical/infisical into activity-logs 2023-01-01 11:18:58 +07:00
9c83808e2e Added populate statement 2022-12-31 20:17:40 -08:00
ce66e55c8e Merge remote-tracking branch 'origin' into activity-logs 2023-01-01 10:55:39 +07:00
0aff94cfb3 Add action error 2023-01-01 10:55:23 +07:00
4dac65eb8a Begin action route for getting an action by id 2023-01-01 10:54:23 +07:00
3c349b1e28 Merge remote-tracking branch 'origin' into activity-logs 2023-01-01 10:36:31 +07:00
6f054d8f2c Add requireSecretAuth middleware 2023-01-01 10:36:07 +07:00
b8a64714d2 Refactor auth middleware to accept multiple auth modes 2023-01-01 09:24:20 +07:00
3c6b1e51b5 Add non try catch error handle and fix bulk patch 2022-12-31 20:43:49 -05:00
7e4bf7f44b Continue developing service token v2 2023-01-01 07:10:47 +07:00
a5e8741442 update json5 2022-12-31 17:57:07 -05:00
60445727e9 merge with own change 2022-12-31 17:48:56 -05:00
618dc10e45 Added .NET to available frameworks 2022-12-31 01:11:13 -05:00
01d969190b Begin service token data refactor 2022-12-30 23:57:21 +03:00
6fa84bf0cb Fix merge conflicts 2022-12-28 13:42:27 -05:00
fc61849120 Added .NET to available frameworks 2022-12-28 12:02:42 -05:00
b062c44742 Merge pull request from jon4hz/dotnet
docs: add dotnet example
2022-12-28 11:34:13 -05:00
9ea12b93f7 Merge pull request from jon4hz/completion
ci: add autocompletion and manpage
2022-12-28 11:14:02 -05:00
c409a89e93 docs: add dotnet example 2022-12-28 16:53:28 +01:00
a36a59a4c0 Merge pull request from Infisical/snyk-upgrade-1e6565738ff7cb9c1330ef01a3b0c7f1
[Snyk] Upgrade next from 12.3.1 to 12.3.4
2022-12-27 21:53:36 -05:00
b8e5a2c5c2 Merge branch 'main' into snyk-upgrade-1e6565738ff7cb9c1330ef01a3b0c7f1 2022-12-27 21:51:56 -05:00
e664a8b307 Added resuest for translations to README.md 2022-12-27 21:48:38 -05:00
146d683e75 Added translations to integrations 2022-12-27 21:22:12 -05:00
2032318491 Added translations to the sidebar 2022-12-27 20:56:16 -05:00
d4925e090d Fixed merge conflicts with translations; updated starting secrets 2022-12-27 19:12:38 -05:00
c9dc0243b6 Merge pull request from gangjun06/feat/39
Localize Web UI ()
2022-12-27 18:37:12 -05:00
cc2803acee Fixed the merge conflicts on the signup page 2022-12-27 18:32:12 -05:00
c9d71ad887 Fixing react hook error 2022-12-27 18:24:27 -05:00
1459370458 Fixing more merge conflicts 2022-12-27 18:22:48 -05:00
cbb99844f1 Fixing merge conflicts for pages/users 2022-12-27 18:19:56 -05:00
f6faad267c Returned the missing package 2022-12-27 18:11:00 -05:00
74d883c15a Fixing merge conflicts 2022-12-27 17:57:14 -05:00
876c5f51c2 Fixed the bugs with secret overrides and the sidebar 2022-12-27 16:46:15 -05:00
4c43bdac93 Merge pull request from akhilmhdh/fix/failed-workspace-membership-invite
fix(backend): resolved workspace membership invite failure
2022-12-27 16:13:09 -05:00
bb4d3ba581 Connected version history to backend 2022-12-27 15:02:50 -05:00
2c63559303 Updated request new invite illustration 2022-12-27 14:07:30 -05:00
c653f807f4 fix(api-frontend): resolved failure in inviting existing infisical users to organization 2022-12-27 23:22:47 +05:30
c28d857086 fix(backend): resolved workspace membership invite failure 2022-12-27 23:22:47 +05:30
babf35b44e Added translations to /noprojects route 2022-12-27 12:32:07 -05:00
16f240596a Add audit logs to pulls, still need to refactor 2022-12-27 12:30:33 -05:00
9497a26eb2 Add v1 audit log backend models and wiring to push secrets 2022-12-27 12:12:39 -05:00
cc251ba8ae Fixed the home directory transations 2022-12-27 11:52:23 -05:00
752a2a9085 Update README.md 2022-12-27 09:48:00 -05:00
019e90dc77 Fix merge conflicts 2022-12-27 09:34:07 -05:00
76da449463 fix(frontend): provided href invalid error 2022-12-27 15:59:11 +09:00
f550e4bc87 Merge pull request from Infisical/new-routing
Migrated `POST /v1/secret/:workspaceId` to `POST /v2/workspace/:workspaceId/secrets`
2022-12-26 23:02:40 -05:00
f93594b62f Migrate POST /v1/secret/:workspaceId to /v2/workspace/:workspaceId/secrets and cleared room for /v2 secret routes 2022-12-26 22:50:59 -05:00
924e3d78a3 Merge remote-tracking branch 'origin' into new-routing 2022-12-26 21:45:45 -05:00
07c34c490f Begin moving /secret/workspaceId routes to /workspace/workspaceId 2022-12-26 21:45:26 -05:00
f3e3a9edf1 Added commenting functionality () 2022-12-26 21:28:55 -05:00
ab3f3600e5 Merge pull request from Infisical/new-routing
Create route and controller v1/v2 folder structure separation
2022-12-26 21:07:46 -05:00
229fef8874 Create route and controller v1/v2 folder structure separation 2022-12-26 21:02:39 -05:00
91dbbee9db install docs for arch linux 2022-12-26 20:34:05 -05:00
9ef6f9e554 ci: add autocompletion and manpage 2022-12-27 01:35:03 +01:00
addf04d54d Merge remote-tracking branch 'origin' into service-token-v2 2022-12-26 18:50:55 -05:00
cfea0dc66f chore(frontend): add useEffect to _app for translate 2022-12-26 22:41:56 +09:00
991e4b7bc6 chore(frontend): update some files about translate 2022-12-26 22:14:42 +09:00
5b8337ac41 Merge remote-tracking branch 'upstream/main' into feat/39 2022-12-26 22:11:09 +09:00
bd97e9ebef fix(frontend): navigate not working 2022-12-26 20:33:48 +09:00
888d28d6b9 Continue work on API key 2022-12-25 19:19:56 -05:00
d869968f88 Begin api-key functionality on backend 2022-12-25 12:45:43 -05:00
7e4454b2c7 fix: upgrade next from 12.3.1 to 12.3.4
Snyk has created this PR to upgrade next from 12.3.1 to 12.3.4.

See this package in npm:
https://www.npmjs.com/package/next

See this project in Snyk:
https://app.snyk.io/org/maidul98/project/53d4ecb6-6cc1-4918-aa73-bf9cae4ffd13?utm_source=github&utm_medium=referral&page=upgrade-pr
2022-12-23 21:45:05 +00:00
009f9c6842 Continue developing activity logs backend 2022-12-21 16:27:04 -05:00
6e50adb9ff Fix merge conflicts 2022-12-21 11:36:47 -05:00
72664c5bb3 Merge branch 'activity-logs' of https://github.com/Infisical/infisical into activity-logs 2022-12-21 11:10:11 -05:00
7d280d4e30 Added event filter for logs 2022-12-18 21:53:21 -05:00
648e3e3bbf Continue developing log schema 2022-12-18 17:19:09 -05:00
9d41f753f4 Added Intercom to Docs 2022-12-17 21:43:13 -05:00
939826f28c Merge branch 'logging' into activity-logs 2022-12-17 20:22:28 -05:00
fae27a0b6e Changed text for the activity page 2022-12-17 20:21:25 -05:00
2e84b7e354 Initial schema ideas for logging 2022-12-17 15:10:30 -05:00
9218d2a653 Fixed the padding issue in the login page 2022-12-17 08:48:10 -05:00
4ad4efe9a5 Added a basic framework for activity logs 2022-12-15 23:35:52 -05:00
0e53b78708 feat(frontend): update translate library 2022-12-11 10:54:53 +09:00
96ebe3e3d2 feat(frontend): setting next-i18next 2022-12-09 09:20:37 +09:00
d516b295bf chore(frontend): add next-i18next library 2022-12-09 09:10:14 +09:00
5910bfbb4d Merge remote-tracking branch 'upstream/main' into feat/39 2022-12-07 20:45:49 +09:00
242f7b80e7 Merge remote-tracking branch 'upstream/main' into feat/39 2022-12-06 22:07:29 +09:00
a0abf5339f translate(frontend): many files 2022-12-06 21:59:53 +09:00
4c94ddd1b2 feat(frontend): add change language button in login page 2022-12-02 21:14:28 +09:00
6cebe171d9 feat(frontend): add change language button in personal setting 2022-12-02 21:09:51 +09:00
9685af21f3 translate(frontend): update namespace names, move translate keys, translate some keys into korean 2022-12-02 20:52:26 +09:00
914a78fb15 Merge remote-tracking branch 'upstream/main' into feat/39 2022-11-28 12:24:20 +09:00
2c1398e71c translate(frontend): some setting page 2022-11-27 15:47:02 +09:00
14bffebc55 feat(frontend): update i18n default loader 2022-11-27 14:58:55 +09:00
c14d1d4fcc translate(frontend): dashboard page 2022-11-27 14:18:17 +09:00
20e5100bc4 translate(frontend): update namespaces 2022-11-27 13:56:18 +09:00
4bdb48d8f6 translate(frontend): navbar 2022-11-27 13:09:59 +09:00
8dfcc1f505 translate(frontend): add login, signup page translate 2022-11-27 12:21:23 +09:00
1b0e5d3b29 feat(frontend): setup next-translate package 2022-11-27 00:03:14 +09:00
268 changed files with 11905 additions and 4426 deletions
.github/workflows
.gitignore.goreleaser.yamlREADME.md
backend
Dockerfilepackage-lock.jsonpackage.json
src
app.ts
config
controllers
ee
events
helpers
index.ts
integrations
middleware
models
routes
services
types
utils
variables
tsconfig.json
cli
docker-compose.dev.yml
docs
frontend
.gitignore
components
const.js
ee
next-i18next.config.jsnext.config.jspackage-lock.jsonpackage.json
pages
public
scripts
tailwind.config.js
i18n

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

2
.gitignore vendored

@ -12,6 +12,8 @@ node_modules
.DS_Store
/dist
/completions/
/manpages/
# frontend

@ -6,6 +6,11 @@
# - cd cli && go mod tidy
# # you may remove this if you don't need go generate
# - cd cli && go generate ./...
before:
hooks:
- ./cli/scripts/completions.sh
- ./cli/scripts/manpages.sh
builds:
- id: darwin-build
binary: infisical
@ -44,6 +49,16 @@ builds:
goarch: "386"
dir: ./cli
archives:
- format_overrides:
- goos: windows
format: zip
files:
- README*
- LICENSE*
- manpages/*
- completions/*
release:
replace_existing_draft: true
mode: 'replace'
@ -92,6 +107,15 @@ nfpms:
- apk
- archlinux
bindir: /usr/bin
contents:
- src: ./completions/infisical.bash
dst: /etc/bash_completion.d/infisical
- src: ./completions/infisical.fish
dst: /usr/share/fish/vendor_completions.d/infisical.fish
- src: ./completions/infisical.zsh
dst: /usr/share/zsh/site-functions/_infisical
- src: ./manpages/infisical.1.gz
dst: /usr/share/man/man1/infisical.1.gz
scoop:
bucket:
owner: Infisical
@ -117,7 +141,15 @@ aurs:
install -Dm755 "./infisical" "${pkgdir}/usr/bin/infisical"
# license
install -Dm644 "./LICENSE" "${pkgdir}/usr/share/licenses/infisical/LICENSE"
# completions
mkdir -p "${pkgdir}/usr/share/bash-completion/completions/"
mkdir -p "${pkgdir}/usr/share/zsh/site-functions/"
mkdir -p "${pkgdir}/usr/share/fish/vendor_completions.d/"
install -Dm644 "./completions/infisical.bash" "${pkgdir}/usr/share/bash-completion/completions/infisical"
install -Dm644 "./completions/infisical.zsh" "${pkgdir}/usr/share/zsh/site-functions/infisical"
install -Dm644 "./completions/infisical.fish" "${pkgdir}/usr/share/fish/vendor_completions.d/infisical.fish"
# man pages
install -Dm644 "./manpages/infisical.1.gz" "${pkgdir}/usr/share/man/man1/infisical.1.gz"
# dockers:
# - dockerfile: goreleaser.dockerfile
# goos: linux

@ -41,7 +41,7 @@
- **[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.)
- **Personal/Shared** scoping for environment variables
- **[Integrations](https://infisical.com/docs/integrations/overview)** with CI/CD and production infrastructure (Heroku available, more coming soon)
- **[Integrations](https://infisical.com/docs/integrations/overview)** with CI/CD and production infrastructure
- 🔜 **1-Click Deploy** to Digital Ocean and Heroku
- 🔜 **Authentication/Authorization** for projects (read/write controls soon)
- 🔜 **Automatic Secret Rotation**
@ -270,13 +270,13 @@ We're currently setting the foundation and building [integrations](https://infis
</tr>
<tr>
<td align="left" valign="middle">
<a href="https://infisical.com/docs/integrations/frameworks/rails?ref=github.com">
✔️ Ruby on Rails
<a href="https://infisical.com/docs/integrations/frameworks/vue?ref=github.com">
✔️ Vue
</a>
</td>
<td align="left" valign="middle">
<a href="https://infisical.com/docs/integrations/frameworks/vue?ref=github.com">
✔️ Vue
<a href="https://infisical.com/docs/integrations/frameworks/rails?ref=github.com">
✔️ Ruby on Rails
</a>
</td>
</tr>
@ -292,6 +292,16 @@ We're currently setting the foundation and building [integrations](https://infis
</a>
</td>
</tr>
<tr>
<td align="left" valign="middle">
<a href="https://infisical.com/docs/integrations/frameworks/dotnet?ref=github.com">
✔️ .NET
</a>
</td>
<td align="left" valign="middle">
And more...
</td>
</tr>
</tbody>
</table>
@ -321,4 +331,10 @@ Infisical officially launched as v.1.0 on November 21st, 2022. However, a lot of
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<a href="https://github.com/dangtony98"><img src="https://avatars.githubusercontent.com/u/25857006?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/mv-turtle"><img src="https://avatars.githubusercontent.com/u/78047717?s=96&v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/maidul98"><img src="https://avatars.githubusercontent.com/u/9300960?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gangjun06"><img src="https://avatars.githubusercontent.com/u/50910815?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/reginaldbondoc"><img src="https://avatars.githubusercontent.com/u/7693108?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/SH5H"><img src="https://avatars.githubusercontent.com/u/25437192?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gmgale"><img src="https://avatars.githubusercontent.com/u/62303146?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/asharonbaltazar"><img src="https://avatars.githubusercontent.com/u/58940073?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/edgarrmondragon"><img src="https://avatars.githubusercontent.com/u/16805946?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arjunyel"><img src="https://avatars.githubusercontent.com/u/11153289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/LemmyMwaura"><img src="https://avatars.githubusercontent.com/u/20738858?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/Zamion101"><img src="https://avatars.githubusercontent.com/u/8071263?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/akhilmhdh"><img src="https://avatars.githubusercontent.com/u/31166322?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/naorpeled"><img src="https://avatars.githubusercontent.com/u/6171622?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/jonerrr"><img src="https://avatars.githubusercontent.com/u/73760377?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/adrianmarinwork"><img src="https://avatars.githubusercontent.com/u/118568289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arthurzenika"><img src="https://avatars.githubusercontent.com/u/445200?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/hanywang2"><img src="https://avatars.githubusercontent.com/u/44352119?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/tobias-mintlify"><img src="https://avatars.githubusercontent.com/u/110702161?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wjhurley"><img src="https://avatars.githubusercontent.com/u/15939055?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/0xflotus"><img src="https://avatars.githubusercontent.com/u/26602940?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wanjohiryan"><img src="https://avatars.githubusercontent.com/u/71614375?v=4" width="50" height="50" alt=""/></a>
<a href="https://github.com/dangtony98"><img src="https://avatars.githubusercontent.com/u/25857006?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/mv-turtle"><img src="https://avatars.githubusercontent.com/u/78047717?s=96&v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/maidul98"><img src="https://avatars.githubusercontent.com/u/9300960?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gangjun06"><img src="https://avatars.githubusercontent.com/u/50910815?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/reginaldbondoc"><img src="https://avatars.githubusercontent.com/u/7693108?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/SH5H"><img src="https://avatars.githubusercontent.com/u/25437192?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gmgale"><img src="https://avatars.githubusercontent.com/u/62303146?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/asharonbaltazar"><img src="https://avatars.githubusercontent.com/u/58940073?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/jon4hz"><img src="https://avatars.githubusercontent.com/u/26183582?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/edgarrmondragon"><img src="https://avatars.githubusercontent.com/u/16805946?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arjunyel"><img src="https://avatars.githubusercontent.com/u/11153289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/LemmyMwaura"><img src="https://avatars.githubusercontent.com/u/20738858?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/Zamion101"><img src="https://avatars.githubusercontent.com/u/8071263?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/akhilmhdh"><img src="https://avatars.githubusercontent.com/u/31166322?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/naorpeled"><img src="https://avatars.githubusercontent.com/u/6171622?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/jonerrr"><img src="https://avatars.githubusercontent.com/u/73760377?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/adrianmarinwork"><img src="https://avatars.githubusercontent.com/u/118568289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arthurzenika"><img src="https://avatars.githubusercontent.com/u/445200?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/hanywang2"><img src="https://avatars.githubusercontent.com/u/44352119?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/tobias-mintlify"><img src="https://avatars.githubusercontent.com/u/110702161?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wjhurley"><img src="https://avatars.githubusercontent.com/u/15939055?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/0xflotus"><img src="https://avatars.githubusercontent.com/u/26602940?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wanjohiryan"><img src="https://avatars.githubusercontent.com/u/71614375?v=4" width="50" height="50" alt=""/></a>
## 🌎 Translations
Infisical is currently aviable in English and Korean. Help us translate Infisical to your language!
You can find all the info in [this issue](https://github.com/Infisical/infisical/issues/181).

@ -4,7 +4,10 @@ WORKDIR /app
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 . .

@ -15,7 +15,9 @@
"@sentry/tracing": "^7.19.0",
"@types/crypto-js": "^4.1.1",
"@types/libsodium-wrappers": "^0.7.10",
"await-to-js": "^3.0.0",
"axios": "^1.1.3",
"bcrypt": "^5.1.0",
"bigint-conversion": "^2.2.2",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
@ -38,12 +40,15 @@
"tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1",
"typescript": "^4.9.3",
"utility-types": "^3.10.0",
"winston": "^3.8.2",
"winston-loki": "^6.0.6"
},
"devDependencies": {
"@jest/globals": "^29.3.1",
"@posthog/plugin-scaffold": "^1.3.4",
"@types/bcrypt": "^5.0.0",
"@types/bcryptjs": "^2.4.2",
"@types/cookie-parser": "^1.4.3",
"@types/cors": "^2.8.12",
"@types/express": "^4.17.14",
@ -2576,6 +2581,39 @@
"resolved": "https://registry.npmjs.org/@juanelas/base64/-/base64-1.0.5.tgz",
"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": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/@maxmind/geoip2-node/-/geoip2-node-3.5.0.tgz",
@ -3033,6 +3071,21 @@
"@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": {
"version": "1.19.2",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz",
@ -3483,8 +3536,7 @@
"node_modules/abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
"dev": true
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
},
"node_modules/accepts": {
"version": "1.3.8",
@ -3586,7 +3638,6 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"engines": {
"node": ">=8"
}
@ -3619,6 +3670,23 @@
"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": {
"version": "4.1.3",
"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",
"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": {
"version": "1.1.3",
"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": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz",
@ -4132,6 +4221,14 @@
"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": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.5.0.tgz",
@ -4218,6 +4315,14 @@
"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": {
"version": "1.9.3",
"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",
"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": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@ -4452,6 +4562,11 @@
"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": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
@ -4482,6 +4597,14 @@
"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": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
@ -4585,8 +4708,7 @@
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"node_modules/enabled": {
"version": "2.0.0",
@ -4601,6 +4723,29 @@
"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": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
@ -5259,6 +5404,28 @@
"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": {
"version": "1.0.0",
"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",
"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": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@ -5476,6 +5662,11 @@
"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": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz",
@ -5726,7 +5917,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true,
"engines": {
"node": ">=8"
}
@ -6497,9 +6687,9 @@
"dev": true
},
"node_modules/json5": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true,
"bin": {
"json5": "lib/cli.js"
@ -6696,7 +6886,6 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
"dev": true,
"dependencies": {
"semver": "^6.0.0"
},
@ -6711,7 +6900,6 @@
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true,
"bin": {
"semver": "bin/semver.js"
}
@ -6880,11 +7068,44 @@
"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": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"dev": true,
"bin": {
"mkdirp": "bin/cmd.js"
},
@ -7004,6 +7225,11 @@
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
"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": {
"version": "2.6.7",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
@ -9731,6 +9957,17 @@
"inBundle": true,
"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": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@ -10514,6 +10751,11 @@
"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": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@ -10573,8 +10815,7 @@
"node_modules/signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"dev": true
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
},
"node_modules/simple-swizzle": {
"version": "0.2.2",
@ -10795,7 +11036,6 @@
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
@ -10809,7 +11049,6 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
@ -10935,6 +11174,22 @@
"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": {
"version": "6.0.0",
"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",
"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": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
@ -11403,6 +11666,14 @@
"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": {
"version": "3.8.2",
"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",
"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": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/@maxmind/geoip2-node/-/geoip2-node-3.5.0.tgz",
@ -14152,6 +14449,21 @@
"@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": {
"version": "1.19.2",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz",
@ -14513,8 +14825,7 @@
"abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
"dev": true
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
},
"accepts": {
"version": "1.3.8",
@ -14584,8 +14895,7 @@
"ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
},
"ansi-styles": {
"version": "4.3.0",
@ -14606,6 +14916,20 @@
"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": {
"version": "4.1.3",
"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",
"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": {
"version": "1.1.3",
"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",
"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": {
"version": "2.2.3",
"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": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.5.0.tgz",
@ -15064,6 +15407,11 @@
"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": {
"version": "1.1.4",
"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",
"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": {
"version": "0.5.4",
"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",
"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": {
"version": "2.1.0",
"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",
"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": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
@ -15336,8 +15699,7 @@
"emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"enabled": {
"version": "2.0.0",
@ -15349,6 +15711,28 @@
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"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": {
"version": "1.3.2",
"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",
"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": {
"version": "1.0.0",
"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",
"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": {
"version": "1.0.0-beta.2",
"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",
"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": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz",
@ -16181,8 +16604,7 @@
"is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
},
"is-generator-fn": {
"version": "2.1.0",
@ -16776,9 +17198,9 @@
"dev": true
},
"json5": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true
},
"jsonwebtoken": {
@ -16944,7 +17366,6 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
"dev": true,
"requires": {
"semver": "^6.0.0"
},
@ -16952,8 +17373,7 @@
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
}
}
},
@ -17078,11 +17498,37 @@
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz",
"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": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"dev": true
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
},
"mmdb-lib": {
"version": "2.0.2",
@ -17173,6 +17619,11 @@
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
"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": {
"version": "2.6.7",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
@ -19082,6 +19533,17 @@
"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": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@ -19637,6 +20099,11 @@
"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": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@ -19684,8 +20151,7 @@
"signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"dev": true
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
},
"simple-swizzle": {
"version": "0.2.2",
@ -19860,7 +20326,6 @@
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"requires": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
@ -19871,7 +20336,6 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"requires": {
"ansi-regex": "^5.0.1"
}
@ -19960,6 +20424,19 @@
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"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": {
"version": "6.0.0",
"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",
"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": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
@ -20277,6 +20759,14 @@
"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": {
"version": "3.8.2",
"resolved": "https://registry.npmjs.org/winston/-/winston-3.8.2.tgz",

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

@ -13,28 +13,35 @@ import { apiLimiter } from './helpers/rateLimiter';
import {
workspace as eeWorkspaceRouter,
secret as eeSecretRouter
} from './ee/routes';
secret as eeSecretRouter,
secretSnapshot as eeSecretSnapshotRouter,
action as eeActionRouter
} from './ee/routes/v1';
import {
signup as signupRouter,
auth as authRouter,
bot as botRouter,
organization as organizationRouter,
workspace as workspaceRouter,
membershipOrg as membershipOrgRouter,
membership as membershipRouter,
key as keyRouter,
inviteOrg as inviteOrgRouter,
user as userRouter,
userAction as userActionRouter,
secret as secretRouter,
serviceToken as serviceTokenRouter,
password as passwordRouter,
stripe as stripeRouter,
integration as integrationRouter,
integrationAuth as integrationAuthRouter
} from './routes';
signup as v1SignupRouter,
auth as v1AuthRouter,
bot as v1BotRouter,
organization as v1OrganizationRouter,
workspace as v1WorkspaceRouter,
membershipOrg as v1MembershipOrgRouter,
membership as v1MembershipRouter,
key as v1KeyRouter,
inviteOrg as v1InviteOrgRouter,
user as v1UserRouter,
userAction as v1UserActionRouter,
secret as v1SecretRouter,
serviceToken as v1ServiceTokenRouter,
password as v1PasswordRouter,
stripe as v1StripeRouter,
integration as v1IntegrationRouter,
integrationAuth as v1IntegrationAuthRouter
} from './routes/v1';
import {
secret as v2SecretRouter,
workspace as v2WorkspaceRouter,
serviceTokenData as v2ServiceTokenDataRouter,
apiKeyData as v2APIKeyDataRouter,
} from './routes/v2';
import { getLogger } from './utils/logger';
import { RouteNotFoundError } from './utils/errors';
@ -63,34 +70,41 @@ if (NODE_ENV === 'production') {
app.use(helmet());
}
// /ee routers
// (EE) routes
app.use('/api/v1/secret', eeSecretRouter);
app.use('/api/v1/secret-snapshot', eeSecretSnapshotRouter);
app.use('/api/v1/workspace', eeWorkspaceRouter);
app.use('/api/v1/action', eeActionRouter);
// routers
app.use('/api/v1/signup', signupRouter);
app.use('/api/v1/auth', authRouter);
app.use('/api/v1/bot', botRouter);
app.use('/api/v1/user', userRouter);
app.use('/api/v1/user-action', userActionRouter);
app.use('/api/v1/organization', organizationRouter);
app.use('/api/v1/workspace', workspaceRouter);
app.use('/api/v1/membership-org', membershipOrgRouter);
app.use('/api/v1/membership', membershipRouter);
app.use('/api/v1/key', keyRouter);
app.use('/api/v1/invite-org', inviteOrgRouter);
app.use('/api/v1/secret', secretRouter);
app.use('/api/v1/service-token', serviceTokenRouter);
app.use('/api/v1/password', passwordRouter);
app.use('/api/v1/stripe', stripeRouter);
app.use('/api/v1/integration', integrationRouter);
app.use('/api/v1/integration-auth', integrationAuthRouter);
// v1 routes
app.use('/api/v1/signup', v1SignupRouter);
app.use('/api/v1/auth', v1AuthRouter);
app.use('/api/v1/bot', v1BotRouter);
app.use('/api/v1/user', v1UserRouter);
app.use('/api/v1/user-action', v1UserActionRouter);
app.use('/api/v1/organization', v1OrganizationRouter);
app.use('/api/v1/workspace', v1WorkspaceRouter);
app.use('/api/v1/membership-org', v1MembershipOrgRouter);
app.use('/api/v1/membership', v1MembershipRouter);
app.use('/api/v1/key', v1KeyRouter);
app.use('/api/v1/invite-org', v1InviteOrgRouter);
app.use('/api/v1/secret', v1SecretRouter);
app.use('/api/v1/service-token', v1ServiceTokenRouter); // deprecate
app.use('/api/v1/password', v1PasswordRouter);
app.use('/api/v1/stripe', v1StripeRouter);
app.use('/api/v1/integration', v1IntegrationRouter);
app.use('/api/v1/integration-auth', v1IntegrationAuthRouter);
// v2 routes
app.use('/api/v2/workspace', v2WorkspaceRouter);
app.use('/api/v2/secret', v2SecretRouter);
app.use('/api/v2/service-token', v2ServiceTokenDataRouter);
app.use('/api/v2/api-key-data', v2APIKeyDataRouter);
//* Handle unrouted requests and respond with proper error message as well as status code
app.use((req, res, next)=>{
if(res.headersSent) return next();
next(RouteNotFoundError({message: `The requested source '(${req.method})${req.url}' was not found`}))
app.use((req, res, next) => {
if (res.headersSent) return next();
next(RouteNotFoundError({ message: `The requested source '(${req.method})${req.url}' was not found` }))
})
//* Error Handling Middleware (must be after all routing logic)

@ -1,6 +1,7 @@
const PORT = process.env.PORT || 4000;
const EMAIL_TOKEN_LIFETIME = process.env.EMAIL_TOKEN_LIFETIME! || '86400';
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_SECRET = process.env.JWT_AUTH_SECRET!;
const JWT_REFRESH_LIFETIME = process.env.JWT_REFRESH_LIFETIME! || '90d';
@ -47,6 +48,7 @@ export {
PORT,
EMAIL_TOKEN_LIFETIME,
ENCRYPTION_KEY,
SALT_ROUNDS,
JWT_AUTH_LIFETIME,
JWT_AUTH_SECRET,
JWT_REFRESH_LIFETIME,

@ -4,14 +4,14 @@ import jwt from 'jsonwebtoken';
import * as Sentry from '@sentry/node';
import * as bigintConversion from 'bigint-conversion';
const jsrp = require('jsrp');
import { User } from '../models';
import { createToken, issueTokens, clearTokens } from '../helpers/auth';
import { User } from '../../models';
import { createToken, issueTokens, clearTokens } from '../../helpers/auth';
import {
NODE_ENV,
JWT_AUTH_LIFETIME,
JWT_AUTH_SECRET,
JWT_REFRESH_SECRET
} from '../config';
} from '../../config';
declare module 'jsonwebtoken' {
export interface UserIDJwtPayload extends jwt.JwtPayload {

@ -1,7 +1,7 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Bot, BotKey } from '../models';
import { createBot } from '../helpers/bot';
import { Bot, BotKey } from '../../models';
import { createBot } from '../../helpers/bot';
interface BotKey {
encryptedKey: string;

@ -2,10 +2,10 @@ import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import axios from 'axios';
import { readFileSync } from 'fs';
import { IntegrationAuth, Integration } from '../models';
import { INTEGRATION_SET, INTEGRATION_OPTIONS, ENV_DEV } from '../variables';
import { IntegrationService } from '../services';
import { getApps, revokeAccess } from '../integrations';
import { IntegrationAuth, Integration } from '../../models';
import { INTEGRATION_SET, INTEGRATION_OPTIONS, ENV_DEV } from '../../variables';
import { IntegrationService } from '../../services';
import { getApps, revokeAccess } from '../../integrations';
export const getIntegrationOptions = async (
req: Request,

@ -1,9 +1,9 @@
import { Request, Response } from 'express';
import { readFileSync } from 'fs';
import * as Sentry from '@sentry/node';
import { Integration, Bot, BotKey } from '../models';
import { EventService } from '../services';
import { eventPushSecrets } from '../events';
import { Integration, Bot, BotKey } from '../../models';
import { EventService } from '../../services';
import { eventPushSecrets } from '../../events';
interface Key {
encryptedKey: string;

@ -1,8 +1,7 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Key } from '../models';
import { findMembership } from '../helpers/membership';
import { GRANTED } from '../variables';
import { Key } from '../../models';
import { findMembership } from '../../helpers/membership';
/**
* 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');
}
receiverMembership.status = GRANTED;
await receiverMembership.save();
await new Key({
encryptedKey: key.encryptedKey,
nonce: key.nonce,

@ -1,13 +1,13 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Membership, MembershipOrg, User, Key } from '../models';
import { Membership, MembershipOrg, User, Key } from '../../models';
import {
findMembership,
deleteMembership as deleteMember
} from '../helpers/membership';
import { sendMail } from '../helpers/nodemailer';
import { SITE_URL } from '../config';
import { ADMIN, MEMBER, GRANTED, ACCEPTED } from '../variables';
} from '../../helpers/membership';
import { sendMail } from '../../helpers/nodemailer';
import { SITE_URL } from '../../config';
import { ADMIN, MEMBER, ACCEPTED } from '../../variables';
/**
* 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
const inviteeMembership = await Membership.findOne({
user: invitee._id,
workspace: workspaceId,
status: GRANTED
workspace: workspaceId
});
if (inviteeMembership)
@ -205,8 +204,7 @@ export const inviteUserToWorkspace = async (req: Request, res: Response) => {
const m = await new Membership({
user: invitee._id,
workspace: workspaceId,
role: MEMBER,
status: GRANTED
role: MEMBER
}).save();
await sendMail({

@ -1,14 +1,14 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import crypto from 'crypto';
import { SITE_URL, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../config';
import { MembershipOrg, Organization, User, Token } from '../models';
import { deleteMembershipOrg as deleteMemberFromOrg } from '../helpers/membershipOrg';
import { checkEmailVerification } from '../helpers/signup';
import { createToken } from '../helpers/auth';
import { updateSubscriptionOrgQuantity } from '../helpers/organization';
import { sendMail } from '../helpers/nodemailer';
import { OWNER, ADMIN, MEMBER, ACCEPTED, INVITED } from '../variables';
import { SITE_URL, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../../config';
import { MembershipOrg, Organization, User, Token } from '../../models';
import { deleteMembershipOrg as deleteMemberFromOrg } from '../../helpers/membershipOrg';
import { checkEmailVerification } from '../../helpers/signup';
import { createToken } from '../../helpers/auth';
import { updateSubscriptionOrgQuantity } from '../../helpers/organization';
import { sendMail } from '../../helpers/nodemailer';
import { OWNER, ADMIN, MEMBER, ACCEPTED, INVITED } from '../../variables';
/**
* Delete organization membership with id [membershipOrgId] from organization
@ -80,14 +80,14 @@ export const changeMembershipOrgRole = async (req: Request, res: Response) => {
// TODO
let membershipToChangeRole;
try {
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to change organization membership role'
});
}
// try {
// } catch (err) {
// Sentry.setUser({ email: req.user.email });
// Sentry.captureException(err);
// return res.status(400).send({
// message: 'Failed to change organization membership role'
// });
// }
return res.status(200).send({
membershipOrg: membershipToChangeRole
@ -218,12 +218,6 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => {
const { email, code } = req.body;
user = await User.findOne({ email }).select('+publicKey');
if (user && user?.publicKey) {
// case: user has already completed account
return res.status(403).send({
error: 'Failed email magic link verification for complete account'
});
}
const membershipOrg = await MembershipOrg.findOne({
inviteEmail: email,
@ -238,6 +232,18 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => {
code
});
if (user && user?.publicKey) {
// case: user has already completed account
// membership can be approved and redirected to login/dashboard
membershipOrg.status = ACCEPTED;
await membershipOrg.save();
return res.status(200).send({
message: 'Successfully verified email',
user,
});
}
if (!user) {
// initialize user account
user = await new User({

@ -6,7 +6,7 @@ import {
STRIPE_PRODUCT_STARTER,
STRIPE_PRODUCT_PRO,
STRIPE_PRODUCT_CARD_AUTH
} from '../config';
} from '../../config';
import Stripe from 'stripe';
const stripe = new Stripe(STRIPE_SECRET_KEY, {
@ -18,10 +18,10 @@ import {
Organization,
Workspace,
IncidentContactOrg
} from '../models';
import { createOrganization as create } from '../helpers/organization';
import { addMembershipsOrg } from '../helpers/membershipOrg';
import { OWNER, ACCEPTED } from '../variables';
} from '../../models';
import { createOrganization as create } from '../../helpers/organization';
import { addMembershipsOrg } from '../../helpers/membershipOrg';
import { OWNER, ACCEPTED } from '../../variables';
const productToPriceMap = {
starter: STRIPE_PRODUCT_STARTER,

@ -1,13 +1,14 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import crypto from 'crypto';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const jsrp = require('jsrp');
import * as bigintConversion from 'bigint-conversion';
import { User, Token, BackupPrivateKey } from '../models';
import { checkEmailVerification } from '../helpers/signup';
import { createToken } from '../helpers/auth';
import { sendMail } from '../helpers/nodemailer';
import { JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, SITE_URL } from '../config';
import { User, Token, BackupPrivateKey } from '../../models';
import { checkEmailVerification } from '../../helpers/signup';
import { createToken } from '../../helpers/auth';
import { sendMail } from '../../helpers/nodemailer';
import { JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, SITE_URL } from '../../config';
const clientPublicKeys: any = {};

@ -1,16 +1,16 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Key, Secret } from '../models';
import { Key, Secret } from '../../models';
import {
pushSecrets as push,
v1PushSecrets as push,
pullSecrets as pull,
reformatPullSecrets
} from '../helpers/secret';
import { pushKeys } from '../helpers/key';
import { eventPushSecrets } from '../events';
import { EventService } from '../services';
import { ENV_SET } from '../variables';
import { postHogClient } from '../services';
} from '../../helpers/secret';
import { pushKeys } from '../../helpers/key';
import { eventPushSecrets } from '../../events';
import { EventService } from '../../services';
import { ENV_SET } from '../../variables';
import { postHogClient } from '../../services';
interface PushSecret {
ciphertextKey: string;
@ -21,6 +21,10 @@ interface PushSecret {
ivValue: string;
tagValue: string;
hashValue: string;
ciphertextComment: string;
ivComment: string;
tagComment: string;
hashComment: string;
type: 'shared' | 'personal';
}
@ -119,7 +123,9 @@ export const pullSecrets = async (req: Request, res: Response) => {
secrets = await pull({
userId: req.user._id.toString(),
workspaceId,
environment
environment,
channel: channel ? channel : 'cli',
ipAddress: req.ip
});
key = await Key.findOne({
@ -184,7 +190,9 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => {
secrets = await pull({
userId: req.serviceToken.user._id.toString(),
workspaceId,
environment
environment,
channel: 'cli',
ipAddress: req.ip
});
key = {

@ -1,8 +1,8 @@
import { Request, Response } from 'express';
import { ServiceToken } from '../models';
import { createToken } from '../helpers/auth';
import { ENV_SET } from '../variables';
import { JWT_SERVICE_SECRET } from '../config';
import { ServiceToken } from '../../models';
import { createToken } from '../../helpers/auth';
import { ENV_SET } from '../../variables';
import { JWT_SERVICE_SECRET } from '../../config';
/**
* Return service token on request
@ -11,7 +11,6 @@ import { JWT_SERVICE_SECRET } from '../config';
* @returns
*/
export const getServiceToken = async (req: Request, res: Response) => {
// get service token
return res.status(200).send({
serviceToken: req.serviceToken
});
@ -73,4 +72,4 @@ export const createServiceToken = async (req: Request, res: Response) => {
return res.status(200).send({
token
});
};
};

@ -1,15 +1,15 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { NODE_ENV, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../config';
import { User, MembershipOrg } from '../models';
import { completeAccount } from '../helpers/user';
import { NODE_ENV, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../../config';
import { User, MembershipOrg } from '../../models';
import { completeAccount } from '../../helpers/user';
import {
sendEmailVerification,
checkEmailVerification,
initializeDefaultOrg
} from '../helpers/signup';
import { issueTokens, createToken } from '../helpers/auth';
import { INVITED, ACCEPTED } from '../variables';
} from '../../helpers/signup';
import { issueTokens, createToken } from '../../helpers/auth';
import { INVITED, ACCEPTED } from '../../variables';
import axios from 'axios';
/**

@ -1,6 +1,6 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { UserAction } from '../models';
import { UserAction } from '../../models';
/**
* Add user action [action]

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

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

@ -0,0 +1,168 @@
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";
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 createSingleSecret = async (req: Request, res: Response) => {
try {
const secretFromDB = await Secret.findById(req.params.secretId)
return res.status(200).send(secretFromDB);
} catch (e) {
throw BadRequestError({ message: "Unable to find the requested secret" })
}
}
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 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 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)
}

@ -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
});
}

@ -0,0 +1,217 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import {
Workspace,
Membership,
MembershipOrg,
Integration,
IntegrationAuth,
Key,
IUser,
ServiceToken,
ServiceTokenData
} from '../../models';
import {
v2PushSecrets as push,
pullSecrets as pull,
reformatPullSecrets
} from '../../helpers/secret';
import { pushKeys } from '../../helpers/key';
import { postHogClient, EventService } from '../../services';
import { eventPushSecrets } from '../../events';
import { ENV_SET } from '../../variables';
interface V2PushSecret {
type: string; // personal or shared
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretKeyHash: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretValueHash: string;
secretCommentCiphertext?: string;
secretCommentIV?: string;
secretCommentTag?: string;
secretCommentHash?: string;
}
/**
* Upload (encrypted) secrets to workspace with id [workspaceId]
* for environment [environment]
* @param req
* @param res
* @returns
*/
export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
// upload (encrypted) secrets to workspace with id [workspaceId]
try {
let { secrets }: { secrets: V2PushSecret[] } = req.body;
const { keys, environment, channel } = req.body;
const { workspaceId } = req.params;
// validate environment
if (!ENV_SET.has(environment)) {
throw new Error('Failed to validate environment');
}
// sanitize secrets
secrets = secrets.filter(
(s: V2PushSecret) => s.secretKeyCiphertext !== '' && s.secretValueCiphertext !== ''
);
await push({
userId: req.user._id,
workspaceId,
environment,
secrets,
channel: channel ? channel : 'cli',
ipAddress: req.ip
});
await pushKeys({
userId: req.user._id,
workspaceId,
keys
});
if (postHogClient) {
postHogClient.capture({
event: 'secrets pushed',
distinctId: req.user.email,
properties: {
numberOfSecrets: secrets.length,
environment,
workspaceId,
channel: channel ? channel : 'cli'
}
});
}
// trigger event - push secrets
EventService.handleEvent({
event: eventPushSecrets({
workspaceId
})
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to upload workspace secrets'
});
}
return res.status(200).send({
message: 'Successfully uploaded workspace secrets'
});
};
/**
* Return (encrypted) secrets for workspace with id [workspaceId]
* for environment [environment]
* @param req
* @param res
* @returns
*/
export const pullSecrets = async (req: Request, res: Response) => {
let secrets;
try {
const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string;
const { workspaceId } = req.params;
let userId;
if (req.user) {
userId = req.user._id.toString();
} else if (req.serviceTokenData) {
userId = req.serviceTokenData.user._id
}
secrets = await pull({
userId,
workspaceId,
environment,
channel: channel ? channel : 'cli',
ipAddress: req.ip
});
if (channel !== 'cli') {
secrets = reformatPullSecrets({ secrets });
}
if (postHogClient) {
// capture secrets pushed event in production
postHogClient.capture({
distinctId: req.user.email,
event: 'secrets pulled',
properties: {
numberOfSecrets: secrets.length,
environment,
workspaceId,
channel: channel ? channel : 'cli'
}
});
}
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to pull workspace secrets'
});
}
return res.status(200).send({
secrets
});
};
export const getWorkspaceKey = async (req: Request, res: Response) => {
let key;
try {
const { workspaceId } = req.params;
key = await Key.findOne({
workspace: workspaceId,
receiver: req.user._id
}).populate('sender', '+publicKey');
if (!key) throw new Error('Failed to find workspace key');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
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({
serviceTokenData
});
}

@ -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 secretController from './secretController';
import * as secretSnapshotController from './secretSnapshotController';
import * as workspaceController from './workspaceController';
import * as actionController from './actionController';
export {
stripeController,
secretController,
workspaceController
secretSnapshotController,
workspaceController,
actionController
}

@ -1,6 +1,6 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { SecretVersion } from '../models';
import { SecretVersion } from '../../models';
/**
* Return secret versions for secret with id [secretId]
@ -18,6 +18,7 @@ import { SecretVersion } from '../models';
secretVersions = await SecretVersion.find({
secret: secretId
})
.sort({ createdAt: -1 })
.skip(offset)
.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,7 +1,7 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import Stripe from 'stripe';
import { STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET } from '../config';
import { STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET } from '../../../config';
const stripe = new Stripe(STRIPE_SECRET_KEY, {
apiVersion: '2022-08-01'
});

@ -0,0 +1,112 @@
import e, { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import {
SecretSnapshot,
Log
} from '../../models';
/**
* Return secret snapshots for workspace with id [workspaceId]
* @param req
* @param res
*/
export const getWorkspaceSecretSnapshots = async (req: Request, res: Response) => {
let secretSnapshots;
try {
const { workspaceId } = req.params;
const offset: number = parseInt(req.query.offset as string);
const limit: number = parseInt(req.query.limit as string);
secretSnapshots = await SecretSnapshot.find({
workspace: workspaceId
})
.sort({ createdAt: -1 })
.skip(offset)
.limit(limit);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get secret snapshots'
});
}
return res.status(200).send({
secretSnapshots
});
}
/**
* 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
});
}

@ -1,35 +0,0 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { SecretSnapshot } from '../models';
/**
* Return secret snapshots for workspace with id [workspaceId]
* @param req
* @param res
*/
export const getWorkspaceSecretSnapshots = async (req: Request, res: Response) => {
let secretSnapshots;
try {
const { workspaceId } = req.params;
const offset: number = parseInt(req.query.offset as string);
const limit: number = parseInt(req.query.limit as string);
secretSnapshots = await SecretSnapshot.find({
workspace: workspaceId
})
.skip(offset)
.limit(limit);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get secret snapshots'
});
}
return res.status(200).send({
secretSnapshots
});
}

@ -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 {
Secret
Secret,
ISecret
} from '../../models';
import {
SecretSnapshot,
@ -9,66 +11,159 @@ import {
} 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
* secretsnapshots collection.
* @param {Object} obj
* @param {String} obj.workspaceId
* @returns {SecretSnapshot} secretSnapshot - new secret snapshot
*/
const takeSecretSnapshotHelper = async ({
workspaceId
}: {
workspaceId: string;
}) => {
let secretSnapshot;
try {
const secrets = await Secret.find({
const secretIds = (await Secret.find({
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({
workspace: workspaceId
}).sort({ version: -1 });
if (!latestSecretSnapshot) {
// case: no snapshots exist for workspace -> create first snapshot
await new SecretSnapshot({
workspace: workspaceId,
version: 1,
secrets
}).save();
return;
}
// case: snapshots exist for workspace
await new SecretSnapshot({
secretSnapshot = await new SecretSnapshot({
workspace: workspaceId,
version: latestSecretSnapshot.version + 1,
secrets
version: latestSecretSnapshot ? latestSecretSnapshot.version + 1 : 1,
secretVersions: latestSecretVersions
}).save();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
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 ({
secretVersions
}: {
secretVersions: ISecretVersion[]
}) => {
let newSecretVersions;
try {
await SecretVersion.insertMany(secretVersions);
newSecretVersions = await SecretVersion.insertMany(secretVersions);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
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 {
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 SecretVersion, { ISecretVersion } from "./secretVersion";
import SecretSnapshot, { ISecretSnapshot } from './secretSnapshot';
import SecretVersion, { ISecretVersion } from './secretVersion';
import Log, { ILog } from './log';
import Action, { IAction } from './action';
export {
SecretSnapshot,
ISecretSnapshot,
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 {
SECRET_SHARED,
SECRET_PERSONAL,
ENV_DEV,
ENV_TESTING,
ENV_STAGING,
ENV_PROD
} from '../../variables';
export interface ISecretSnapshot {
workspace: Types.ObjectId;
version: number;
secrets: {
version: number;
workspace: Types.ObjectId;
type: string;
user: Types.ObjectId;
environment: string;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretKeyHash: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretValueHash: string;
}[]
secretVersions: Types.ObjectId[];
}
const secretSnapshotSchema = new Schema<ISecretSnapshot>(
@ -39,64 +17,10 @@ const secretSnapshotSchema = new Schema<ISecretSnapshot>(
type: Number,
required: true
},
secrets: [{
version: {
type: Number,
default: 1,
required: true
},
workspace: {
type: Schema.Types.ObjectId,
ref: 'Workspace',
required: true
},
type: {
type: String,
enum: [SECRET_SHARED, SECRET_PERSONAL],
required: true
},
user: {
// user associated with the personal secret
type: Schema.Types.ObjectId,
ref: 'User'
},
environment: {
type: String,
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD],
required: true
},
secretKeyCiphertext: {
type: String,
required: true
},
secretKeyIV: {
type: String, // symmetric
required: true
},
secretKeyTag: {
type: String, // symmetric
required: true
},
secretKeyHash: {
type: String,
required: true
},
secretValueCiphertext: {
type: String,
required: true
},
secretValueIV: {
type: String, // symmetric
required: true
},
secretValueTag: {
type: String, // symmetric
required: true
},
secretValueHash: {
type: String,
required: true
}
secretVersions: [{
type: Schema.Types.ObjectId,
ref: 'SecretVersion',
required: true
}]
},
{

@ -1,9 +1,30 @@
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 {
_id?: Types.ObjectId;
secret: Types.ObjectId;
version: number;
workspace: Types.ObjectId; // new
type: string; // new
user: Types.ObjectId; // new
environment: string; // new
isDeleted: boolean;
secretKeyCiphertext: string;
secretKeyIV: string;
@ -27,6 +48,26 @@ const secretVersionSchema = new Schema<ISecretVersion>(
default: 1,
required: true
},
workspace: {
type: Schema.Types.ObjectId,
ref: 'Workspace',
required: true
},
type: {
type: String,
enum: [SECRET_SHARED, SECRET_PERSONAL],
required: true
},
user: {
// user associated with the personal secret
type: Schema.Types.ObjectId,
ref: 'User'
},
environment: {
type: String,
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD],
required: true
},
isDeleted: {
type: Boolean,
default: false,

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

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

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

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

@ -1,6 +1,6 @@
import express from 'express';
const router = express.Router();
import { stripeController } from '../controllers';
import { stripeController } from '../../controllers/v1';
router.post('/webhook', stripeController.handleWebhook);

@ -0,0 +1,58 @@
import express from 'express';
const router = express.Router();
import {
requireAuth,
requireWorkspaceAuth,
validateRequest
} from '../../../middleware';
import { param, query } from 'express-validator';
import { ADMIN, MEMBER } from '../../../variables';
import { workspaceController } from '../../controllers/v1';
router.get(
'/:workspaceId/secret-snapshots',
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
}),
param('workspaceId').exists().trim(),
query('offset').exists().isInt(),
query('limit').exists().isInt(),
validateRequest,
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;

@ -1,27 +0,0 @@
import express from 'express';
const router = express.Router();
import {
requireAuth,
requireWorkspaceAuth,
validateRequest
} from '../../middleware';
import { param, query } from 'express-validator';
import { ADMIN, MEMBER, GRANTED } from '../../variables';
import { workspaceController } from '../controllers';
router.get(
'/:workspaceId/secret-snapshots',
requireAuth,
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [GRANTED]
}),
param('workspaceId').exists().trim(),
query('offset').exists().isInt(),
query('limit').exists().isInt(),
validateRequest,
workspaceController.getWorkspaceSecretSnapshots
);
export default router;

@ -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 {
takeSecretSnapshotHelper,
addSecretVersionsHelper
addSecretVersionsHelper,
markDeletedSecretVersionsHelper,
initSecretVersioningHelper
} from '../helpers/secret';
import EELicenseService from './EELicenseService';
@ -11,12 +14,13 @@ import EELicenseService from './EELicenseService';
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
* SecretSnapshot collection.
* Requires a valid license key [licenseKey]
* @param {Object} obj
* @param {String} obj.workspaceId
* @returns {SecretSnapshot} secretSnapshot - new secret snpashot
*/
static async takeSecretSnapshot({
workspaceId
@ -24,13 +28,14 @@ class EESecretService {
workspaceId: string;
}) {
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 {SecretVersion} obj.secretVersions
* @param {Object[]} obj.secretVersions
* @returns {SecretVersion[]} newSecretVersions - new secret versions
*/
static async addSecretVersions({
secretVersions
@ -38,10 +43,36 @@ class EESecretService {
secretVersions: ISecretVersion[];
}) {
if (!EELicenseService.isLicenseValid) return;
await addSecretVersionsHelper({
return await addSecretVersionsHelper({
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;

@ -1,7 +1,9 @@
import EELicenseService from "./EELicenseService";
import EESecretService from "./EESecretService";
import EELogService from "./EELogService";
export {
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 {
ciphertextKey: string;
@ -19,7 +22,7 @@ interface PushSecret {
* @returns
*/
const eventPushSecrets = ({
workspaceId,
workspaceId
}: {
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 {
eventPushSecrets
}

@ -1,7 +1,10 @@
import jwt from 'jsonwebtoken';
import * as Sentry from '@sentry/node';
import bcrypt from 'bcrypt';
import {
User
User,
ServiceTokenData,
APIKeyData
} from '../models';
import {
JWT_AUTH_LIFETIME,
@ -9,6 +12,179 @@ import {
JWT_REFRESH_LIFETIME,
JWT_REFRESH_SECRET
} 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]
@ -99,4 +275,12 @@ const createToken = ({
}
};
export { createToken, issueTokens, clearTokens };
export {
validateAuthMode,
getAuthUserPayload,
getAuthSTDPayload,
getAuthAPIKeyPayload,
createToken,
issueTokens,
clearTokens
};

@ -12,7 +12,6 @@ import {
decryptSymmetric,
decryptAsymmetric
} from '../utils/crypto';
import { decryptSecrets } from '../helpers/secret';
import { ENCRYPTION_KEY } from '../config';
import { SECRET_SHARED } from '../variables';

@ -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]
* 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 {String} obj.userId - id of user to validate
* @param {String} obj.workspaceId - id of workspace
@ -12,12 +12,10 @@ const validateMembership = async ({
userId,
workspaceId,
acceptedRoles,
acceptedStatuses
}: {
userId: string;
workspaceId: string;
acceptedRoles: string[];
acceptedStatuses: string[];
}) => {
let membership;
@ -26,18 +24,13 @@ const validateMembership = async ({
membership = await Membership.findOne({
user: userId,
workspace: workspaceId
});
}).populate("workspace");
if (!membership) throw new Error('Failed to find membership');
if (!acceptedRoles.includes(membership.role)) {
throw new Error('Failed to validate membership role');
}
if (!acceptedStatuses.includes(membership.status)) {
throw new Error('Failed to validate membership status');
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
@ -72,18 +65,15 @@ const findMembership = async (queryObj: any) => {
* @param {String[]} obj.userIds - id of users.
* @param {String} obj.workspaceId - id of workspace.
* @param {String[]} obj.roles - roles of users.
* @param {String[]} obj.statuses - statuses of users.
*/
const addMemberships = async ({
userIds,
workspaceId,
roles,
statuses
roles
}: {
userIds: string[];
workspaceId: string;
roles: string[];
statuses: string[];
}): Promise<void> => {
try {
const operations = userIds.map((userId, idx) => {
@ -92,14 +82,12 @@ const addMemberships = async ({
filter: {
user: userId,
workspace: workspaceId,
role: roles[idx],
status: statuses[idx]
role: roles[idx]
},
update: {
user: userId,
workspace: workspaceId,
role: roles[idx],
status: statuses[idx]
role: roles[idx]
},
upsert: true
}

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

@ -1,22 +1,26 @@
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import {
Secret,
ISecret,
} from '../models';
import {
EESecretService
EESecretService,
EELogService
} from '../ee/services';
import {
SecretVersion
IAction
} from '../ee/models';
import {
takeSecretSnapshotHelper
} from '../ee/helpers/secret';
import { decryptSymmetric } from '../utils/crypto';
import { SECRET_SHARED, SECRET_PERSONAL } from '../variables';
import { LICENSE_KEY } from '../config';
import {
SECRET_SHARED,
SECRET_PERSONAL,
ACTION_ADD_SECRETS,
ACTION_UPDATE_SECRETS,
ACTION_DELETE_SECRETS,
ACTION_READ_SECRETS
} from '../variables';
interface PushSecret {
interface V1PushSecret {
ciphertextKey: string;
ivKey: string;
tagKey: string;
@ -25,15 +29,33 @@ interface PushSecret {
ivValue: string;
tagValue: string;
hashValue: string;
ciphertextComment: string;
ivComment: string;
tagComment: string;
hashComment: string;
type: 'shared' | 'personal';
}
interface V2PushSecret {
type: string; // personal or shared
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretKeyHash: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretValueHash: string;
secretCommentCiphertext?: string;
secretCommentIV?: string;
secretCommentTag?: string;
secretCommentHash?: string;
}
interface Update {
[index: string]: any;
}
type DecryptSecretType = 'text' | 'object' | 'expanded';
/**
* Push secrets for user with id [userId] to workspace
* with id [workspaceId] with environment [environment]. Follow steps:
@ -45,21 +67,21 @@ type DecryptSecretType = 'text' | 'object' | 'expanded';
* @param {String} obj.environment - environment for secrets
* @param {Object[]} obj.secrets - secrets to push
*/
const pushSecrets = async ({
const v1PushSecrets = async ({
userId,
workspaceId,
environment,
secrets
secrets,
}: {
userId: string;
workspaceId: string;
environment: string;
secrets: PushSecret[];
secrets: V1PushSecret[];
}): Promise<void> => {
// TODO: clean up function and fix up types
try {
// construct useful data structures
const oldSecrets = await pullSecrets({
const oldSecrets = await getSecrets({
userId,
workspaceId,
environment
@ -82,19 +104,18 @@ const pushSecrets = async ({
await Secret.deleteMany({
_id: { $in: toDelete }
});
await SecretVersion.updateMany({
secret: { $in: toDelete }
}, {
isDeleted: true
await EESecretService.markDeletedSecretVersions({
secretIds: toDelete
});
}
const toUpdate = oldSecrets
.filter((s) => {
if (`${s.type}-${s.secretKeyHash}` in newSecretsObj) {
if (s.secretValueHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].hashValue) {
// case: filter secrets where value changed
if (s.secretValueHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].hashValue
|| s.secretCommentHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].hashComment) {
// case: filter secrets where value or comment changed
return true;
}
@ -113,14 +134,22 @@ const pushSecrets = async ({
ciphertextValue,
ivValue,
tagValue,
hashValue
hashValue,
ciphertextComment,
ivComment,
tagComment,
hashComment
} = newSecretsObj[`${s.type}-${s.secretKeyHash}`];
const update: Update = {
secretValueCiphertext: ciphertextValue,
secretValueIV: ivValue,
secretValueTag: tagValue,
secretValueHash: hashValue
secretValueHash: hashValue,
secretCommentCiphertext: ciphertextComment,
secretCommentIV: ivComment,
secretCommentTag: tagComment,
secretCommentHash: hashComment,
}
if (!s.version) {
@ -160,6 +189,10 @@ const pushSecrets = async ({
return ({
secret: _id,
version: version ? version + 1 : 1,
workspace: new Types.ObjectId(workspaceId),
type: newSecret.type,
user: new Types.ObjectId(userId),
environment,
isDeleted: false,
secretKeyCiphertext: newSecret.ciphertextKey,
secretKeyIV: newSecret.ivKey,
@ -192,7 +225,11 @@ const pushSecrets = async ({
secretValueCiphertext: s.ciphertextValue,
secretValueIV: s.ivValue,
secretValueTag: s.tagValue,
secretValueHash: s.hashValue
secretValueHash: s.hashValue,
secretCommentCiphertext: s.ciphertextComment,
secretCommentIV: s.ivComment,
secretCommentTag: s.tagComment,
secretCommentHash: s.hashComment
};
if (toAdd[idx].type === 'personal') {
@ -207,6 +244,11 @@ const pushSecrets = async ({
EESecretService.addSecretVersions({
secretVersions: newSecrets.map(({
_id,
version,
workspace,
type,
user,
environment,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
@ -217,7 +259,11 @@ const pushSecrets = async ({
secretValueHash
}) => ({
secret: _id,
version: 1,
version,
workspace,
type,
user,
environment,
isDeleted: false,
secretKeyCiphertext,
secretKeyIV,
@ -243,15 +289,235 @@ const pushSecrets = async ({
};
/**
* Pull secrets for user with id [userId] for workspace
* Push secrets for user with id [userId] to workspace
* with id [workspaceId] with environment [environment]. Follow steps:
* 1. Handle shared secrets (insert, delete)
* 2. handle personal secrets (insert, delete)
* @param {Object} obj
* @param {String} obj.userId - id of user to push secrets for
* @param {String} obj.workspaceId - id of workspace to push to
* @param {String} obj.environment - environment for secrets
* @param {Object[]} obj.secrets - secrets to push
* @param {String} obj.channel - channel (web/cli/auto)
* @param {String} obj.ipAddress - ip address of request to push secrets
*/
const v2PushSecrets = async ({
userId,
workspaceId,
environment,
secrets,
channel,
ipAddress
}: {
userId: string;
workspaceId: string;
environment: string;
secrets: V2PushSecret[];
channel: string;
ipAddress: string;
}): Promise<void> => {
// TODO: clean up function and fix up types
try {
const actions: IAction[] = [];
// construct useful data structures
const oldSecrets = await getSecrets({
userId,
workspaceId,
environment
});
const oldSecretsObj: any = oldSecrets.reduce((accumulator, s: any) =>
({ ...accumulator, [`${s.type}-${s.secretKeyHash}`]: s })
, {});
const newSecretsObj: any = secrets.reduce((accumulator, s) =>
({ ...accumulator, [`${s.type}-${s.secretKeyHash}`]: s })
, {});
// handle deleting secrets
const toDelete = oldSecrets
.filter(
(s: ISecret) => !(`${s.type}-${s.secretKeyHash}` in newSecretsObj)
)
.map((s) => s._id);
if (toDelete.length > 0) {
await Secret.deleteMany({
_id: { $in: toDelete }
});
await EESecretService.markDeletedSecretVersions({
secretIds: toDelete
});
const deleteAction = await EELogService.createActionSecret({
name: ACTION_DELETE_SECRETS,
userId,
workspaceId,
secretIds: toDelete
});
deleteAction && actions.push(deleteAction);
}
const toUpdate = oldSecrets
.filter((s) => {
if (`${s.type}-${s.secretKeyHash}` in newSecretsObj) {
if (s.secretValueHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].secretValueHash
|| s.secretCommentHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].secretCommentHash) {
// case: filter secrets where value or comment changed
return true;
}
if (!s.version) {
// case: filter (legacy) secrets that were not versioned
return true;
}
}
return false;
});
if (toUpdate.length > 0) {
const operations = toUpdate
.map((s) => {
const {
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
secretCommentHash,
} = newSecretsObj[`${s.type}-${s.secretKeyHash}`];
const update: Update = {
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
secretCommentHash,
}
if (!s.version) {
// case: (legacy) secret was not versioned
update.version = 1;
} else {
update['$inc'] = {
version: 1
}
}
if (s.type === SECRET_PERSONAL) {
// attach user associated with the personal secret
update['user'] = userId;
}
return {
updateOne: {
filter: {
_id: oldSecretsObj[`${s.type}-${s.secretKeyHash}`]._id
},
update
}
};
});
await Secret.bulkWrite(operations as any);
// (EE) add secret versions for updated secrets
await EESecretService.addSecretVersions({
secretVersions: toUpdate.map((s) => {
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
EESecretService.addSecretVersions({
secretVersions: newSecrets.map((secretDocument) => {
return {
...secretDocument.toObject(),
secret: secretDocument._id,
isDeleted: false
}})
});
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
await EESecretService.takeSecretSnapshot({
workspaceId
})
// (EE) create (audit) log
if (actions.length > 0) {
await EELogService.createLog({
userId,
workspaceId,
actions,
channel,
ipAddress
});
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to push shared and personal secrets');
}
};
/**
* Get 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
*
*/
const pullSecrets = async ({
const getSecrets = async ({
userId,
workspaceId,
environment
@ -288,9 +554,64 @@ const pullSecrets = async ({
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
* clients handle secrets
* web client handle secrets
* @param {Object} obj
* @param {Object} obj.secrets
*/
@ -315,6 +636,13 @@ const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => {
iv: s.secretValueIV,
tag: s.secretValueTag,
hash: s.secretValueHash
},
secretComment: {
workspace: s.workspace,
ciphertext: s.secretCommentCiphertext,
iv: s.secretCommentIV,
tag: s.secretCommentTag,
hash: s.secretCommentHash
}
}));
} catch (err) {
@ -326,73 +654,9 @@ const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => {
return reformatedSecrets;
};
/**
* Return decrypted secrets in format [format]
* @param {Object} obj
* @param {Object[]} obj.secrets - array of (encrypted) secret key-value pair objects
* @param {String} obj.key - symmetric key to decrypt secret key-value pairs
* @param {String} obj.format - desired return format that is either "text," "object," or "expanded"
* @return {String|Object} (decrypted) secrets also called the content
*/
const decryptSecrets = ({
secrets,
key,
format
}: {
secrets: PushSecret[];
key: string;
format: DecryptSecretType;
}) => {
// init content
let content: any = format === 'text' ? '' : {};
// decrypt secrets
secrets.forEach((s, idx) => {
const secretKey = decryptSymmetric({
ciphertext: s.ciphertextKey,
iv: s.ivKey,
tag: s.tagKey,
key
});
const secretValue = decryptSymmetric({
ciphertext: s.ciphertextValue,
iv: s.ivValue,
tag: s.tagValue,
key
});
switch (format) {
case 'text':
content += secretKey;
content += '=';
content += secretValue;
if (idx < secrets.length) {
content += '\n';
}
break;
case 'object':
content[secretKey] = secretValue;
break;
case 'expanded':
content[secretKey] = {
...s,
plaintextKey: secretKey,
plaintextValue: secretValue
};
break;
}
});
return content;
};
export {
pushSecrets,
v1PushSecrets,
v2PushSecrets,
pullSecrets,
reformatPullSecrets,
decryptSecrets
reformatPullSecrets
};

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

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

@ -21,7 +21,6 @@ import {
CLIENT_SECRET_NETLIFY,
CLIENT_SECRET_GITHUB
} from '../config';
import { user } from '../routes';
interface ExchangeCodeHerokuResponse {
token_type: string;

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

@ -4,26 +4,33 @@ import * as Sentry from '@sentry/node';
import { InternalServerError } from "../utils/errors";
import { getLogger } from "../utils/logger";
import RequestError, { LogLevel } from "../utils/requestError";
import { NODE_ENV } from "../config";
export const requestErrorHandler: ErrorRequestHandler = (error: RequestError|Error, req, res, next) => {
if(res.headersSent) return next();
export const requestErrorHandler: ErrorRequestHandler = (error: RequestError | Error, req, res, 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
if(!(error instanceof RequestError)){
error = InternalServerError({context: {exception: error.message}, stack: error.stack})
if (!(error instanceof RequestError)) {
error = InternalServerError({ context: { exception: error.message }, stack: error.stack })
getLogger('backend-main').log((<RequestError>error).levelName.toLowerCase(), (<RequestError>error).message)
}
//* 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 })
}
//* Only sent error to Sentry if LogLevel is one of the following level 'ERROR', 'EMERGENCY' or 'CRITICAL'
//* with this we will eliminate false-positive errors like 'BadRequestError', 'UnauthorizedRequestError' and so on
if([LogLevel.ERROR, LogLevel.EMERGENCY, LogLevel.CRITICAL].includes((<RequestError>error).level)){
if ([LogLevel.ERROR, LogLevel.EMERGENCY, LogLevel.CRITICAL].includes((<RequestError>error).level)) {
Sentry.captureException(error)
}
res.status((<RequestError>error).statusCode).json((<RequestError>error).format(req))
next()
}

@ -1,8 +1,13 @@
import jwt from 'jsonwebtoken';
import { Request, Response, NextFunction } from 'express';
import { User } from '../models';
import { JWT_AUTH_SECRET } from '../config';
import { AccountNotFoundError, BadRequestError, UnauthorizedRequestError } from '../utils/errors';
import { User, ServiceTokenData } from '../models';
import {
validateAuthMode,
getAuthUserPayload,
getAuthSTDPayload,
getAuthAPIKeyPayload
} from '../helpers/auth';
import { BadRequestError } from '../utils/errors';
declare module 'jsonwebtoken' {
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),
* if there is an associated user, and if that user is fully setup.
* @param req - express request object
* @param res - express response object
* @param next - express next function
* Validate if token on request is valid (e.g. not expired) for various auth modes:
* - If token is a JWT token, then check if there is an associated user
* and if user is fully setup.
* - If token is a service token (st), then check if there is associated
* service token data.
* @param {Object} obj
* @param {String[]} obj.acceptedAuthModes - accepted modes of authentication (jwt/st)
* @returns
*/
const requireAuth = async (req: Request, res: Response, next: NextFunction) => {
// JWT authentication middleware
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 requireAuth = ({
acceptedAuthModes = ['jwt']
}: {
acceptedAuthModes: string[];
}) => {
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>(
jwt.verify(AUTH_TOKEN_VALUE, JWT_AUTH_SECRET)
);
// validate auth token against
const authMode = validateAuthMode({
authTokenValue: AUTH_TOKEN_VALUE,
acceptedAuthModes
});
const user = await User.findOne({
_id: decodedToken.userId
}).select('+publicKey');
if (!acceptedAuthModes.includes(authMode)) throw new Error('Failed to validate auth mode');
if (!user) return next(AccountNotFoundError({message: 'Failed to locate User account'}))
if (!user?.publicKey)
return next(UnauthorizedRequestError({message: 'Unable to authenticate due to partially set up account'}))
// attach auth payloads
switch (authMode) {
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;

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

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

@ -10,16 +10,13 @@ import { UnauthorizedRequestError } from '../utils/errors';
* with the integration authorization on request params.
* @param {Object} obj
* @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
*/
const requireIntegrationAuthorizationAuth = ({
acceptedRoles,
acceptedStatuses,
attachAccessToken = true
}: {
acceptedRoles: string[];
acceptedStatuses: string[];
attachAccessToken?: boolean;
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
@ -38,8 +35,7 @@ const requireIntegrationAuthorizationAuth = ({
await validateMembership({
userId: req.user._id.toString(),
workspaceId: integrationAuth.workspace.toString(),
acceptedRoles,
acceptedStatuses
acceptedRoles
});
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 { BadRequestError, UnauthorizedRequestError } from '../utils/errors';
// TODO: deprecate
declare module 'jsonwebtoken' {
export interface UserIDJwtPayload extends jwt.JwtPayload {
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
* on request params.
* @param {Object} obj
* @param {String[]} obj.acceptedRoles - accepted workspace roles
* @param {String[]} obj.acceptedStatuses - accepted workspace statuses
* @param {String[]} obj.acceptedRoles - accepted workspace roles for JWT auth
* @param {String[]} obj.location - location of [workspaceId] on request (e.g. params, body) for parsing
*/
const requireWorkspaceAuth = ({
acceptedRoles,
acceptedStatuses,
location = 'params'
}: {
acceptedRoles: string[];
acceptedStatuses: string[];
location?: req;
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
// workspace authorization middleware
try {
const membership = await validateMembership({
userId: req.user._id.toString(),
workspaceId: req[location].workspaceId,
acceptedRoles,
acceptedStatuses
});
const { workspaceId } = req[location];
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();
} catch (err) {

@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from 'express';
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
@ -15,12 +15,12 @@ const validate = (req: Request, res: Response, next: NextFunction) => {
try {
const errors = validationResult(req);
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();
} 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 UserAction, { IUserAction } from './userAction';
import Workspace, { IWorkspace } from './workspace';
import ServiceTokenData, { IServiceTokenData } from './serviceTokenData';
import APIKeyData, { IAPIKeyData } from './apiKeyData';
export {
BackupPrivateKey,
@ -47,5 +49,9 @@ export {
UserAction,
IUserAction,
Workspace,
IWorkspace
IWorkspace,
ServiceTokenData,
IServiceTokenData,
APIKeyData,
IAPIKeyData
};

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

@ -23,13 +23,18 @@ export interface ISecret {
secretValueIV: string;
secretValueTag: string;
secretValueHash: string;
secretCommentCiphertext?: string;
secretCommentIV?: string;
secretCommentTag?: string;
secretCommentHash?: string;
}
const secretSchema = new Schema<ISecret>(
{
version: {
type: Number,
required: true
required: true,
default: 1
},
workspace: {
type: Schema.Types.ObjectId,
@ -82,6 +87,22 @@ const secretSchema = new Schema<ISecret>(
secretValueHash: {
type: String,
required: true
},
secretCommentCiphertext: {
type: String,
required: false
},
secretCommentIV: {
type: String, // symmetric
required: false
},
secretCommentTag: {
type: String, // symmetric
required: false
},
secretCommentHash: {
type: String,
required: false
}
},
{

@ -1,6 +1,7 @@
import { Schema, model, Types } from 'mongoose';
import { ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD } from '../variables';
// TODO: deprecate
export interface IServiceToken {
_id: Types.ObjectId;
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;
salt?: string;
verifier?: string;
refreshVersion?: Number;
refreshVersion?: number;
}
const userSchema = new Schema<IUser>(
@ -52,7 +52,8 @@ const userSchema = new Schema<IUser>(
},
refreshVersion: {
type: Number,
default: 0
default: 0,
select: false
}
},
{

@ -1,8 +0,0 @@
import express from 'express';
const router = express.Router();
import { requireAuth } from '../middleware';
import { userController } from '../controllers';
router.get('/', requireAuth, userController.getUser);
export default router;

@ -1,9 +1,9 @@
import express from 'express';
const router = express.Router();
import { body } from 'express-validator';
import { requireAuth, validateRequest } from '../middleware';
import { authController } from '../controllers';
import { loginLimiter } from '../helpers/rateLimiter';
import { requireAuth, validateRequest } from '../../middleware';
import { authController } from '../../controllers/v1';
import { loginLimiter } from '../../helpers/rateLimiter';
router.post('/token', validateRequest, authController.getNewToken);
@ -25,7 +25,19 @@ router.post(
authController.login2
);
router.post('/logout', requireAuth, authController.logout);
router.post('/checkAuth', requireAuth, authController.checkAuth);
router.post(
'/logout',
requireAuth({
acceptedAuthModes: ['jwt']
}),
authController.logout
);
router.post(
'/checkAuth',
requireAuth({
acceptedAuthModes: ['jwt']
}),
authController.checkAuth
);
export default router;

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

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

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

@ -1,12 +1,14 @@
import express from 'express';
const router = express.Router();
import { body } from 'express-validator';
import { requireAuth, validateRequest } from '../middleware';
import { membershipOrgController } from '../controllers';
import { requireAuth, validateRequest } from '../../middleware';
import { membershipOrgController } from '../../controllers/v1';
router.post(
'/signup',
requireAuth,
requireAuth({
acceptedAuthModes: ['jwt']
}),
body('inviteeEmail').exists().trim().notEmpty().isEmail(),
body('organizationId').exists().trim().notEmpty(),
validateRequest,

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

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

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

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

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

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