diff --git a/appveyor.yml.flagged-for-delete b/appveyor.yml.flagged-for-delete deleted file mode 100644 index f1550d9..0000000 --- a/appveyor.yml.flagged-for-delete +++ /dev/null @@ -1,169 +0,0 @@ -image: -- Visual Studio 2017 -- Ubuntu1804 - -branches: - except: - - l10n_master - -environment: - WIN_PKG: C:\Users\appveyor\.pkg-cache\v2.5\fetched-v10.4.1-win-x64 - -stack: node 10 - -init: -- ps: | - if($isWindows -and $env:DEBUG_RDP -eq "true") { - iex ((new-object net.webclient).DownloadString(` - 'https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) - } -- ps: | - if($isWindows) { - Install-Product node 10 - $env:PATH = "C:\Program Files (x86)\Resource Hacker;${env:PATH}" - } - if($env:APPVEYOR_REPO_TAG -eq "true") { - $tagName = $env:APPVEYOR_REPO_TAG_NAME.TrimStart("v") - $env:RELEASE_NAME = "Version ${tagName}" - } - -install: -- ps: | - $env:PACKAGE_VERSION = (Get-Content -Raw -Path .\package.json | ConvertFrom-Json).version - $env:PROD_DEPLOY = "false" - if($env:APPVEYOR_REPO_TAG -eq "true" -and $env:APPVEYOR_RE_BUILD -eq "True") { - $env:PROD_DEPLOY = "true" - echo "This is a production deployment." - } -- ps: | - if($isWindows) { - if(Test-Path -Path $env:WIN_PKG) { - $env:VER_INFO = "true" - } - choco install reshack --no-progress - choco install cloc --no-progress - choco install checksum --no-progress - cloc --include-lang TypeScript,JavaScript --vcs git - .\scripts\make-versioninfo.ps1 - } - -before_build: -- node --version -- npm --version -# Get new $SNAP_TOKEN with: -# $ snapcraft export-login --snaps bw --acls package_push,package_release - -- sh: | - if [ "${SNAP_TOKEN}" != "" -a "${PROD_DEPLOY}" == "true" ] - then - sudo apt-get update - sudo apt-get -y install snapd - sudo snap install snapcraft --classic - export PATH="$PATH:/snap/bin" - echo "$SNAP_TOKEN" | snapcraft login --with - - fi -- ps: | - if($isWindows -and $env:PROD_DEPLOY -eq "true") { - if($env:CHOCO_API_KEY -ne $null) { - choco apikey --key $env:CHOCO_API_KEY --source https://push.chocolatey.org/ - } - if($env:NPM_TOKEN -ne $null) { - "//registry.npmjs.org/:_authToken=${env:NPM_TOKEN}" | Out-File ".npmrc" -Encoding UTF8 - } - } - -build_script: -- cmd: | - if defined VER_INFO ResourceHacker -open %WIN_PKG% -save %WIN_PKG% -action delete -mask ICONGROUP,1, - if defined VER_INFO ResourceHacker -open version-info.rc -save version-info.res -action compile - if defined VER_INFO ResourceHacker -open %WIN_PKG% -save %WIN_PKG% -action addoverwrite -resource version-info.res -- cmd: npm install -- cmd: npm run sub:init -- cmd: npm run dist -- cmd: 7z a ./dist/bw-windows-%PACKAGE_VERSION%.zip ./dist/windows/bw.exe -- cmd: 7z a ./dist/bw-macos-%PACKAGE_VERSION%.zip ./dist/macos/bw -- cmd: 7z a ./dist/bw-linux-%PACKAGE_VERSION%.zip ./dist/linux/bw -- ps: | - if($isWindows) { - Expand-Archive -Path "./dist/bw-windows-${env:PACKAGE_VERSION}.zip" -DestinationPath "./test/windows" - $testVersion = Invoke-Expression '& ./test/windows/bw.exe -v' - if($testVersion -ne $env:PACKAGE_VERSION) { - Throw "Version test failed." - } - } -- ps: | - if($isWindows) { - .\scripts\choco-pack.ps1 - checksum -f="./dist/bw-windows-${env:PACKAGE_VERSION}.zip" ` - -t sha256 | Out-File -Encoding ASCII ./dist/bw-windows-sha256-${env:PACKAGE_VERSION}.txt - checksum -f="./dist/bw-macos-${env:PACKAGE_VERSION}.zip" ` - -t sha256 | Out-File -Encoding ASCII ./dist/bw-macos-sha256-${env:PACKAGE_VERSION}.txt - checksum -f="./dist/bw-linux-${env:PACKAGE_VERSION}.zip" ` - -t sha256 | Out-File -Encoding ASCII ./dist/bw-linux-sha256-${env:PACKAGE_VERSION}.txt - - if($env:PROD_DEPLOY -ne "true") { - Push-AppveyorArtifact .\dist\bw-windows-${env:PACKAGE_VERSION}.zip - Push-AppveyorArtifact .\dist\bw-macos-${env:PACKAGE_VERSION}.zip - Push-AppveyorArtifact .\dist\bw-linux-${env:PACKAGE_VERSION}.zip - Push-AppveyorArtifact .\dist\bw-windows-sha256-${env:PACKAGE_VERSION}.txt - Push-AppveyorArtifact .\dist\bw-macos-sha256-${env:PACKAGE_VERSION}.txt - Push-AppveyorArtifact .\dist\bw-linux-sha256-${env:PACKAGE_VERSION}.txt - Push-AppveyorArtifact .\dist\chocolatey\bitwarden-cli.${env:PACKAGE_VERSION}.nupkg - } - } - -after_build: -- ps: | - if($env:PROD_DEPLOY -eq "true") { - if($isLinux) { - echo "Deploy Linux..." - ./scripts/snap-build.ps1 -version $env:PACKAGE_VERSION - - sudo snap install ./dist/snap/bw*.snap --dangerous - $testVersion = Invoke-Expression '& bw -v' - if($testVersion -ne $env:PACKAGE_VERSION) { - Throw "Version test failed." - } - sudo snap remove bw - - ./scripts/snap-update.ps1 - Push-AppveyorArtifact ./dist/snap/bw_${env:PACKAGE_VERSION}_amd64.snap - } - else { - echo "Deploy Windows..." - .\scripts\choco-update.ps1 -version $env:PACKAGE_VERSION - } - } -- sh: | - if [ "${SNAP_TOKEN}" != "" -a "${PROD_DEPLOY}" == "true" ] - then - snapcraft logout - fi -- cmd: if ["%PROD_DEPLOY%"] equ ["true"] npm run publish:npm - -on_finish: - - ps: | - if($env:DEBUG_RDP -eq "true") { - $blockRdp = $true - iex ((new-object net.webclient).DownloadString(` - 'https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) - } - -for: -- - matrix: - only: - - image: Visual Studio 2017 - cache: - - 'C:\Users\appveyor\.pkg-cache\' - -deploy: - tag: $(APPVEYOR_REPO_TAG_NAME) - release: $(RELEASE_NAME) - provider: GitHub - auth_token: $(GH_TOKEN) - artifact: /.*/ - force_update: true - on: - branch: master - APPVEYOR_REPO_TAG: true - diff --git a/jslib b/jslib index 06239ae..09c444d 160000 --- a/jslib +++ b/jslib @@ -1 +1 @@ -Subproject commit 06239aea2d811852561711bd73e14729fba2071a +Subproject commit 09c444ddd4498b5417769e8a795671a6a8ef6ade diff --git a/package-lock.json b/package-lock.json index 5909d92..8a1d35d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1258,9 +1258,9 @@ } }, "commander": { - "version": "2.18.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.18.0.tgz", - "integrity": "sha512-6CYPa+JP2ftfRU2qkDK+UTVeQYosOg/2GbcjIcKPHfinyOLPVGXu/ovN86RP49Re5ndJK1N0kuiidFFuepc4ZQ==" + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.0.0.tgz", + "integrity": "sha512-ovx/7NkTrnPuIV8sqk/GjUIIM1+iUQeqA3ye2VNpq9sVoiZsooObWlQy+OPWGI17GDaEoybuAGJm6U8yC077BA==" }, "commondir": { "version": "1.0.1", @@ -2138,24 +2138,32 @@ "dependencies": { "abbrev": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "bundled": true, "dev": true, "optional": true }, "ansi-regex": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", "bundled": true, "dev": true, "optional": true }, "aproba": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", "bundled": true, "dev": true, "optional": true }, "are-we-there-yet": { "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", "bundled": true, "dev": true, "optional": true, @@ -2166,12 +2174,16 @@ }, "balanced-match": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "bundled": true, "dev": true, "optional": true }, "brace-expansion": { "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "bundled": true, "dev": true, "optional": true, @@ -2182,36 +2194,48 @@ }, "chownr": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", + "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==", "bundled": true, "dev": true, "optional": true }, "code-point-at": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", "bundled": true, "dev": true, "optional": true }, "concat-map": { "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "bundled": true, "dev": true, "optional": true }, "console-control-strings": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", "bundled": true, "dev": true, "optional": true }, "core-util-is": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "bundled": true, "dev": true, "optional": true }, "debug": { "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "bundled": true, "dev": true, "optional": true, @@ -2221,24 +2245,32 @@ }, "deep-extend": { "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "bundled": true, "dev": true, "optional": true }, "delegates": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", "bundled": true, "dev": true, "optional": true }, "detect-libc": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", "bundled": true, "dev": true, "optional": true }, "fs-minipass": { "version": "1.2.5", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", + "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", "bundled": true, "dev": true, "optional": true, @@ -2248,12 +2280,16 @@ }, "fs.realpath": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "bundled": true, "dev": true, "optional": true }, "gauge": { "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "bundled": true, "dev": true, "optional": true, @@ -2270,6 +2306,8 @@ }, "glob": { "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", "bundled": true, "dev": true, "optional": true, @@ -2284,12 +2322,16 @@ }, "has-unicode": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", "bundled": true, "dev": true, "optional": true }, "iconv-lite": { "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "bundled": true, "dev": true, "optional": true, @@ -2299,6 +2341,8 @@ }, "ignore-walk": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", + "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", "bundled": true, "dev": true, "optional": true, @@ -2308,6 +2352,8 @@ }, "inflight": { "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "bundled": true, "dev": true, "optional": true, @@ -2318,18 +2364,24 @@ }, "inherits": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", "bundled": true, "dev": true, "optional": true }, "ini": { "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", "bundled": true, "dev": true, "optional": true }, "is-fullwidth-code-point": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "bundled": true, "dev": true, "optional": true, @@ -2339,12 +2391,16 @@ }, "isarray": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", "bundled": true, "dev": true, "optional": true }, "minimatch": { "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "bundled": true, "dev": true, "optional": true, @@ -2354,12 +2410,16 @@ }, "minimist": { "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", "bundled": true, "dev": true, "optional": true }, "minipass": { "version": "2.3.5", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz", + "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", "bundled": true, "dev": true, "optional": true, @@ -2370,6 +2430,8 @@ }, "minizlib": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.2.1.tgz", + "integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==", "bundled": true, "dev": true, "optional": true, @@ -2379,6 +2441,8 @@ }, "mkdirp": { "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "bundled": true, "dev": true, "optional": true, @@ -2388,12 +2452,16 @@ }, "ms": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "bundled": true, "dev": true, "optional": true }, "needle": { "version": "2.2.4", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.2.4.tgz", + "integrity": "sha512-HyoqEb4wr/rsoaIDfTH2aVL9nWtQqba2/HvMv+++m8u0dz808MaagKILxtfeSN7QU7nvbQ79zk3vYOJp9zsNEA==", "bundled": true, "dev": true, "optional": true, @@ -2405,6 +2473,8 @@ }, "node-pre-gyp": { "version": "0.10.3", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz", + "integrity": "sha512-d1xFs+C/IPS8Id0qPTZ4bUT8wWryfR/OzzAFxweG+uLN85oPzyo2Iw6bVlLQ/JOdgNonXLCoRyqDzDWq4iw72A==", "bundled": true, "dev": true, "optional": true, @@ -2423,6 +2493,8 @@ }, "nopt": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", "bundled": true, "dev": true, "optional": true, @@ -2433,12 +2505,16 @@ }, "npm-bundled": { "version": "1.0.5", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.5.tgz", + "integrity": "sha512-m/e6jgWu8/v5niCUKQi9qQl8QdeEduFA96xHDDzFGqly0OOjI7c+60KM/2sppfnUU9JJagf+zs+yGhqSOFj71g==", "bundled": true, "dev": true, "optional": true }, "npm-packlist": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.2.0.tgz", + "integrity": "sha512-7Mni4Z8Xkx0/oegoqlcao/JpPCPEMtUvsmB0q7mgvlMinykJLSRTYuFqoQLYgGY8biuxIeiHO+QNJKbCfljewQ==", "bundled": true, "dev": true, "optional": true, @@ -2449,6 +2525,8 @@ }, "npmlog": { "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "bundled": true, "dev": true, "optional": true, @@ -2461,18 +2539,24 @@ }, "number-is-nan": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", "bundled": true, "dev": true, "optional": true }, "object-assign": { "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", "bundled": true, "dev": true, "optional": true }, "once": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "bundled": true, "dev": true, "optional": true, @@ -2482,18 +2566,24 @@ }, "os-homedir": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "bundled": true, "dev": true, "optional": true }, "os-tmpdir": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "bundled": true, "dev": true, "optional": true }, "osenv": { "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", "bundled": true, "dev": true, "optional": true, @@ -2504,18 +2594,24 @@ }, "path-is-absolute": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "bundled": true, "dev": true, "optional": true }, "process-nextick-args": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", "bundled": true, "dev": true, "optional": true }, "rc": { "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "bundled": true, "dev": true, "optional": true, @@ -2528,6 +2624,8 @@ "dependencies": { "minimist": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "bundled": true, "dev": true, "optional": true @@ -2536,6 +2634,8 @@ }, "readable-stream": { "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "bundled": true, "dev": true, "optional": true, @@ -2551,6 +2651,8 @@ }, "rimraf": { "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", "bundled": true, "dev": true, "optional": true, @@ -2560,42 +2662,67 @@ }, "safe-buffer": { "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "bundled": true, "dev": true, "optional": true }, "safer-buffer": { "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "bundled": true, "dev": true, "optional": true }, "sax": { "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "bundled": true, "dev": true, "optional": true }, "semver": { "version": "5.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", + "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==", "bundled": true, "dev": true, "optional": true }, "set-blocking": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "bundled": true, "dev": true, "optional": true }, "signal-exit": { "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "bundled": true, "dev": true, "optional": true }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, "string-width": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "bundled": true, "dev": true, "optional": true, @@ -2605,17 +2732,10 @@ "strip-ansi": "^3.0.0" } }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, "strip-ansi": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "bundled": true, "dev": true, "optional": true, @@ -2625,12 +2745,16 @@ }, "strip-json-comments": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", "bundled": true, "dev": true, "optional": true }, "tar": { "version": "4.4.8", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.8.tgz", + "integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==", "bundled": true, "dev": true, "optional": true, @@ -2646,12 +2770,16 @@ }, "util-deprecate": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "bundled": true, "dev": true, "optional": true }, "wide-align": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", "bundled": true, "dev": true, "optional": true, @@ -2661,12 +2789,16 @@ }, "wrappy": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "bundled": true, "dev": true, "optional": true }, "yallist": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", + "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", "bundled": true, "dev": true, "optional": true @@ -5157,15 +5289,6 @@ "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=", "dev": true }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -5175,6 +5298,15 @@ "safe-buffer": "~5.1.0" } }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, "stringstream": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.6.tgz", @@ -5715,6 +5847,12 @@ "tsutils": "^2.29.0" }, "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, "minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", diff --git a/package.json b/package.json index bf5ca5f..de96506 100644 --- a/package.json +++ b/package.json @@ -39,11 +39,11 @@ "dist:mac": "npm run build:prod && npm run clean && npm run package:mac", "dist:lin": "npm run build:prod && npm run clean && npm run package:lin", "publish:npm": "npm run build:prod && npm publish --access public", - "lint": "tslint src/**/*.ts spec/**/*.ts || true", - "lint:fix": "tslint src/**/*.ts spec/**/*.ts --fix" + "lint": "tslint 'src/**/*.ts' 'spec/**/*.ts' || true", + "lint:fix": "tslint 'src/**/*.ts' 'spec/**/*.ts' --fix" }, "bin": { - "bw": "./build/bw.js" + "bw": "build/bw.js" }, "pkg": { "assets": "./build/**/*" @@ -77,7 +77,7 @@ "big-integer": "1.6.36", "browser-hrtime": "^1.1.8", "chalk": "2.4.1", - "commander": "2.18.0", + "commander": "7.0.0", "form-data": "2.3.2", "https-proxy-agent": "5.0.0", "inquirer": "6.2.0", diff --git a/src/bw.ts b/src/bw.ts index 3352f7c..4b2d94f 100644 --- a/src/bw.ts +++ b/src/bw.ts @@ -1,3 +1,4 @@ +import * as program from 'commander'; import * as fs from 'fs'; import * as jsdom from 'jsdom'; import * as path from 'path'; @@ -39,6 +40,8 @@ import { UserService } from 'jslib/services/user.service'; import { VaultTimeoutService } from 'jslib/services/vaultTimeout.service'; import { Program } from './program'; +import { SendProgram } from './send.program'; +import { VaultProgram } from './vault.program'; // Polyfills (global as any).DOMParser = new jsdom.JSDOM().window.DOMParser; @@ -76,6 +79,8 @@ export class Main { authService: AuthService; policyService: PolicyService; program: Program; + vaultProgram: VaultProgram; + sendProgram: SendProgram; logService: ConsoleLogService; sendService: SendService; @@ -145,11 +150,24 @@ export class Main { this.vaultTimeoutService, this.logService, true); this.auditService = new AuditService(this.cryptoFunctionService, this.apiService); this.program = new Program(this); + this.vaultProgram = new VaultProgram(this); + this.sendProgram = new SendProgram(this); } async run() { await this.init(); - this.program.run(); + + this.program.register(); + this.vaultProgram.register(); + this.sendProgram.register(); + + program + .parse(process.argv); + + if (process.argv.slice(2).length === 0) { + program.outputHelp(); + } + } async logout() { diff --git a/src/commands/completion.command.ts b/src/commands/completion.command.ts index 88cbf28..9f57e0c 100644 --- a/src/commands/completion.command.ts +++ b/src/commands/completion.command.ts @@ -4,8 +4,8 @@ import { Response } from 'jslib/cli/models/response'; import { MessageResponse } from 'jslib/cli/models/response/messageResponse'; interface IOption { - long: string; - short: string; + long?: string; + short?: string; description: string; } @@ -19,8 +19,8 @@ interface ICommand { const validShells = ['zsh']; export class CompletionCommand { - async run(cmd: program.Command) { - const shell: typeof validShells[number] = cmd.shell; + async run(options: program.OptionValues) { + const shell: typeof validShells[number] = options.shell; if (!shell) { return Response.badRequest('`shell` was not provided.'); @@ -33,7 +33,7 @@ export class CompletionCommand { let content = ''; if (shell === 'zsh') { - content = this.zshCompletion('bw', cmd.parent).render(); + content = this.zshCompletion('bw', program as any as ICommand).render(); } const res = new MessageResponse(content, null); diff --git a/src/commands/config.command.ts b/src/commands/config.command.ts index 06b5d65..23d0777 100644 --- a/src/commands/config.command.ts +++ b/src/commands/config.command.ts @@ -9,20 +9,20 @@ import { StringResponse } from 'jslib/cli/models/response/stringResponse'; export class ConfigCommand { constructor(private environmentService: EnvironmentService) { } - async run(setting: string, value: string, cmd: program.Command): Promise { + async run(setting: string, value: string, options: program.OptionValues): Promise { setting = setting.toLowerCase(); switch (setting) { case 'server': - return await this.getOrSetServer(value, cmd); + return await this.getOrSetServer(value, options); default: return Response.badRequest('Unknown setting.'); } } - private async getOrSetServer(url: string, cmd: program.Command): Promise { + private async getOrSetServer(url: string, options: program.OptionValues): Promise { if ((url == null || url.trim() === '') && - !cmd.webVault && !cmd.api && !cmd.identity && !cmd.icons && !cmd.notifications && !cmd.events) { + !options.webVault && !options.api && !options.identity && !options.icons && !options.notifications && !options.events) { const baseUrl = this.environmentService.baseUrl; const stringRes = new StringResponse(baseUrl == null ? 'https://bitwarden.com' : baseUrl); return Response.success(stringRes); @@ -31,12 +31,12 @@ export class ConfigCommand { url = (url === 'null' || url === 'bitwarden.com' || url === 'https://bitwarden.com' ? null : url); await this.environmentService.setUrls({ base: url, - webVault: cmd.webVault || null, - api: cmd.api || null, - identity: cmd.identity || null, - icons: cmd.icons || null, - notifications: cmd.notifications || null, - events: cmd.events || null, + webVault: options.webVault || null, + api: options.api || null, + identity: options.identity || null, + icons: options.icons || null, + notifications: options.notifications || null, + events: options.events || null, }); const res = new MessageResponse('Saved setting `config`.', null); return Response.success(res); diff --git a/src/commands/confirm.command.ts b/src/commands/confirm.command.ts index fbff835..f3dc82a 100644 --- a/src/commands/confirm.command.ts +++ b/src/commands/confirm.command.ts @@ -25,22 +25,22 @@ export class ConfirmCommand { } } - private async confirmOrganizationMember(id: string, cmd: program.Command) { - if (cmd.organizationid == null || cmd.organizationid === '') { + private async confirmOrganizationMember(id: string, options: program.OptionValues) { + if (options.organizationid == null || options.organizationid === '') { return Response.badRequest('--organizationid required.'); } if (!Utils.isGuid(id)) { return Response.error('`' + id + '` is not a GUID.'); } - if (!Utils.isGuid(cmd.organizationid)) { - return Response.error('`' + cmd.organizationid + '` is not a GUID.'); + if (!Utils.isGuid(options.organizationid)) { + return Response.error('`' + options.organizationid + '` is not a GUID.'); } try { - const orgKey = await this.cryptoService.getOrgKey(cmd.organizationid); + const orgKey = await this.cryptoService.getOrgKey(options.organizationid); if (orgKey == null) { throw new Error('No encryption key for this organization.'); } - const orgUser = await this.apiService.getOrganizationUser(cmd.organizationid, id); + const orgUser = await this.apiService.getOrganizationUser(options.organizationid, id); if (orgUser == null) { throw new Error('Member id does not exist for this organization.'); } @@ -49,7 +49,7 @@ export class ConfirmCommand { const key = await this.cryptoService.rsaEncrypt(orgKey.key, publicKey.buffer); const req = new OrganizationUserConfirmRequest(); req.key = key.encryptedString; - await this.apiService.postOrganizationUserConfirm(cmd.organizationid, id, req); + await this.apiService.postOrganizationUserConfirm(options.organizationid, id, req); return Response.success(); } catch (e) { return Response.error(e); diff --git a/src/commands/create.command.ts b/src/commands/create.command.ts index cd5f221..dad7707 100644 --- a/src/commands/create.command.ts +++ b/src/commands/create.command.ts @@ -78,19 +78,19 @@ export class CreateCommand { } } - private async createAttachment(cmd: program.Command) { - if (cmd.itemid == null || cmd.itemid === '') { + private async createAttachment(options: program.OptionValues) { + if (options.itemid == null || options.itemid === '') { return Response.badRequest('--itemid required.'); } - if (cmd.file == null || cmd.file === '') { + if (options.file == null || options.file === '') { return Response.badRequest('--file required.'); } - const filePath = path.resolve(cmd.file); - if (!fs.existsSync(cmd.file)) { + const filePath = path.resolve(options.file); + if (!fs.existsSync(options.file)) { return Response.badRequest('Cannot find file at ' + filePath); } - const itemId = cmd.itemid.toLowerCase(); + const itemId = options.itemid.toLowerCase(); const cipher = await this.cipherService.get(itemId); if (cipher == null) { return Response.notFound(); @@ -132,14 +132,14 @@ export class CreateCommand { } } - private async createOrganizationCollection(req: OrganizationCollectionRequest, cmd: program.Command) { - if (cmd.organizationid == null || cmd.organizationid === '') { + private async createOrganizationCollection(req: OrganizationCollectionRequest, options: program.OptionValues) { + if (options.organizationid == null || options.organizationid === '') { return Response.badRequest('--organizationid required.'); } - if (!Utils.isGuid(cmd.organizationid)) { - return Response.error('`' + cmd.organizationid + '` is not a GUID.'); + if (!Utils.isGuid(options.organizationid)) { + return Response.error('`' + options.organizationid + '` is not a GUID.'); } - if (cmd.organizationid !== req.organizationId) { + if (options.organizationid !== req.organizationId) { return Response.error('--organizationid does not match request object.'); } try { diff --git a/src/commands/delete.command.ts b/src/commands/delete.command.ts index ea2a160..5200acc 100644 --- a/src/commands/delete.command.ts +++ b/src/commands/delete.command.ts @@ -32,14 +32,14 @@ export class DeleteCommand { } } - private async deleteCipher(id: string, cmd: program.Command) { + private async deleteCipher(id: string, options: program.OptionValues) { const cipher = await this.cipherService.get(id); if (cipher == null) { return Response.notFound(); } try { - if (cmd.permanent) { + if (options.permanent) { await this.cipherService.deleteWithServer(id); } else { await this.cipherService.softDeleteWithServer(id); @@ -50,12 +50,12 @@ export class DeleteCommand { } } - private async deleteAttachment(id: string, cmd: program.Command) { - if (cmd.itemid == null || cmd.itemid === '') { + private async deleteAttachment(id: string, options: program.OptionValues) { + if (options.itemid == null || options.itemid === '') { return Response.badRequest('--itemid required.'); } - const itemId = cmd.itemid.toLowerCase(); + const itemId = options.itemid.toLowerCase(); const cipher = await this.cipherService.get(itemId); if (cipher == null) { return Response.notFound(); @@ -96,18 +96,18 @@ export class DeleteCommand { } } - private async deleteOrganizationCollection(id: string, cmd: program.Command) { - if (cmd.organizationid == null || cmd.organizationid === '') { + private async deleteOrganizationCollection(id: string, options: program.OptionValues) { + if (options.organizationid == null || options.organizationid === '') { return Response.badRequest('--organizationid required.'); } if (!Utils.isGuid(id)) { return Response.error('`' + id + '` is not a GUID.'); } - if (!Utils.isGuid(cmd.organizationid)) { - return Response.error('`' + cmd.organizationid + '` is not a GUID.'); + if (!Utils.isGuid(options.organizationid)) { + return Response.error('`' + options.organizationid + '` is not a GUID.'); } try { - await this.apiService.deleteCollection(cmd.organizationid, id); + await this.apiService.deleteCollection(options.organizationid, id); return Response.success(); } catch (e) { return Response.error(e); diff --git a/src/commands/download.command.ts b/src/commands/download.command.ts new file mode 100644 index 0000000..2e00be8 --- /dev/null +++ b/src/commands/download.command.ts @@ -0,0 +1,32 @@ +import * as fet from 'node-fetch'; + +import { CryptoService } from 'jslib/abstractions/crypto.service'; + +import { SymmetricCryptoKey } from 'jslib/models/domain/symmetricCryptoKey'; + +import { Response } from 'jslib/cli/models/response'; + +import { CliUtils } from '../utils'; + +export abstract class DownloadCommand { + constructor(protected cryptoService: CryptoService) { } + + protected async saveAttachmentToFile(url: string, key: SymmetricCryptoKey, fileName: string, output?: string) { + const response = await fet.default(new fet.Request(url, { headers: { cache: 'no-cache' } })); + if (response.status !== 200) { + return Response.error('A ' + response.status + ' error occurred while downloading the attachment.'); + } + + try { + const buf = await response.arrayBuffer(); + const decBuf = await this.cryptoService.decryptFromBytes(buf, key); + return await CliUtils.saveResultToFile(Buffer.from(decBuf), output, fileName); + } catch (e) { + if (typeof (e) === 'string') { + return Response.error(e); + } else { + return Response.error('An error occurred while saving the attachment.'); + } + } + } +} diff --git a/src/commands/edit.command.ts b/src/commands/edit.command.ts index ae3b37a..ee4b53c 100644 --- a/src/commands/edit.command.ts +++ b/src/commands/edit.command.ts @@ -127,17 +127,17 @@ export class EditCommand { } } - private async editOrganizationCollection(id: string, req: OrganizationCollectionRequest, cmd: program.Command) { - if (cmd.organizationid == null || cmd.organizationid === '') { + private async editOrganizationCollection(id: string, req: OrganizationCollectionRequest, options: program.OptionValues) { + if (options.organizationid == null || options.organizationid === '') { return Response.badRequest('--organizationid required.'); } if (!Utils.isGuid(id)) { return Response.error('`' + id + '` is not a GUID.'); } - if (!Utils.isGuid(cmd.organizationid)) { - return Response.error('`' + cmd.organizationid + '` is not a GUID.'); + if (!Utils.isGuid(options.organizationid)) { + return Response.error('`' + options.organizationid + '` is not a GUID.'); } - if (cmd.organizationid !== req.organizationId) { + if (options.organizationid !== req.organizationId) { return Response.error('--organizationid does not match request object.'); } try { diff --git a/src/commands/encode.command.ts b/src/commands/encode.command.ts index bf70cd8..00788bb 100644 --- a/src/commands/encode.command.ts +++ b/src/commands/encode.command.ts @@ -6,7 +6,7 @@ import { StringResponse } from 'jslib/cli/models/response/stringResponse'; import { CliUtils } from '../utils'; export class EncodeCommand { - async run(cmd: program.Command): Promise { + async run(): Promise { if (process.stdin.isTTY) { return Response.badRequest('No stdin was piped in.'); } diff --git a/src/commands/export.command.ts b/src/commands/export.command.ts index aa43d2c..c2dc471 100644 --- a/src/commands/export.command.ts +++ b/src/commands/export.command.ts @@ -14,7 +14,7 @@ import { Utils } from 'jslib/misc/utils'; export class ExportCommand { constructor(private cryptoService: CryptoService, private exportService: ExportService) { } - async run(password: string, cmd: program.Command): Promise { + async run(password: string, options: program.OptionValues): Promise { const canInteract = process.env.BW_NOINTERACTION !== 'true'; if ((password == null || password === '') && canInteract) { const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({ @@ -31,31 +31,31 @@ export class ExportCommand { const keyHash = await this.cryptoService.hashPassword(password, null); const storedKeyHash = await this.cryptoService.getKeyHash(); if (storedKeyHash != null && keyHash != null && storedKeyHash === keyHash) { - let format = cmd.format; + let format = options.format; if (format !== 'encrypted_json' && format !== 'json') { format = 'csv'; } - if (cmd.organizationid != null && !Utils.isGuid(cmd.organizationid)) { - return Response.error('`' + cmd.organizationid + '` is not a GUID.'); + if (options.organizationid != null && !Utils.isGuid(options.organizationid)) { + return Response.error('`' + options.organizationid + '` is not a GUID.'); } let exportContent: string = null; try { - exportContent = cmd.organizationid != null ? - await this.exportService.getOrganizationExport(cmd.organizationid, format) : + exportContent = options.organizationid != null ? + await this.exportService.getOrganizationExport(options.organizationid, format) : await this.exportService.getExport(format); } catch (e) { return Response.error(e); } - return await this.saveFile(exportContent, cmd, format); + return await this.saveFile(exportContent, options, format); } else { return Response.error('Invalid master password.'); } } - async saveFile(exportContent: string, cmd: program.Command, format: string): Promise { + async saveFile(exportContent: string, options: program.OptionValues, format: string): Promise { try { - const fileName = this.exportService.getFileName(cmd.organizationid != null ? 'org' : null, format); - return await CliUtils.saveResultToFile(exportContent, cmd.output, fileName); + const fileName = this.exportService.getFileName(options.organizationid != null ? 'org' : null, format); + return await CliUtils.saveResultToFile(exportContent, options.output, fileName); } catch (e) { return Response.error(e.toString()); } diff --git a/src/commands/generate.command.ts b/src/commands/generate.command.ts index 79a3c3a..291158a 100644 --- a/src/commands/generate.command.ts +++ b/src/commands/generate.command.ts @@ -8,16 +8,16 @@ import { StringResponse } from 'jslib/cli/models/response/stringResponse'; export class GenerateCommand { constructor(private passwordGenerationService: PasswordGenerationService) { } - async run(cmd: program.Command): Promise { + async run(cmdOptions: program.OptionValues): Promise { const options = { - uppercase: cmd.uppercase || false, - lowercase: cmd.lowercase || false, - number: cmd.number || false, - special: cmd.special || false, - length: cmd.length || 14, - type: cmd.passphrase ? 'passphrase' : 'password', - wordSeparator: cmd.separator == null ? '-' : cmd.separator, - numWords: cmd.words || 3, + uppercase: cmdOptions.uppercase || false, + lowercase: cmdOptions.lowercase || false, + number: cmdOptions.number || false, + special: cmdOptions.special || false, + length: cmdOptions.length || 14, + type: cmdOptions.passphrase ? 'passphrase' : 'password', + wordSeparator: cmdOptions.separator == null ? '-' : cmdOptions.separator, + numWords: cmdOptions.words || 3, }; if (!options.uppercase && !options.lowercase && !options.special && !options.number) { options.lowercase = true; diff --git a/src/commands/get.command.ts b/src/commands/get.command.ts index f429946..dd251de 100644 --- a/src/commands/get.command.ts +++ b/src/commands/get.command.ts @@ -1,5 +1,4 @@ import * as program from 'commander'; -import * as fet from 'node-fetch'; import { CipherType } from 'jslib/enums/cipherType'; @@ -8,8 +7,10 @@ import { AuditService } from 'jslib/abstractions/audit.service'; import { CipherService } from 'jslib/abstractions/cipher.service'; import { CollectionService } from 'jslib/abstractions/collection.service'; import { CryptoService } from 'jslib/abstractions/crypto.service'; +import { EnvironmentService } from 'jslib/abstractions/environment.service'; import { FolderService } from 'jslib/abstractions/folder.service'; import { SearchService } from 'jslib/abstractions/search.service'; +import { SendService } from 'jslib/abstractions/send.service'; import { TotpService } from 'jslib/abstractions/totp.service'; import { UserService } from 'jslib/abstractions/user.service'; @@ -40,24 +41,32 @@ import { CollectionResponse } from '../models/response/collectionResponse'; import { FolderResponse } from '../models/response/folderResponse'; import { OrganizationCollectionResponse } from '../models/response/organizationCollectionResponse'; import { OrganizationResponse } from '../models/response/organizationResponse'; +import { SendFileResponse } from '../models/response/sendFileResponse'; +import { SendResponse } from '../models/response/sendResponse'; +import { SendTextResponse } from '../models/response/sendTextResponse'; import { TemplateResponse } from '../models/response/templateResponse'; import { OrganizationCollectionRequest } from '../models/request/organizationCollectionRequest'; import { SelectionReadOnly } from '../models/selectionReadOnly'; +import { DownloadCommand } from './download.command'; + import { CliUtils } from '../utils'; import { Utils } from 'jslib/misc/utils'; -export class GetCommand { +export class GetCommand extends DownloadCommand { constructor(private cipherService: CipherService, private folderService: FolderService, private collectionService: CollectionService, private totpService: TotpService, - private auditService: AuditService, private cryptoService: CryptoService, + private auditService: AuditService, cryptoService: CryptoService, private userService: UserService, private searchService: SearchService, - private apiService: ApiService) { } + private apiService: ApiService, private sendService: SendService, + private environmentService: EnvironmentService) { + super(cryptoService); + } - async run(object: string, id: string, cmd: program.Command): Promise { + async run(object: string, id: string, options: program.OptionValues): Promise { if (id != null) { id = id.toLowerCase(); } @@ -76,13 +85,13 @@ export class GetCommand { case 'exposed': return await this.getExposed(id); case 'attachment': - return await this.getAttachment(id, cmd); + return await this.getAttachment(id, options); case 'folder': return await this.getFolder(id); case 'collection': return await this.getCollection(id); case 'org-collection': - return await this.getOrganizationCollection(id, cmd); + return await this.getOrganizationCollection(id, options); case 'organization': return await this.getOrganization(id); case 'template': @@ -241,12 +250,12 @@ export class GetCommand { return Response.success(res); } - private async getAttachment(id: string, cmd: program.Command) { - if (cmd.itemid == null || cmd.itemid === '') { + private async getAttachment(id: string, options: program.OptionValues) { + if (options.itemid == null || options.itemid === '') { return Response.badRequest('--itemid required.'); } - const itemId = cmd.itemid.toLowerCase(); + const itemId = options.itemid.toLowerCase(); const cipherResponse = await this.getCipher(itemId); if (!cipherResponse.success) { return cipherResponse; @@ -273,24 +282,9 @@ export class GetCommand { } } - const response = await fet.default(new fet.Request(attachments[0].url, { headers: { cache: 'no-cache' } })); - if (response.status !== 200) { - return Response.error('A ' + response.status + ' error occurred while downloading the attachment.'); - } - - try { - const buf = await response.arrayBuffer(); - const key = attachments[0].key != null ? attachments[0].key : - await this.cryptoService.getOrgKey(cipher.organizationId); - const decBuf = await this.cryptoService.decryptFromBytes(buf, key); - return await CliUtils.saveResultToFile(Buffer.from(decBuf), cmd.output, attachments[0].fileName); - } catch (e) { - if (typeof (e) === 'string') { - return Response.error(e); - } else { - return Response.error('An error occurred while saving the attachment.'); - } - } + const key = attachments[0].key != null ? attachments[0].key : + await this.cryptoService.getOrgKey(cipher.organizationId); + return await this.saveAttachmentToFile(attachments[0].url, key, attachments[0].fileName, options.output); } private async getFolder(id: string) { @@ -343,23 +337,23 @@ export class GetCommand { return Response.success(res); } - private async getOrganizationCollection(id: string, cmd: program.Command) { - if (cmd.organizationid == null || cmd.organizationid === '') { + private async getOrganizationCollection(id: string, options: program.OptionValues) { + if (options.organizationid == null || options.organizationid === '') { return Response.badRequest('--organizationid required.'); } if (!Utils.isGuid(id)) { return Response.error('`' + id + '` is not a GUID.'); } - if (!Utils.isGuid(cmd.organizationid)) { - return Response.error('`' + cmd.organizationid + '` is not a GUID.'); + if (!Utils.isGuid(options.organizationid)) { + return Response.error('`' + options.organizationid + '` is not a GUID.'); } try { - const orgKey = await this.cryptoService.getOrgKey(cmd.organizationid); + const orgKey = await this.cryptoService.getOrgKey(options.organizationid); if (orgKey == null) { throw new Error('No encryption key for this organization.'); } - const response = await this.apiService.getCollectionDetails(cmd.organizationid, id); + const response = await this.apiService.getCollectionDetails(options.organizationid, id); const decCollection = new CollectionView(response); decCollection.name = await this.cryptoService.decryptToUtf8( new CipherString(response.name), orgKey); @@ -430,6 +424,15 @@ export class GetCommand { case 'org-collection': template = OrganizationCollectionRequest.template(); break; + case 'send': + template = SendResponse.template(); + break; + case 'send.text': + template = SendTextResponse.template(); + break; + case 'send.file': + template = SendFileResponse.template(); + break; default: return Response.badRequest('Unknown template object.'); } diff --git a/src/commands/import.command.ts b/src/commands/import.command.ts index ad4e806..e8d2e34 100644 --- a/src/commands/import.command.ts +++ b/src/commands/import.command.ts @@ -9,8 +9,8 @@ import { CliUtils } from '../utils'; export class ImportCommand { constructor(private importService: ImportService) { } - async run(format: string, filepath: string, cmd: program.Command): Promise { - if (cmd.formats || false) { + async run(format: string, filepath: string, options: program.OptionValues): Promise { + if (options.formats || false) { return this.list(); } else { return this.import(format, filepath); diff --git a/src/commands/list.command.ts b/src/commands/list.command.ts index c36fc32..5206fde 100644 --- a/src/commands/list.command.ts +++ b/src/commands/list.command.ts @@ -56,45 +56,45 @@ export class ListCommand { } } - private async listCiphers(cmd: program.Command) { + private async listCiphers(options: program.OptionValues) { let ciphers: CipherView[]; - cmd.trash = cmd.trash || false; - if (cmd.url != null && cmd.url.trim() !== '') { - ciphers = await this.cipherService.getAllDecryptedForUrl(cmd.url); + options.trash = options.trash || false; + if (options.url != null && options.url.trim() !== '') { + ciphers = await this.cipherService.getAllDecryptedForUrl(options.url); } else { ciphers = await this.cipherService.getAllDecrypted(); } - if (cmd.folderid != null || cmd.collectionid != null || cmd.organizationid != null) { + if (options.folderid != null || options.collectionid != null || options.organizationid != null) { ciphers = ciphers.filter((c) => { - if (cmd.trash !== c.isDeleted) { + if (options.trash !== c.isDeleted) { return false; } - if (cmd.folderid != null) { - if (cmd.folderid === 'notnull' && c.folderId != null) { + if (options.folderid != null) { + if (options.folderid === 'notnull' && c.folderId != null) { return true; } - const folderId = cmd.folderid === 'null' ? null : cmd.folderid; + const folderId = options.folderid === 'null' ? null : options.folderid; if (folderId === c.folderId) { return true; } } - if (cmd.organizationid != null) { - if (cmd.organizationid === 'notnull' && c.organizationId != null) { + if (options.organizationid != null) { + if (options.organizationid === 'notnull' && c.organizationId != null) { return true; } - const organizationId = cmd.organizationid === 'null' ? null : cmd.organizationid; + const organizationId = options.organizationid === 'null' ? null : options.organizationid; if (organizationId === c.organizationId) { return true; } } - if (cmd.collectionid != null) { - if (cmd.collectionid === 'notnull' && c.collectionIds != null && c.collectionIds.length > 0) { + if (options.collectionid != null) { + if (options.collectionid === 'notnull' && c.collectionIds != null && c.collectionIds.length > 0) { return true; } - const collectionId = cmd.collectionid === 'null' ? null : cmd.collectionid; + const collectionId = options.collectionid === 'null' ? null : options.collectionid; if (collectionId == null && (c.collectionIds == null || c.collectionIds.length === 0)) { return true; } @@ -104,57 +104,57 @@ export class ListCommand { } return false; }); - } else if (cmd.search == null || cmd.search.trim() === '') { - ciphers = ciphers.filter((c) => cmd.trash === c.isDeleted); + } else if (options.search == null || options.search.trim() === '') { + ciphers = ciphers.filter((c) => options.trash === c.isDeleted); } - if (cmd.search != null && cmd.search.trim() !== '') { - ciphers = this.searchService.searchCiphersBasic(ciphers, cmd.search, cmd.trash); + if (options.search != null && options.search.trim() !== '') { + ciphers = this.searchService.searchCiphersBasic(ciphers, options.search, options.trash); } const res = new ListResponse(ciphers.map((o) => new CipherResponse(o))); return Response.success(res); } - private async listFolders(cmd: program.Command) { + private async listFolders(options: program.OptionValues) { let folders = await this.folderService.getAllDecrypted(); - if (cmd.search != null && cmd.search.trim() !== '') { - folders = CliUtils.searchFolders(folders, cmd.search); + if (options.search != null && options.search.trim() !== '') { + folders = CliUtils.searchFolders(folders, options.search); } const res = new ListResponse(folders.map((o) => new FolderResponse(o))); return Response.success(res); } - private async listCollections(cmd: program.Command) { + private async listCollections(options: program.OptionValues) { let collections = await this.collectionService.getAllDecrypted(); - if (cmd.organizationid != null) { + if (options.organizationid != null) { collections = collections.filter((c) => { - if (cmd.organizationid === c.organizationId) { + if (options.organizationid === c.organizationId) { return true; } return false; }); } - if (cmd.search != null && cmd.search.trim() !== '') { - collections = CliUtils.searchCollections(collections, cmd.search); + if (options.search != null && options.search.trim() !== '') { + collections = CliUtils.searchCollections(collections, options.search); } const res = new ListResponse(collections.map((o) => new CollectionResponse(o))); return Response.success(res); } - private async listOrganizationCollections(cmd: program.Command) { - if (cmd.organizationid == null || cmd.organizationid === '') { + private async listOrganizationCollections(options: program.OptionValues) { + if (options.organizationid == null || options.organizationid === '') { return Response.badRequest('--organizationid required.'); } - if (!Utils.isGuid(cmd.organizationid)) { - return Response.error('`' + cmd.organizationid + '` is not a GUID.'); + if (!Utils.isGuid(options.organizationid)) { + return Response.error('`' + options.organizationid + '` is not a GUID.'); } - const organization = await this.userService.getOrganization(cmd.organizationid); + const organization = await this.userService.getOrganization(options.organizationid); if (organization == null) { return Response.error('Organization not found.'); } @@ -162,15 +162,15 @@ export class ListCommand { try { let response: ApiListResponse; if (organization.canManageAllCollections) { - response = await this.apiService.getCollections(cmd.organizationid); + response = await this.apiService.getCollections(options.organizationid); } else { response = await this.apiService.getUserCollections(); } - const collections = response.data.filter((c) => c.organizationId === cmd.organizationid).map((r) => + const collections = response.data.filter((c) => c.organizationId === options.organizationid).map((r) => new Collection(new CollectionData(r as ApiCollectionDetailsResponse))); let decCollections = await this.collectionService.decryptMany(collections); - if (cmd.search != null && cmd.search.trim() !== '') { - decCollections = CliUtils.searchCollections(decCollections, cmd.search); + if (options.search != null && options.search.trim() !== '') { + decCollections = CliUtils.searchCollections(decCollections, options.search); } const res = new ListResponse(decCollections.map((o) => new CollectionResponse(o))); return Response.success(res); @@ -179,20 +179,20 @@ export class ListCommand { } } - private async listOrganizationMembers(cmd: program.Command) { - if (cmd.organizationid == null || cmd.organizationid === '') { + private async listOrganizationMembers(options: program.OptionValues) { + if (options.organizationid == null || options.organizationid === '') { return Response.badRequest('--organizationid required.'); } - if (!Utils.isGuid(cmd.organizationid)) { - return Response.error('`' + cmd.organizationid + '` is not a GUID.'); + if (!Utils.isGuid(options.organizationid)) { + return Response.error('`' + options.organizationid + '` is not a GUID.'); } - const organization = await this.userService.getOrganization(cmd.organizationid); + const organization = await this.userService.getOrganization(options.organizationid); if (organization == null) { return Response.error('Organization not found.'); } try { - const response = await this.apiService.getOrganizationUsers(cmd.organizationid); + const response = await this.apiService.getOrganizationUsers(options.organizationid); const res = new ListResponse(response.data.map((r) => { const u = new OrganizationUserResponse(); u.email = r.email; @@ -209,11 +209,11 @@ export class ListCommand { } } - private async listOrganizations(cmd: program.Command) { + private async listOrganizations(options: program.OptionValues) { let organizations = await this.userService.getAllOrganizations(); - if (cmd.search != null && cmd.search.trim() !== '') { - organizations = CliUtils.searchOrganizations(organizations, cmd.search); + if (options.search != null && options.search.trim() !== '') { + organizations = CliUtils.searchOrganizations(organizations, options.search); } const res = new ListResponse(organizations.map((o) => new OrganizationResponse(o))); diff --git a/src/commands/login.command.ts b/src/commands/login.command.ts index d9a14b9..cc9dc17 100644 --- a/src/commands/login.command.ts +++ b/src/commands/login.command.ts @@ -16,7 +16,7 @@ import { Utils } from 'jslib/misc/utils'; import { LoginCommand as BaseLoginCommand } from 'jslib/cli/commands/login.command'; export class LoginCommand extends BaseLoginCommand { - private cmd: program.Command; + private options: program.OptionValues; constructor(authService: AuthService, apiService: ApiService, cryptoFunctionService: CryptoFunctionService, syncService: SyncService, @@ -30,7 +30,7 @@ export class LoginCommand extends BaseLoginCommand { }; this.success = async () => { await syncService.fullSync(true); - if ((this.cmd.sso != null || this.cmd.apikey != null) && this.canInteract) { + if ((this.options.sso != null || this.options.apikey != null) && this.canInteract) { const res = new MessageResponse('You are logged in!', '\n' + 'To unlock your vault, use the `unlock` command. ex:\n' + '$ bw unlock'); @@ -48,8 +48,8 @@ export class LoginCommand extends BaseLoginCommand { }; } - run(email: string, password: string, cmd: program.Command) { - this.cmd = cmd; - return super.run(email, password, cmd); + run(email: string, password: string, options: program.OptionValues) { + this.options = options; + return super.run(email, password, options); } } diff --git a/src/commands/send/create.command.ts b/src/commands/send/create.command.ts new file mode 100644 index 0000000..871e47f --- /dev/null +++ b/src/commands/send/create.command.ts @@ -0,0 +1,112 @@ +import * as program from 'commander'; +import * as fs from 'fs'; +import * as path from 'path'; + +import { EnvironmentService } from 'jslib/abstractions/environment.service'; +import { SendService } from 'jslib/abstractions/send.service'; +import { UserService } from 'jslib/abstractions/user.service'; + +import { SendType } from 'jslib/enums/sendType'; + +import { NodeUtils } from 'jslib/misc/nodeUtils'; + +import { Response } from 'jslib/cli/models/response'; +import { StringResponse } from 'jslib/cli/models/response/stringResponse'; + +import { SendResponse } from '../../models/response/sendResponse'; +import { SendTextResponse } from '../../models/response/sendTextResponse'; + +import { CliUtils } from '../../utils'; + +export class SendCreateCommand { + constructor(private sendService: SendService, private userService: UserService, + private environmentService: EnvironmentService) { } + + async run(requestJson: string, options: program.OptionValues) { + let req: any = null; + if (requestJson == null || requestJson === '') { + requestJson = await CliUtils.readStdin(); + } + + if (requestJson == null || requestJson === '') { + return Response.badRequest('`requestJson` was not provided.'); + } + + try { + const reqJson = Buffer.from(requestJson, 'base64').toString(); + req = SendResponse.fromJson(reqJson); + + if (req == null) { + throw new Error('Null request'); + } + } catch (e) { + return Response.badRequest('Error parsing the encoded request data.'); + } + + if (req.deletionDate == null || isNaN(new Date(req.deletionDate).getTime()) || + new Date(req.deletionDate) <= new Date()) { + return Response.badRequest('Must specify a valid deletion date after the current time'); + } + + if (req.expirationDate != null && isNaN(new Date(req.expirationDate).getTime())) { + return Response.badRequest('Unable to parse expirationDate: ' + req.expirationDate); + } + + return this.createSend(req, options); + } + + private async createSend(req: SendResponse, options: program.OptionValues) { + const filePath = req.file?.fileName ?? options.file; + const text = req.text?.text ?? options.text; + const hidden = req.text?.hidden ?? options.hidden; + const password = req.password ?? options.password; + + req.key = null; + + switch (req.type) { + case SendType.File: + if (!(await this.userService.canAccessPremium())) { + return Response.error('Premium status is required to use this feature.'); + } + + if (filePath == null) { + return Response.badRequest('Must specify a file to Send either with the --file option or in the encoded json'); + } + + req.file.fileName = path.basename(filePath) + break; + case SendType.Text: + if (text == null) { + return Response.badRequest('Must specify text content to Send either with the --text option or in the encoded json'); + } + req.text = new SendTextResponse(); + req.text.text = text; + req.text.hidden = hidden; + break; + default: + return Response.badRequest('Unknown Send type ' + SendType[req.type] + 'valid types are: file, text'); + } + + try { + let fileBuffer: ArrayBuffer = null; + if (req.type === SendType.File) { + fileBuffer = NodeUtils.bufferToArrayBuffer(fs.readFileSync(filePath)); + } + + const sendView = SendResponse.toView(req); + const [encSend, fileData] = await this.sendService.encrypt(sendView, fileBuffer, password); + // Add dates from template + encSend.deletionDate = sendView.deletionDate; + encSend.expirationDate = sendView.expirationDate; + + await this.sendService.saveWithServer([encSend, fileData]); + const newSend = await this.sendService.get(encSend.id); + const decSend = await newSend.decrypt(); + const res = new SendResponse(decSend, this.environmentService.getWebVaultUrl()); + return Response.success(options.fullObject ? res : + new StringResponse('Send created! It can be accessed at:\n' + res.accessUrl)); + } catch (e) { + return Response.error(e); + } + } +} diff --git a/src/commands/send/delete.command.ts b/src/commands/send/delete.command.ts new file mode 100644 index 0000000..d727ad4 --- /dev/null +++ b/src/commands/send/delete.command.ts @@ -0,0 +1,22 @@ +import { SendService } from 'jslib/abstractions/send.service'; + +import { Response } from 'jslib/cli/models/response'; + +export class SendDeleteCommand { + constructor(private sendService: SendService) { } + + async run(id: string) { + const send = await this.sendService.get(id); + + if (send == null) { + return Response.notFound(); + } + + try { + this.sendService.deleteWithServer(id); + return Response.success(); + } catch (e) { + return Response.error(e); + } + } +} diff --git a/src/commands/send/edit.command.ts b/src/commands/send/edit.command.ts new file mode 100644 index 0000000..0947700 --- /dev/null +++ b/src/commands/send/edit.command.ts @@ -0,0 +1,75 @@ +import * as program from 'commander'; + +import { SendService } from 'jslib/abstractions/send.service'; +import { UserService } from 'jslib/abstractions/user.service'; + +import { Response } from 'jslib/cli/models/response'; +import { SendType } from 'jslib/enums/sendType'; + +import { SendResponse } from '../../models/response/sendResponse'; + +import { CliUtils } from '../../utils'; + +export class SendEditCommand { + constructor(private sendService: SendService, private userService: UserService) { } + + async run(encodedJson: string, options: program.OptionValues): Promise { + if (encodedJson == null || encodedJson === '') { + encodedJson = await CliUtils.readStdin(); + } + + if (encodedJson == null || encodedJson === '') { + return Response.badRequest('`encodedJson` was not provided.'); + } + + let req: SendResponse = null; + try { + const reqJson = Buffer.from(encodedJson, 'base64').toString(); + req = SendResponse.fromJson(reqJson); + } catch (e) { + return Response.badRequest('Error parsing the encoded request data.'); + } + + req.id = options.itemid || req.id; + + if (req.id != null) { + req.id = req.id.toLowerCase(); + } + + const send = await this.sendService.get(req.id); + + if (send == null) { + return Response.notFound(); + } + + if (send.type !== req.type) { + return Response.badRequest('Cannot change a Send\'s type'); + } + + if (send.type === SendType.File && !(await this.userService.canAccessPremium())) { + return Response.error('Premium status is required to use this feature.'); + } + + let sendView = await send.decrypt(); + sendView = SendResponse.toView(req, sendView); + + if (typeof (req.password) !== 'string' || req.password === '') { + req.password = null; + } + + try { + const [encSend, encFileData] = await this.sendService.encrypt(sendView, null, req.password); + // Add dates from template + encSend.deletionDate = sendView.deletionDate; + encSend.expirationDate = sendView.expirationDate; + + await this.sendService.saveWithServer([encSend, encFileData]); + const updatedSend = await this.sendService.get(send.id); + const decSend = await updatedSend.decrypt(); + const res = new SendResponse(decSend); + return Response.success(res); + } catch (e) { + return Response.error(e); + } + } +} diff --git a/src/commands/send/get.command.ts b/src/commands/send/get.command.ts new file mode 100644 index 0000000..bad4409 --- /dev/null +++ b/src/commands/send/get.command.ts @@ -0,0 +1,84 @@ +import * as program from 'commander'; + +import { CryptoService } from 'jslib/abstractions/crypto.service'; +import { EnvironmentService } from 'jslib/abstractions/environment.service'; +import { SearchService } from 'jslib/abstractions/search.service'; +import { SendService } from 'jslib/abstractions/send.service'; + +import { SendView } from 'jslib/models/view/sendView'; + +import { Response } from 'jslib/cli/models/response'; + +import { DownloadCommand } from '../download.command'; + +import { SendResponse } from '../../models/response/sendResponse'; + +import { Utils } from 'jslib/misc/utils'; + +export class SendGetCommand extends DownloadCommand { + constructor(private sendService: SendService, private environmentService: EnvironmentService, + private searchService: SearchService, cryptoService: CryptoService) { + super(cryptoService); + } + + async run(id: string, options: program.OptionValues) { + let sends = await this.getSendView(id); + if (sends == null) { + return Response.notFound(); + } + + const webVaultUrl = this.environmentService.getWebVaultUrl(); + let filter = (s: SendView) => true; + let selector = async (s: SendView): Promise => Response.success(new SendResponse(s, webVaultUrl)); + if (options.text != null) { + filter = s => { + return filter(s) && s.text != null; + }; + selector = async s => { + // Write to stdout and response success so we get the text string only to stdout + process.stdout.write(s.text.text); + return Response.success(); + }; + } + if (options.file != null) { + filter = s => { + return filter(s) && s.file != null && s.file.url != null; + }; + selector = async s => await this.saveAttachmentToFile(s.file.url, s.cryptoKey, s.file.fileName, options.output); + } + + if (Array.isArray(sends)) { + if (filter != null) { + sends = sends.filter(filter); + } + if (sends.length > 1) { + return Response.multipleResults(sends.map(s => s.id)); + } + if (sends.length > 0) { + return selector(sends[0]); + } + else { + return Response.notFound(); + } + } + + return selector(sends); + } + + private async getSendView(id: string): Promise { + if (Utils.isGuid(id)) { + const send = await this.sendService.get(id); + if (send != null) { + return await send.decrypt(); + } + } else if (id.trim() !== '') { + let sends = await this.sendService.getAllDecrypted(); + sends = this.searchService.searchSends(sends, id); + if (sends.length > 1) { + return sends; + } else if (sends.length > 0) { + return sends[0]; + } + } + } +} diff --git a/src/commands/send/list.command.ts b/src/commands/send/list.command.ts new file mode 100644 index 0000000..1e00fb9 --- /dev/null +++ b/src/commands/send/list.command.ts @@ -0,0 +1,28 @@ +import * as program from 'commander'; + +import { EnvironmentService } from 'jslib/abstractions/environment.service'; +import { SearchService } from 'jslib/abstractions/search.service'; +import { SendService } from 'jslib/abstractions/send.service'; + +import { Response } from 'jslib/cli/models/response'; +import { ListResponse } from 'jslib/cli/models/response/listResponse'; + +import { SendResponse } from '../..//models/response/sendResponse'; + +export class SendListCommand { + + constructor(private sendService: SendService, private environmentService: EnvironmentService, + private searchService: SearchService) { } + + async run(options: program.OptionValues): Promise { + let sends = await this.sendService.getAllDecrypted(); + + if (options.search != null && options.search.trim() !== '') { + sends = this.searchService.searchSends(sends, options.search); + } + + const webVaultUrl = this.environmentService.getWebVaultUrl(); + const res = new ListResponse(sends.map(s => new SendResponse(s, webVaultUrl))); + return Response.success(res); + } +} diff --git a/src/commands/send/receive.command.ts b/src/commands/send/receive.command.ts new file mode 100644 index 0000000..a85de76 --- /dev/null +++ b/src/commands/send/receive.command.ts @@ -0,0 +1,147 @@ +import * as program from 'commander'; +import * as inquirer from 'inquirer'; + +import { ApiService } from 'jslib/abstractions/api.service'; +import { CryptoService } from 'jslib/abstractions/crypto.service'; +import { CryptoFunctionService } from 'jslib/abstractions/cryptoFunction.service'; +import { EnvironmentService } from 'jslib/abstractions/environment.service'; +import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service'; + +import { SendAccessRequest } from 'jslib/models/request/sendAccessRequest'; +import { ErrorResponse } from 'jslib/models/response/errorResponse'; +import { SendAccessView } from 'jslib/models/view/sendAccessView'; + +import { Response } from 'jslib/cli/models/response'; + +import { SendAccess } from 'jslib/models/domain/sendAccess'; +import { SymmetricCryptoKey } from 'jslib/models/domain/symmetricCryptoKey'; + +import { SendType } from 'jslib/enums/sendType'; + +import { NodeUtils } from 'jslib/misc/nodeUtils'; +import { Utils } from 'jslib/misc/utils'; + +import { SendAccessResponse } from '../../models/response/sendAccessResponse'; + +import { DownloadCommand } from '../download.command'; + +export class SendReceiveCommand extends DownloadCommand { + private canInteract: boolean; + private decKey: SymmetricCryptoKey; + + constructor(private apiService: ApiService, cryptoService: CryptoService, + private cryptoFunctionService: CryptoFunctionService, private platformUtilsService: PlatformUtilsService, + private environmentService: EnvironmentService) { + super(cryptoService); + } + + async run(url: string, options: program.OptionValues): Promise { + this.canInteract = process.env.BW_NOINTERACTION !== 'true'; + + let urlObject: URL; + try { + urlObject = new URL(url); + } catch (e) { + return Response.badRequest('Failed to parse the provided Send url'); + } + + const apiUrl = this.getApiUrl(urlObject); + const [id, key] = this.getIdAndKey(urlObject); + + if (Utils.isNullOrWhitespace(id) || Utils.isNullOrWhitespace(key)) { + return Response.badRequest('Failed to parse url, the url provided is not a valid Send url'); + } + + const keyArray = Utils.fromUrlB64ToArray(key); + const request = new SendAccessRequest(); + + let password = options.password; + if (password == null || password === '') { + if (options.passwordfile) { + password = await NodeUtils.readFirstLine(options.passwordfile); + } else if (options.passwordenv && process.env[options.passwordenv]) { + password = process.env[options.passwordenv]; + } + } + + if (password != null && password !== '') { + request.password = await this.getUnlockedPassword(password, keyArray); + } + + const response = await this.sendRequest(request, apiUrl, id, keyArray); + + if (response instanceof Response) { + // Error scenario + return response; + } + + if (options.obj != null) { + return Response.success(new SendAccessResponse(response)); + } + + switch (response.type) { + case SendType.Text: + // Write to stdout and response success so we get the text string only to stdout + process.stdout.write(response?.text?.text); + return Response.success(); + case SendType.File: + return await this.saveAttachmentToFile(response?.file?.url, this.decKey, response?.file?.fileName, options.output); + default: + return Response.success(new SendAccessResponse(response)); + } + } + + private getIdAndKey(url: URL): [string, string] { + const result = url.hash.split('/').slice(2); + return [result[0], result[1]]; + } + + private getApiUrl(url: URL) { + if (url.origin === this.apiService.apiBaseUrl) { + return url.origin; + } else if (this.platformUtilsService.isDev() && url.origin === this.environmentService.getWebVaultUrl()) { + return this.apiService.apiBaseUrl; + } else { + return url.origin + '/api'; + } + } + + private async getUnlockedPassword(password: string, keyArray: ArrayBuffer) { + const passwordHash = await this.cryptoFunctionService.pbkdf2(password, keyArray, 'sha256', 100000); + return Utils.fromBufferToB64(passwordHash); + } + + private async sendRequest(request: SendAccessRequest, url: string, id: string, key: ArrayBuffer): Promise { + try { + const sendResponse = await this.apiService.postSendAccess(id, request, url); + + const sendAccess = new SendAccess(sendResponse); + this.decKey = await this.cryptoService.makeSendKey(key); + return await sendAccess.decrypt(this.decKey); + } catch (e) { + if (e instanceof ErrorResponse) { + if (e.statusCode === 401) { + if (this.canInteract) { + const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({ + type: 'password', + name: 'password', + message: 'Send password:', + }); + + // reattempt with new password + request.password = await this.getUnlockedPassword(answer.password, key); + return await this.sendRequest(request, url, id, key); + } + + return Response.badRequest('Incorrect or missing password'); + } else if (e.statusCode === 405) { + return Response.badRequest('Bad Request'); + } else if (e.statusCode === 404) { + return Response.notFound(); + } else { + return Response.error(e); + } + } + } + } +} diff --git a/src/commands/send/removePassword.command.ts b/src/commands/send/removePassword.command.ts new file mode 100644 index 0000000..f10ea3a --- /dev/null +++ b/src/commands/send/removePassword.command.ts @@ -0,0 +1,22 @@ +import { SendService } from 'jslib/abstractions/send.service'; + +import { Response } from 'jslib/cli/models/response'; + +import { SendResponse } from '../../models/response/sendResponse'; + +export class SendRemovePasswordCommand { + constructor(private sendService: SendService) { } + + async run(id: string) { + try { + await this.sendService.removePasswordWithServer(id); + + const updatedSend = await this.sendService.get(id); + const decSend = await updatedSend.decrypt(); + const res = new SendResponse(decSend); + return Response.success(res); + } catch (e) { + return Response.error(e); + } + } +} diff --git a/src/commands/status.command.ts b/src/commands/status.command.ts index 1f415bc..dbd7d12 100644 --- a/src/commands/status.command.ts +++ b/src/commands/status.command.ts @@ -14,7 +14,7 @@ export class StatusCommand { private userService: UserService, private vaultTimeoutService: VaultTimeoutService) { } - async run(cmd: program.Command): Promise { + async run(): Promise { try { const baseUrl = this.baseUrl(); const status = await this.status(); diff --git a/src/commands/sync.command.ts b/src/commands/sync.command.ts index 8a4006f..c231b1e 100644 --- a/src/commands/sync.command.ts +++ b/src/commands/sync.command.ts @@ -9,13 +9,13 @@ import { StringResponse } from 'jslib/cli/models/response/stringResponse'; export class SyncCommand { constructor(private syncService: SyncService) { } - async run(cmd: program.Command): Promise { - if (cmd.last || false) { + async run(options: program.OptionValues): Promise { + if (options.last || false) { return await this.getLastSync(); } try { - const result = await this.syncService.fullSync(cmd.force || false, true); + const result = await this.syncService.fullSync(options.force || false, true); const res = new MessageResponse('Syncing complete.', null); return Response.success(res); } catch (e) { diff --git a/src/models/response/SendAccessResponse.ts b/src/models/response/SendAccessResponse.ts new file mode 100644 index 0000000..15e5307 --- /dev/null +++ b/src/models/response/SendAccessResponse.ts @@ -0,0 +1,42 @@ +import { SendType } from 'jslib/enums/sendType'; + +import { SendAccessView } from 'jslib/models/view/sendAccessView'; + +import { BaseResponse } from 'jslib/cli/models/response/baseResponse'; + +import { SendFileResponse } from './sendFileResponse'; +import { SendTextResponse } from './sendTextResponse'; + +export class SendAccessResponse implements BaseResponse { + static template(): SendAccessResponse { + const req = new SendAccessResponse(); + req.name = 'Send name'; + req.type = SendType.Text; + req.text = null; + req.file = null; + return req; + } + + object = 'send-access'; + id: string; + name: string; + type: SendType; + text: SendTextResponse; + file: SendFileResponse; + + constructor(o?: SendAccessView) { + if (o == null) { + return; + } + this.id = o.id; + this.name = o.name; + this.type = o.type; + + if (o.type === SendType.Text && o.text != null) { + this.text = new SendTextResponse(o.text); + } + if (o.type === SendType.File && o.file != null) { + this.file = new SendFileResponse(o.file); + } + } +} diff --git a/src/models/response/sendFileResponse.ts b/src/models/response/sendFileResponse.ts new file mode 100644 index 0000000..2a4880c --- /dev/null +++ b/src/models/response/sendFileResponse.ts @@ -0,0 +1,39 @@ +import { SendFileView } from 'jslib/models/view/sendFileView'; + +export class SendFileResponse { + static template(fileName = 'file attachment location'): SendFileResponse { + const req = new SendFileResponse(); + req.fileName = fileName; + return req; + } + + static toView(file: SendFileResponse, view = new SendFileView()) { + if (file == null) { + return null; + } + + view.id = file.id; + view.url = file.url; + view.size = file.size; + view.sizeName = file.sizeName; + view.fileName = file.fileName; + return view; + } + + id: string; + url: string; + size: string; + sizeName: string; + fileName: string; + + constructor(o?: SendFileView) { + if (o == null) { + return; + } + this.id = o.id; + this.url = o.url; + this.size = o.size; + this.sizeName = o.sizeName; + this.fileName = o.fileName; + } +} diff --git a/src/models/response/sendResponse.ts b/src/models/response/sendResponse.ts new file mode 100644 index 0000000..29fe548 --- /dev/null +++ b/src/models/response/sendResponse.ts @@ -0,0 +1,115 @@ +import { SendView } from 'jslib/models/view/sendView'; + +import { BaseResponse } from 'jslib/cli/models/response/baseResponse'; + +import { SendType } from 'jslib/enums/sendType'; + +import { Utils } from 'jslib/misc/utils'; + +import { SendFileResponse } from './sendFileResponse'; +import { SendTextResponse } from './sendTextResponse'; + + +const dateProperties: string[] = [Utils.nameOf('deletionDate'), Utils.nameOf('expirationDate')]; + +export class SendResponse implements BaseResponse { + + static template(deleteInDays = 7): SendResponse { + const req = new SendResponse(); + req.name = 'Send name'; + req.notes = 'Some notes about this send.'; + req.type = SendType.Text; + req.text = null; + req.file = null; + req.maxAccessCount = null; + req.deletionDate = this.getStandardDeletionDate(deleteInDays); + req.expirationDate = null; + req.password = null; + req.disabled = false; + return req; + } + + static toView(send: SendResponse, view = new SendView()): SendView { + if (send == null) { + return null; + } + + view.id = send.id; + view.accessId = send.accessId; + view.name = send.name; + view.notes = send.notes; + view.key = send.key == null ? null : Utils.fromB64ToArray(send.key); + view.type = send.type; + view.file = SendFileResponse.toView(send.file); + view.text = SendTextResponse.toView(send.text); + view.maxAccessCount = send.maxAccessCount; + view.accessCount = send.accessCount; + view.revisionDate = send.revisionDate; + view.deletionDate = send.deletionDate; + view.expirationDate = send.expirationDate; + view.password = send.password; + view.disabled = send.disabled; + return view; + } + + static fromJson(json: string) { + return JSON.parse(json, (key, value) => { + if (dateProperties.includes(key)) { + return value == null ? null : new Date(value); + } + return value; + }); + } + + private static getStandardDeletionDate(days: number) { + const d = new Date(); + d.setHours(d.getHours() + (days * 24)); + return d; + } + + object = 'send'; + id: string; + accessId: string; + accessUrl: string; + name: string; + notes: string; + key: string; + type: SendType; + text: SendTextResponse; + file: SendFileResponse; + maxAccessCount?: number; + accessCount: number; + revisionDate: Date; + deletionDate: Date; + expirationDate: Date; + password: string; + passwordSet: boolean; + disabled: boolean; + + constructor(o?: SendView, webVaultUrl?: string) { + if (o == null) { + return; + } + this.id = o.id; + this.accessId = o.accessId; + this.accessUrl = (webVaultUrl ?? 'https://vault.bitwarden.com') + '/#/send/' + this.accessId + '/' + o.urlB64Key + this.name = o.name; + this.notes = o.notes; + this.key = Utils.fromBufferToB64(o.key); + this.type = o.type; + this.maxAccessCount = o.maxAccessCount; + this.accessCount = o.accessCount; + this.revisionDate = o.revisionDate; + this.deletionDate = o.deletionDate; + this.expirationDate = o.expirationDate; + this.passwordSet = o.password != null; + this.disabled = o.disabled; + + if (o.type === SendType.Text && o.text != null) { + this.text = new SendTextResponse(o.text); + } + if (o.type === SendType.File && o.file != null) { + this.file = new SendFileResponse(o.file); + } + } +} diff --git a/src/models/response/sendTextResponse.ts b/src/models/response/sendTextResponse.ts new file mode 100644 index 0000000..5798d80 --- /dev/null +++ b/src/models/response/sendTextResponse.ts @@ -0,0 +1,30 @@ +import { SendTextView } from 'jslib/models/view/sendTextView'; + +export class SendTextResponse { + static template(text = 'Text contained in the send.', hidden = false): SendTextResponse { + const req = new SendTextResponse(); + req.text = text; + req.hidden = hidden; + return req; + } + + static toView(text: SendTextResponse, view = new SendTextView()) { + if (text == null) { + return null; + } + + view.text = text.text; + view.hidden = text.hidden; + return view; + } + text: string; + hidden: boolean; + + constructor(o?: SendTextView) { + if (o == null) { + return; + } + this.text = o.text; + this.hidden = o.hidden; + } +} diff --git a/src/program.ts b/src/program.ts index 2b0a27d..e6435e3 100644 --- a/src/program.ts +++ b/src/program.ts @@ -4,20 +4,10 @@ import * as program from 'commander'; import { Main } from './bw'; import { ConfigCommand } from './commands/config.command'; -import { ConfirmCommand } from './commands/confirm.command'; -import { CreateCommand } from './commands/create.command'; -import { DeleteCommand } from './commands/delete.command'; -import { EditCommand } from './commands/edit.command'; import { EncodeCommand } from './commands/encode.command'; -import { ExportCommand } from './commands/export.command'; import { GenerateCommand } from './commands/generate.command'; -import { GetCommand } from './commands/get.command'; -import { ImportCommand } from './commands/import.command'; -import { ListCommand } from './commands/list.command'; import { LockCommand } from './commands/lock.command'; import { LoginCommand } from './commands/login.command'; -import { RestoreCommand } from './commands/restore.command'; -import { ShareCommand } from './commands/share.command'; import { StatusCommand } from './commands/status.command'; import { SyncCommand } from './commands/sync.command'; import { UnlockCommand } from './commands/unlock.command'; @@ -39,11 +29,11 @@ const chalk = chk.default; const writeLn = CliUtils.writeLn; export class Program extends BaseProgram { - constructor(private main: Main) { + constructor(protected main: Main) { super(main.userService, writeLn); } - run() { + register() { program .option('--pretty', 'Format output. JSON is tabbed with two spaces.') .option('--raw', 'Return raw output instead of a descriptive message.') @@ -99,6 +89,10 @@ export class Program extends BaseProgram { writeLn(' bw delete item 99ee88d2-6046-4ea7-92c2-acac464b1412'); writeLn(' bw generate -lusn --length 18'); writeLn(' bw config server https://bitwarden.example.com'); + writeLn(' bw send -f ./file.ext'); + writeLn(' bw send "text to send"'); + writeLn(' echo "text to send" | bw send'); + writeLn(' bw receive https://vault.bitwarden.com/#/send/rg3iuoS_Akm2gqy6ADRHmg/Ht7dYjsqjmgqUM3rjzZDSQ'); writeLn('', true); }); @@ -109,6 +103,8 @@ export class Program extends BaseProgram { .option('--code ', 'Two-step login code.') .option('--sso', 'Log in with Single-Sign On.') .option('--apikey', 'Log in with an Api Key.') + .option('--passwordenv ', 'Environment variable storing your password') + .option('--passwordfile ', 'Path to a file containing your password as its first line') .option('--check', 'Check login status.', async () => { const authed = await this.main.userService.isAuthenticated(); if (authed) { @@ -132,14 +128,14 @@ export class Program extends BaseProgram { writeLn(' bw login --sso'); writeLn('', true); }) - .action(async (email: string, password: string, cmd: program.Command) => { - if (!cmd.check) { + .action(async (email: string, password: string, options: program.OptionValues) => { + if (!options.check) { await this.exitIfAuthed(); const command = new LoginCommand(this.main.authService, this.main.apiService, this.main.cryptoFunctionService, this.main.syncService, this.main.i18nService, this.main.environmentService, this.main.passwordGenerationService, this.main.platformUtilsService); - const response = await command.run(email, password, cmd); + const response = await command.run(email, password, options); this.processResponse(response); } }); @@ -232,343 +228,6 @@ export class Program extends BaseProgram { this.processResponse(response); }); - program - .command('list ') - .description('List an array of objects from the vault.') - .option('--search ', 'Perform a search on the listed objects.') - .option('--url ', 'Filter items of type login with a url-match search.') - .option('--folderid ', 'Filter items by folder id.') - .option('--collectionid ', 'Filter items by collection id.') - .option('--organizationid ', 'Filter items or collections by organization id.') - .option('--trash', 'Filter items that are deleted and in the trash.') - .on('--help', () => { - writeLn('\n Objects:'); - writeLn(''); - writeLn(' items'); - writeLn(' folders'); - writeLn(' collections'); - writeLn(' organizations'); - writeLn(' org-collections'); - writeLn(' org-members'); - writeLn(''); - writeLn(' Notes:'); - writeLn(''); - writeLn(' Combining search with a filter performs a logical AND operation.'); - writeLn(''); - writeLn(' Combining multiple filters performs a logical OR operation.'); - writeLn(''); - writeLn(' Examples:'); - writeLn(''); - writeLn(' bw list items'); - writeLn(' bw list items --folderid 60556c31-e649-4b5d-8daf-fc1c391a1bf2'); - writeLn(' bw list items --search google --folderid 60556c31-e649-4b5d-8daf-fc1c391a1bf2'); - writeLn(' bw list items --url https://google.com'); - writeLn(' bw list items --folderid null'); - writeLn(' bw list items --organizationid notnull'); - writeLn(' bw list items --folderid 60556c31-e649-4b5d-8daf-fc1c391a1bf2 --organizationid notnull'); - writeLn(' bw list items --trash'); - writeLn(' bw list folders --search email'); - writeLn(' bw list org-members --organizationid 60556c31-e649-4b5d-8daf-fc1c391a1bf2'); - writeLn('', true); - }) - .action(async (object, cmd) => { - await this.exitIfLocked(); - const command = new ListCommand(this.main.cipherService, this.main.folderService, - this.main.collectionService, this.main.userService, this.main.searchService, this.main.apiService); - const response = await command.run(object, cmd); - this.processResponse(response); - }); - - program - .command('get ') - .description('Get an object from the vault.') - .option('--itemid ', 'Attachment\'s item id.') - .option('--output ', 'Output directory or filename for attachment.') - .option('--organizationid ', 'Organization id for an organization object.') - .on('--help', () => { - writeLn('\n Objects:'); - writeLn(''); - writeLn(' item'); - writeLn(' username'); - writeLn(' password'); - writeLn(' uri'); - writeLn(' totp'); - writeLn(' exposed'); - writeLn(' attachment'); - writeLn(' folder'); - writeLn(' collection'); - writeLn(' org-collection'); - writeLn(' organization'); - writeLn(' template'); - writeLn(' fingerprint'); - writeLn(''); - writeLn(' Id:'); - writeLn(''); - writeLn(' Search term or object\'s globally unique `id`.'); - writeLn(''); - writeLn(' If raw output is specified and no output filename or directory is given for'); - writeLn(' an attachment query, the attachment content is written to stdout.'); - writeLn(''); - writeLn(' Examples:'); - writeLn(''); - writeLn(' bw get item 99ee88d2-6046-4ea7-92c2-acac464b1412'); - writeLn(' bw get password https://google.com'); - writeLn(' bw get totp google.com'); - writeLn(' bw get exposed yahoo.com'); - writeLn(' bw get attachment b857igwl1dzrs2 --itemid 99ee88d2-6046-4ea7-92c2-acac464b1412 ' + - '--output ./photo.jpg'); - writeLn(' bw get attachment photo.jpg --itemid 99ee88d2-6046-4ea7-92c2-acac464b1412 --raw'); - writeLn(' bw get folder email'); - writeLn(' bw get template folder'); - writeLn('', true); - }) - .action(async (object, id, cmd) => { - await this.exitIfLocked(); - const command = new GetCommand(this.main.cipherService, this.main.folderService, - this.main.collectionService, this.main.totpService, this.main.auditService, - this.main.cryptoService, this.main.userService, this.main.searchService, - this.main.apiService); - const response = await command.run(object, id, cmd); - this.processResponse(response); - }); - - program - .command('create [encodedJson]') - .option('--file ', 'Path to file for attachment.') - .option('--itemid ', 'Attachment\'s item id.') - .option('--organizationid ', 'Organization id for an organization object.') - .description('Create an object in the vault.') - .on('--help', () => { - writeLn('\n Objects:'); - writeLn(''); - writeLn(' item'); - writeLn(' attachment'); - writeLn(' folder'); - writeLn(' org-collection'); - writeLn(''); - writeLn(' Notes:'); - writeLn(''); - writeLn(' `encodedJson` can also be piped into stdin.'); - writeLn(''); - writeLn(' Examples:'); - writeLn(''); - writeLn(' bw create folder eyJuYW1lIjoiTXkgRm9sZGVyIn0K'); - writeLn(' echo \'eyJuYW1lIjoiTXkgRm9sZGVyIn0K\' | bw create folder'); - writeLn(' bw create attachment --file ./myfile.csv ' + - '--itemid 16b15b89-65b3-4639-ad2a-95052a6d8f66'); - writeLn('', true); - }) - .action(async (object, encodedJson, cmd) => { - await this.exitIfLocked(); - const command = new CreateCommand(this.main.cipherService, this.main.folderService, - this.main.userService, this.main.cryptoService, this.main.apiService); - const response = await command.run(object, encodedJson, cmd); - this.processResponse(response); - }); - - program - .command('edit [encodedJson]') - .option('--organizationid ', 'Organization id for an organization object.') - .description('Edit an object from the vault.') - .on('--help', () => { - writeLn('\n Objects:'); - writeLn(''); - writeLn(' item'); - writeLn(' item-collections'); - writeLn(' folder'); - writeLn(' org-collection'); - writeLn(''); - writeLn(' Id:'); - writeLn(''); - writeLn(' Object\'s globally unique `id`.'); - writeLn(''); - writeLn(' Notes:'); - writeLn(''); - writeLn(' `encodedJson` can also be piped into stdin.'); - writeLn(''); - writeLn(' Examples:'); - writeLn(''); - writeLn(' bw edit folder 5cdfbd80-d99f-409b-915b-f4c5d0241b02 eyJuYW1lIjoiTXkgRm9sZGVyMiJ9Cg=='); - writeLn(' echo \'eyJuYW1lIjoiTXkgRm9sZGVyMiJ9Cg==\' | ' + - 'bw edit folder 5cdfbd80-d99f-409b-915b-f4c5d0241b02'); - writeLn(' bw edit item-collections 78307355-fd25-416b-88b8-b33fd0e88c82 ' + - 'WyI5NzQwNTNkMC0zYjMzLTRiOTgtODg2ZS1mZWNmNWM4ZGJhOTYiXQ=='); - writeLn('', true); - }) - .action(async (object, id, encodedJson, cmd) => { - await this.exitIfLocked(); - const command = new EditCommand(this.main.cipherService, this.main.folderService, - this.main.cryptoService, this.main.apiService); - const response = await command.run(object, id, encodedJson, cmd); - this.processResponse(response); - }); - - program - .command('delete ') - .option('--itemid ', 'Attachment\'s item id.') - .option('--organizationid ', 'Organization id for an organization object.') - .option('-p, --permanent', 'Permanently deletes the item instead of soft-deleting it (item only).') - .description('Delete an object from the vault.') - .on('--help', () => { - writeLn('\n Objects:'); - writeLn(''); - writeLn(' item'); - writeLn(' attachment'); - writeLn(' folder'); - writeLn(' org-collection'); - writeLn(''); - writeLn(' Id:'); - writeLn(''); - writeLn(' Object\'s globally unique `id`.'); - writeLn(''); - writeLn(' Examples:'); - writeLn(''); - writeLn(' bw delete item 7063feab-4b10-472e-b64c-785e2b870b92'); - writeLn(' bw delete item 89c21cd2-fab0-4f69-8c6e-ab8a0168f69a --permanent'); - writeLn(' bw delete folder 5cdfbd80-d99f-409b-915b-f4c5d0241b02'); - writeLn(' bw delete attachment b857igwl1dzrs2 --itemid 310d5ffd-e9a2-4451-af87-ea054dce0f78'); - writeLn('', true); - }) - .action(async (object, id, cmd) => { - await this.exitIfLocked(); - const command = new DeleteCommand(this.main.cipherService, this.main.folderService, - this.main.userService, this.main.apiService); - const response = await command.run(object, id, cmd); - this.processResponse(response); - }); - - program - .command('restore ') - .description('Restores an object from the trash.') - .on('--help', () => { - writeLn('\n Objects:'); - writeLn(''); - writeLn(' item'); - writeLn(''); - writeLn(' Id:'); - writeLn(''); - writeLn(' Object\'s globally unique `id`.'); - writeLn(''); - writeLn(' Examples:'); - writeLn(''); - writeLn(' bw restore item 7063feab-4b10-472e-b64c-785e2b870b92'); - writeLn('', true); - }) - .action(async (object, id, cmd) => { - await this.exitIfLocked(); - const command = new RestoreCommand(this.main.cipherService); - const response = await command.run(object, id, cmd); - this.processResponse(response); - }); - - program - .command('share [encodedJson]') - .description('Share an item to an organization.') - .on('--help', () => { - writeLn('\n Id:'); - writeLn(''); - writeLn(' Item\'s globally unique `id`.'); - writeLn(''); - writeLn(' Organization Id:'); - writeLn(''); - writeLn(' Organization\'s globally unique `id`.'); - writeLn(''); - writeLn(' Notes:'); - writeLn(''); - writeLn(' `encodedJson` can also be piped into stdin. `encodedJson` contains ' + - 'an array of collection ids.'); - writeLn(''); - writeLn(' Examples:'); - writeLn(''); - writeLn(' bw share 4af958ce-96a7-45d9-beed-1e70fabaa27a 6d82949b-b44d-468a-adae-3f3bacb0ea32 ' + - 'WyI5NzQwNTNkMC0zYjMzLTRiOTgtODg2ZS1mZWNmNWM4ZGJhOTYiXQ=='); - writeLn(' echo \'["974053d0-3b33-4b98-886e-fecf5c8dba96"]\' | bw encode | ' + - 'bw share 4af958ce-96a7-45d9-beed-1e70fabaa27a 6d82949b-b44d-468a-adae-3f3bacb0ea32'); - writeLn('', true); - }) - .action(async (id, organizationId, encodedJson, cmd) => { - await this.exitIfLocked(); - const command = new ShareCommand(this.main.cipherService); - const response = await command.run(id, organizationId, encodedJson, cmd); - this.processResponse(response); - }); - - program - .command('confirm ') - .option('--organizationid ', 'Organization id for an organization object.') - .description('Confirm an object to the organization.') - .on('--help', () => { - writeLn('\n Objects:'); - writeLn(''); - writeLn(' org-member'); - writeLn(''); - writeLn(' Id:'); - writeLn(''); - writeLn(' Object\'s globally unique `id`.'); - writeLn(''); - writeLn(' Examples:'); - writeLn(''); - writeLn(' bw confirm org-member 7063feab-4b10-472e-b64c-785e2b870b92 ' + - '--organizationid 310d5ffd-e9a2-4451-af87-ea054dce0f78'); - writeLn('', true); - }) - .action(async (object, id, cmd) => { - await this.exitIfLocked(); - const command = new ConfirmCommand(this.main.apiService, this.main.cryptoService); - const response = await command.run(object, id, cmd); - this.processResponse(response); - }); - - program - .command('import [format] [input]') - .description('Import vault data from a file.') - .option('--formats', 'List formats') - .on('--help', () => { - writeLn('\n Examples:'); - writeLn(''); - writeLn(' bw import --formats'); - writeLn(' bw import bitwardencsv ./from/source.csv'); - writeLn(' bw import keepass2xml keepass_backup.xml'); - }) - .action(async (format, filepath, cmd) => { - await this.exitIfLocked(); - const command = new ImportCommand(this.main.importService); - const response = await command.run(format, filepath, cmd); - this.processResponse(response); - }); - - program - .command('export [password]') - .description('Export vault data to a CSV or JSON file.') - .option('--output ', 'Output directory or filename.') - .option('--format ', 'Export file format.') - .option('--organizationid ', 'Organization id for an organization.') - .on('--help', () => { - writeLn('\n Notes:'); - writeLn(''); - writeLn(' Valid formats are `csv`, `json`, `encrypted_json`. Default format is `csv`.'); - writeLn(''); - writeLn(' If --raw option is specified and no output filename or directory is given, the'); - writeLn(' result is written to stdout.'); - writeLn(''); - writeLn(' Examples:'); - writeLn(''); - writeLn(' bw export'); - writeLn(' bw --raw export'); - writeLn(' bw export myPassword321'); - writeLn(' bw export myPassword321 --format json'); - writeLn(' bw export --output ./exp/bw.csv'); - writeLn(' bw export myPassword321 --output bw.json --format json'); - writeLn(' bw export myPassword321 --organizationid 7063feab-4b10-472e-b64c-785e2b870b92'); - writeLn('', true); - }) - .action(async (password, cmd) => { - await this.exitIfLocked(); - const command = new ExportCommand(this.main.cryptoService, this.main.exportService); - const response = await command.run(password, cmd); - this.processResponse(response); - }); - program .command('generate') .description('Generate a password/passphrase.') @@ -599,9 +258,9 @@ export class Program extends BaseProgram { writeLn(' bw generate -p --words 5 --separator space'); writeLn('', true); }) - .action(async (cmd) => { + .action(async (options) => { const command = new GenerateCommand(this.main.passwordGenerationService); - const response = await command.run(cmd); + const response = await command.run(options); this.processResponse(response); }); @@ -618,9 +277,9 @@ export class Program extends BaseProgram { writeLn(' echo \'{"name":"My Folder"}\' | bw encode'); writeLn('', true); }) - .action(async (object, id, cmd) => { + .action(async () => { const command = new EncodeCommand(); - const response = await command.run(cmd); + const response = await command.run(); this.processResponse(response); }); @@ -646,9 +305,9 @@ export class Program extends BaseProgram { writeLn(' bw config server --api http://localhost:4000 --identity http://localhost:33656'); writeLn('', true); }) - .action(async (setting, value, cmd) => { + .action(async (setting, value, options) => { const command = new ConfigCommand(this.main.environmentService); - const response = await command.run(setting, value, cmd); + const response = await command.run(setting, value, options); this.processResponse(response); }); @@ -668,10 +327,10 @@ export class Program extends BaseProgram { writeLn(' bw update --raw'); writeLn('', true); }) - .action(async (cmd) => { + .action(async () => { const command = new UpdateCommand(this.main.platformUtilsService, this.main.i18nService, 'cli', 'bw', true); - const response = await command.run(cmd); + const response = await command.run(); this.processResponse(response); }); @@ -689,9 +348,9 @@ export class Program extends BaseProgram { writeLn(' bw completion --shell zsh'); writeLn('', true); }) - .action(async (cmd: program.Command) => { + .action(async (options: program.OptionValues, cmd: program.Command) => { const command = new CompletionCommand(); - const response = await command.run(cmd); + const response = await command.run(options); this.processResponse(response); }); @@ -719,22 +378,16 @@ export class Program extends BaseProgram { writeLn(' - `unlocked` when you are logged in and the vault is unlocked'); writeLn('', true); }) - .action(async (cmd: program.Command) => { + .action(async () => { const command = new StatusCommand( this.main.environmentService, this.main.syncService, this.main.userService, this.main.vaultTimeoutService); - const response = await command.run(cmd); + const response = await command.run(); this.processResponse(response); }); - program - .parse(process.argv); - - if (process.argv.slice(2).length === 0) { - program.outputHelp(); - } } protected processResponse(response: Response, exitImmediately = false) { @@ -746,7 +399,7 @@ export class Program extends BaseProgram { }); } - private async exitIfLocked() { + protected async exitIfLocked() { await this.exitIfNotAuthed(); const hasKey = await this.main.cryptoService.hasKey(); if (!hasKey) { @@ -763,4 +416,5 @@ export class Program extends BaseProgram { } } } + } diff --git a/src/send.program.ts b/src/send.program.ts new file mode 100644 index 0000000..a6b0705 --- /dev/null +++ b/src/send.program.ts @@ -0,0 +1,271 @@ +import * as chk from 'chalk'; +import * as program from 'commander'; +import * as fs from 'fs'; +import * as path from 'path'; + +import { Response } from 'jslib/cli/models/response'; + +import { SendType } from 'jslib/enums/sendType'; + +import { Utils } from 'jslib/misc/utils'; + +import { GetCommand } from './commands/get.command'; +import { SendCreateCommand } from './commands/send/create.command'; +import { SendDeleteCommand } from './commands/send/delete.command'; +import { SendEditCommand } from './commands/send/edit.command'; +import { SendGetCommand } from './commands/send/get.command'; +import { SendListCommand } from './commands/send/list.command'; +import { SendReceiveCommand } from './commands/send/receive.command'; +import { SendRemovePasswordCommand } from './commands/send/removePassword.command'; + +import { SendFileResponse } from './models/response/sendFileResponse'; +import { SendResponse } from './models/response/sendResponse'; +import { SendTextResponse } from './models/response/sendTextResponse'; + +import { Main } from './bw'; +import { Program } from './program'; +import { CliUtils } from './utils'; + +const chalk = chk.default; +const writeLn = CliUtils.writeLn; + +export class SendProgram extends Program { + constructor(main: Main) { + super(main); + } + + register() { + program.addCommand(this.sendCommand()); + // receive is accessible both at `bw receive` and `bw send receive` + program.addCommand(this.receiveCommand()); + } + + private sendCommand(): program.Command { + return new program.Command('send') + .arguments('') + .description('Work with Bitwarden sends. A Send can be quickly created using this command or subcommands can be used to fine-tune the Send', { + data: 'The data to Send. Specify as a filepath with the --file option' + }) + .option('-f, --file', 'Specifies that is a filepath') + .option('-d, --deleteInDays ', 'The number of days in the future to set deletion date, defaults to 7', '7') + .option('--hidden', 'Hide in web by default. Valid only if --file is not set.') + .option('-n, --name ', 'The name of the Send. Defaults to a guid for text Sends and the filename for files.') + .option('--notes ', 'Notes to add to the Send.') + .option('--fullObject', 'Specifies that the full Send object should be returned rather than just the access url.') + .addCommand(this.listCommand()) + .addCommand(this.templateCommand()) + .addCommand(this.getCommand()) + .addCommand(this.receiveCommand()) + .addCommand(this.createCommand()) + .addCommand(this.editCommand()) + .addCommand(this.removePasswordCommand()) + .addCommand(this.deleteCommand()) + .action(async (data: string, options: program.OptionValues) => { + const encodedJson = this.makeSendJson(data, options); + + let response: Response; + if (encodedJson instanceof Response) { + response = encodedJson; + } else { + response = await this.runCreate(encodedJson, options); + } + + this.processResponse(response); + }); + } + + private receiveCommand(): program.Command { + return new program.Command('receive') + .arguments('') + .description('Access a Bitwarden Send from a url') + .option('--password ', 'Password needed to access the Send.') + .option('--passwordenv ', 'Environment variable storing the Send\'s password') + .option('--passwordfile ', 'Path to a file containing the Send\s password as its first line') + .option('--obj', 'Return the Send\'s json object rather than the Send\'s content') + .option('--output', 'Specify a file path to save a File-type Send to') + .on('--help', () => { + writeLn(''); + writeLn('If a password is required, the provided password is used or the user is prompted.'); + writeLn('', true); + }) + .action(async (url: string, options: program.OptionValues) => { + const cmd = new SendReceiveCommand(this.main.apiService, this.main.cryptoService, + this.main.cryptoFunctionService, this.main.platformUtilsService, this.main.environmentService); + const response = await cmd.run(url, options); + this.processResponse(response); + }); + } + + private listCommand(): program.Command { + return new program.Command('list') + + .description('List all the Sends owned by you') + .on('--help', () => { writeLn(chk.default('This is in the list command')); }) + .action(async (options: program.OptionValues) => { + await this.exitIfLocked(); + const cmd = new SendListCommand(this.main.sendService, this.main.environmentService, + this.main.searchService); + const response = await cmd.run(options); + this.processResponse(response); + }); + } + + private templateCommand(): program.Command { + return new program.Command('template') + .arguments('') + .description('Get json templates for send objects', { + object: 'Valid objects are: send, send.text, send.file' + }) + .action(async (object) => { + const cmd = new GetCommand(this.main.cipherService, this.main.folderService, + this.main.collectionService, this.main.totpService, this.main.auditService, this.main.cryptoService, + this.main.userService, this.main.searchService, this.main.apiService, this.main.sendService, + this.main.environmentService); + const response = await cmd.run('template', object, null); + this.processResponse(response); + }); + } + + private getCommand(): program.Command { + return new program.Command('get') + .arguments('') + .description('Get Sends owned by you.') + .option('--output ', 'Output directory or filename for attachment.') + .option('--text', 'Specifies to return the text content of a Send') + .option('--file', 'Specifies to return the file content of a Send. This can be paired with --output or --raw to output to stdout') + .on('--help', () => { + writeLn(''); + writeLn(' Id:'); + writeLn(''); + writeLn(' Search term or Send\'s globally unique `id`.'); + writeLn(''); + writeLn(' If raw output is specified and no output filename or directory is given for'); + writeLn(' an attachment query, the attachment content is written to stdout.'); + writeLn(''); + writeLn(' Examples:'); + writeLn(''); + writeLn(' bw get send searchText'); + writeLn(' bw get send id'); + writeLn(' bw get send searchText --text'); + writeLn(' bw get send searchText --file'); + writeLn(' bw get send searchText --file --output ../Photos/photo.jpg'); + writeLn(' bw get send searchText --file --raw'); + writeLn('', true); + }) + .action(async (id: string, options: program.OptionValues) => { + await this.exitIfLocked(); + const cmd = new SendGetCommand(this.main.sendService, this.main.environmentService, + this.main.searchService, this.main.cryptoService); + const response = await cmd.run(id, options); + this.processResponse(response); + }); + } + + private createCommand(): program.Command { + return new program.Command('create') + .arguments('[encodedJson]') + .description('create a Send', { + encodedJson: 'JSON object to upload. Can also be piped in through stdin.', + }) + .option('--file ', 'file to Send. Can also be specified in parent\'s JSON.') + .option('--text ', 'text to Send. Can also be specified in parent\'s JSON.') + .option('--hidden', 'text hidden flag. Valid only with the --text option.') + .option('--password ', 'optional password to access this Send. Can also be specified in JSON') + .option('--fullObject', 'Specifies that the full Send object should be returned rather than just the access url.') + .on('--help', () => { + writeLn(''); + writeLn('Note:'); + writeLn(' Options specified in JSON take precedence over command options'); + writeLn('', true); + }) + .action(async (encodedJson: string, options: program.OptionValues) => { + const response = await this.runCreate(encodedJson, options); + this.processResponse(response); + }); + } + + private editCommand(): program.Command { + return new program.Command('edit') + .arguments('[encodedJson]') + .description('edit a Send', { + encodedJson: 'Updated JSON object to save. If not provided, encodedJson is read from stdin.' + }) + .option('--itemid ', 'Overrides the itemId provided in [encodedJson]') + .on('--help', () => { + writeLn(''); + writeLn('Note:'); + writeLn(' You cannot update a File-type Send\'s file. Just delete and remake it'); + writeLn('', true); + }) + .action(async (encodedJson: string, options: program.OptionValues) => { + await this.exitIfLocked(); + const cmd = new SendEditCommand(this.main.sendService, this.main.userService); + const response = await cmd.run(encodedJson, options); + this.processResponse(response); + }); + } + + private deleteCommand(): program.Command { + return new program.Command('delete') + .arguments('') + .description('delete a Send', { + id: 'The id of the Send to delete.' + }) + .action(async (id: string) => { + await this.exitIfLocked(); + const cmd = new SendDeleteCommand(this.main.sendService); + const response = await cmd.run(id); + this.processResponse(response); + }); + } + + private removePasswordCommand(): program.Command { + return new program.Command('remove-password') + .arguments('') + .description('removes the saved password from a Send.', { + id: 'The id of the Send to alter.' + }) + .action(async (id: string) => { + await this.exitIfLocked(); + const cmd = new SendRemovePasswordCommand(this.main.sendService); + const response = await cmd.run(id); + this.processResponse(response); + }); + } + + private makeSendJson(data: string, options: program.OptionValues) { + let sendFile = null; + let sendText = null; + let name = Utils.newGuid(); + let type = SendType.Text; + if (options.file != null) { + data = path.resolve(data); + if (!fs.existsSync(data)) { + return Response.badRequest('data path does not exist'); + } + + sendFile = SendFileResponse.template(data); + name = path.basename(data); + type = SendType.File; + } else { + sendText = SendTextResponse.template(data, options.hidden); + } + + const template = Utils.assign(SendResponse.template(options.deleteInDays), { + name: options.name ?? name, + notes: options.notes, + file: sendFile, + text: sendText, + type: type + }); + + return Buffer.from(JSON.stringify(template), 'utf8').toString('base64'); + } + + private async runCreate(encodedJson: string, options: program.OptionValues) { + await this.exitIfLocked(); + const cmd = new SendCreateCommand(this.main.sendService, this.main.userService, + this.main.environmentService); + return await cmd.run(encodedJson, options); + } +} diff --git a/src/vault.program.ts b/src/vault.program.ts new file mode 100644 index 0000000..6679e0d --- /dev/null +++ b/src/vault.program.ts @@ -0,0 +1,369 @@ +import * as chk from 'chalk'; +import * as program from 'commander'; + +import { Main } from './bw'; + +import { ConfirmCommand } from './commands/confirm.command'; +import { CreateCommand } from './commands/create.command'; +import { DeleteCommand } from './commands/delete.command'; +import { EditCommand } from './commands/edit.command'; +import { ExportCommand } from './commands/export.command'; +import { GetCommand } from './commands/get.command'; +import { ImportCommand } from './commands/import.command'; +import { ListCommand } from './commands/list.command'; +import { RestoreCommand } from './commands/restore.command'; +import { ShareCommand } from './commands/share.command'; + +import { CliUtils } from './utils'; + +import { Program } from './program'; + +const chalk = chk.default; +const writeLn = CliUtils.writeLn; + +export class VaultProgram extends Program { + constructor(protected main: Main) { + super(main); + } + + register() { + program + .command('list ') + .description('List an array of objects from the vault.') + .option('--search ', 'Perform a search on the listed objects.') + .option('--url ', 'Filter items of type login with a url-match search.') + .option('--folderid ', 'Filter items by folder id.') + .option('--collectionid ', 'Filter items by collection id.') + .option('--organizationid ', 'Filter items or collections by organization id.') + .option('--trash', 'Filter items that are deleted and in the trash.') + .on('--help', () => { + writeLn('\n Objects:'); + writeLn(''); + writeLn(' items'); + writeLn(' folders'); + writeLn(' collections'); + writeLn(' organizations'); + writeLn(' org-collections'); + writeLn(' org-members'); + writeLn(''); + writeLn(' Notes:'); + writeLn(''); + writeLn(' Combining search with a filter performs a logical AND operation.'); + writeLn(''); + writeLn(' Combining multiple filters performs a logical OR operation.'); + writeLn(''); + writeLn(' Examples:'); + writeLn(''); + writeLn(' bw list items'); + writeLn(' bw list items --folderid 60556c31-e649-4b5d-8daf-fc1c391a1bf2'); + writeLn(' bw list items --search google --folderid 60556c31-e649-4b5d-8daf-fc1c391a1bf2'); + writeLn(' bw list items --url https://google.com'); + writeLn(' bw list items --folderid null'); + writeLn(' bw list items --organizationid notnull'); + writeLn(' bw list items --folderid 60556c31-e649-4b5d-8daf-fc1c391a1bf2 --organizationid notnull'); + writeLn(' bw list items --trash'); + writeLn(' bw list folders --search email'); + writeLn(' bw list org-members --organizationid 60556c31-e649-4b5d-8daf-fc1c391a1bf2'); + writeLn('', true); + }) + .action(async (object, cmd) => { + await this.exitIfLocked(); + const command = new ListCommand(this.main.cipherService, this.main.folderService, + this.main.collectionService, this.main.userService, this.main.searchService, this.main.apiService); + const response = await command.run(object, cmd); + this.processResponse(response); + }); + + program + .command('get ') + .description('Get an object from the vault.') + .option('--itemid ', 'Attachment\'s item id.') + .option('--output ', 'Output directory or filename for attachment.') + .option('--organizationid ', 'Organization id for an organization object.') + .on('--help', () => { + writeLn('\n Objects:'); + writeLn(''); + writeLn(' item'); + writeLn(' username'); + writeLn(' password'); + writeLn(' uri'); + writeLn(' totp'); + writeLn(' exposed'); + writeLn(' attachment'); + writeLn(' folder'); + writeLn(' collection'); + writeLn(' org-collection'); + writeLn(' organization'); + writeLn(' template'); + writeLn(' fingerprint'); + writeLn(' send'); + writeLn(''); + writeLn(' Id:'); + writeLn(''); + writeLn(' Search term or object\'s globally unique `id`.'); + writeLn(''); + writeLn(' If raw output is specified and no output filename or directory is given for'); + writeLn(' an attachment query, the attachment content is written to stdout.'); + writeLn(''); + writeLn(' Examples:'); + writeLn(''); + writeLn(' bw get item 99ee88d2-6046-4ea7-92c2-acac464b1412'); + writeLn(' bw get password https://google.com'); + writeLn(' bw get totp google.com'); + writeLn(' bw get exposed yahoo.com'); + writeLn(' bw get attachment b857igwl1dzrs2 --itemid 99ee88d2-6046-4ea7-92c2-acac464b1412 ' + + '--output ./photo.jpg'); + writeLn(' bw get attachment photo.jpg --itemid 99ee88d2-6046-4ea7-92c2-acac464b1412 --raw'); + writeLn(' bw get folder email'); + writeLn(' bw get template folder'); + writeLn('', true); + }) + .action(async (object, id, cmd) => { + await this.exitIfLocked(); + const command = new GetCommand(this.main.cipherService, this.main.folderService, + this.main.collectionService, this.main.totpService, this.main.auditService, + this.main.cryptoService, this.main.userService, this.main.searchService, + this.main.apiService, this.main.sendService, this.main.environmentService); + const response = await command.run(object, id, cmd); + this.processResponse(response); + }); + + program + .command('create [encodedJson]') + .option('--file ', 'Path to file for attachment.') + .option('--itemid ', 'Attachment\'s item id.') + .option('--organizationid ', 'Organization id for an organization object.') + .description('Create an object in the vault.') + .on('--help', () => { + writeLn('\n Objects:'); + writeLn(''); + writeLn(' item'); + writeLn(' attachment'); + writeLn(' folder'); + writeLn(' org-collection'); + writeLn(''); + writeLn(' Notes:'); + writeLn(''); + writeLn(' `encodedJson` can also be piped into stdin.'); + writeLn(''); + writeLn(' Examples:'); + writeLn(''); + writeLn(' bw create folder eyJuYW1lIjoiTXkgRm9sZGVyIn0K'); + writeLn(' echo \'eyJuYW1lIjoiTXkgRm9sZGVyIn0K\' | bw create folder'); + writeLn(' bw create attachment --file ./myfile.csv ' + + '--itemid 16b15b89-65b3-4639-ad2a-95052a6d8f66'); + writeLn('', true); + }) + .action(async (object, encodedJson, cmd) => { + await this.exitIfLocked(); + const command = new CreateCommand(this.main.cipherService, this.main.folderService, + this.main.userService, this.main.cryptoService, this.main.apiService); + const response = await command.run(object, encodedJson, cmd); + this.processResponse(response); + }); + + program + .command('edit [encodedJson]') + .option('--organizationid ', 'Organization id for an organization object.') + .description('Edit an object from the vault.') + .on('--help', () => { + writeLn('\n Objects:'); + writeLn(''); + writeLn(' item'); + writeLn(' item-collections'); + writeLn(' folder'); + writeLn(' org-collection'); + writeLn(''); + writeLn(' Id:'); + writeLn(''); + writeLn(' Object\'s globally unique `id`.'); + writeLn(''); + writeLn(' Notes:'); + writeLn(''); + writeLn(' `encodedJson` can also be piped into stdin.'); + writeLn(''); + writeLn(' Examples:'); + writeLn(''); + writeLn(' bw edit folder 5cdfbd80-d99f-409b-915b-f4c5d0241b02 eyJuYW1lIjoiTXkgRm9sZGVyMiJ9Cg=='); + writeLn(' echo \'eyJuYW1lIjoiTXkgRm9sZGVyMiJ9Cg==\' | ' + + 'bw edit folder 5cdfbd80-d99f-409b-915b-f4c5d0241b02'); + writeLn(' bw edit item-collections 78307355-fd25-416b-88b8-b33fd0e88c82 ' + + 'WyI5NzQwNTNkMC0zYjMzLTRiOTgtODg2ZS1mZWNmNWM4ZGJhOTYiXQ=='); + writeLn('', true); + }) + .action(async (object, id, encodedJson, cmd) => { + await this.exitIfLocked(); + const command = new EditCommand(this.main.cipherService, this.main.folderService, + this.main.cryptoService, this.main.apiService); + const response = await command.run(object, id, encodedJson, cmd); + this.processResponse(response); + }); + + program + .command('delete ') + .option('--itemid ', 'Attachment\'s item id.') + .option('--organizationid ', 'Organization id for an organization object.') + .option('-p, --permanent', 'Permanently deletes the item instead of soft-deleting it (item only).') + .description('Delete an object from the vault.') + .on('--help', () => { + writeLn('\n Objects:'); + writeLn(''); + writeLn(' item'); + writeLn(' attachment'); + writeLn(' folder'); + writeLn(' org-collection'); + writeLn(''); + writeLn(' Id:'); + writeLn(''); + writeLn(' Object\'s globally unique `id`.'); + writeLn(''); + writeLn(' Examples:'); + writeLn(''); + writeLn(' bw delete item 7063feab-4b10-472e-b64c-785e2b870b92'); + writeLn(' bw delete item 89c21cd2-fab0-4f69-8c6e-ab8a0168f69a --permanent'); + writeLn(' bw delete folder 5cdfbd80-d99f-409b-915b-f4c5d0241b02'); + writeLn(' bw delete attachment b857igwl1dzrs2 --itemid 310d5ffd-e9a2-4451-af87-ea054dce0f78'); + writeLn('', true); + }) + .action(async (object, id, cmd) => { + await this.exitIfLocked(); + const command = new DeleteCommand(this.main.cipherService, this.main.folderService, + this.main.userService, this.main.apiService); + const response = await command.run(object, id, cmd); + this.processResponse(response); + }); + + program + .command('restore ') + .description('Restores an object from the trash.') + .on('--help', () => { + writeLn('\n Objects:'); + writeLn(''); + writeLn(' item'); + writeLn(''); + writeLn(' Id:'); + writeLn(''); + writeLn(' Object\'s globally unique `id`.'); + writeLn(''); + writeLn(' Examples:'); + writeLn(''); + writeLn(' bw restore item 7063feab-4b10-472e-b64c-785e2b870b92'); + writeLn('', true); + }) + .action(async (object, id, cmd) => { + await this.exitIfLocked(); + const command = new RestoreCommand(this.main.cipherService); + const response = await command.run(object, id, cmd); + this.processResponse(response); + }); + + program + .command('share [encodedJson]') + .description('Share an item to an organization.') + .on('--help', () => { + writeLn('\n Id:'); + writeLn(''); + writeLn(' Item\'s globally unique `id`.'); + writeLn(''); + writeLn(' Organization Id:'); + writeLn(''); + writeLn(' Organization\'s globally unique `id`.'); + writeLn(''); + writeLn(' Notes:'); + writeLn(''); + writeLn(' `encodedJson` can also be piped into stdin. `encodedJson` contains ' + + 'an array of collection ids.'); + writeLn(''); + writeLn(' Examples:'); + writeLn(''); + writeLn(' bw share 4af958ce-96a7-45d9-beed-1e70fabaa27a 6d82949b-b44d-468a-adae-3f3bacb0ea32 ' + + 'WyI5NzQwNTNkMC0zYjMzLTRiOTgtODg2ZS1mZWNmNWM4ZGJhOTYiXQ=='); + writeLn(' echo \'["974053d0-3b33-4b98-886e-fecf5c8dba96"]\' | bw encode | ' + + 'bw share 4af958ce-96a7-45d9-beed-1e70fabaa27a 6d82949b-b44d-468a-adae-3f3bacb0ea32'); + writeLn('', true); + }) + .action(async (id, organizationId, encodedJson, cmd) => { + await this.exitIfLocked(); + const command = new ShareCommand(this.main.cipherService); + const response = await command.run(id, organizationId, encodedJson, cmd); + this.processResponse(response); + }); + + program + .command('confirm ') + .option('--organizationid ', 'Organization id for an organization object.') + .description('Confirm an object to the organization.') + .on('--help', () => { + writeLn('\n Objects:'); + writeLn(''); + writeLn(' org-member'); + writeLn(''); + writeLn(' Id:'); + writeLn(''); + writeLn(' Object\'s globally unique `id`.'); + writeLn(''); + writeLn(' Examples:'); + writeLn(''); + writeLn(' bw confirm org-member 7063feab-4b10-472e-b64c-785e2b870b92 ' + + '--organizationid 310d5ffd-e9a2-4451-af87-ea054dce0f78'); + writeLn('', true); + }) + .action(async (object, id, cmd) => { + await this.exitIfLocked(); + const command = new ConfirmCommand(this.main.apiService, this.main.cryptoService); + const response = await command.run(object, id, cmd); + this.processResponse(response); + }); + + program + .command('import [format] [input]') + .description('Import vault data from a file.') + .option('--formats', 'List formats') + .on('--help', () => { + writeLn('\n Examples:'); + writeLn(''); + writeLn(' bw import --formats'); + writeLn(' bw import bitwardencsv ./from/source.csv'); + writeLn(' bw import keepass2xml keepass_backup.xml'); + }) + .action(async (format, filepath, options) => { + await this.exitIfLocked(); + const command = new ImportCommand(this.main.importService); + const response = await command.run(format, filepath, options); + this.processResponse(response); + }); + + program + .command('export [password]') + .description('Export vault data to a CSV or JSON file.') + .option('--output ', 'Output directory or filename.') + .option('--format ', 'Export file format.') + .option('--organizationid ', 'Organization id for an organization.') + .on('--help', () => { + writeLn('\n Notes:'); + writeLn(''); + writeLn(' Valid formats are `csv`, `json`, `encrypted_json`. Default format is `csv`.'); + writeLn(''); + writeLn(' If --raw option is specified and no output filename or directory is given, the'); + writeLn(' result is written to stdout.'); + writeLn(''); + writeLn(' Examples:'); + writeLn(''); + writeLn(' bw export'); + writeLn(' bw --raw export'); + writeLn(' bw export myPassword321'); + writeLn(' bw export myPassword321 --format json'); + writeLn(' bw export --output ./exp/bw.csv'); + writeLn(' bw export myPassword321 --output bw.json --format json'); + writeLn(' bw export myPassword321 --organizationid 7063feab-4b10-472e-b64c-785e2b870b92'); + writeLn('', true); + }) + .action(async (password, options) => { + await this.exitIfLocked(); + const command = new ExportCommand(this.main.cryptoService, this.main.exportService); + const response = await command.run(password, options); + this.processResponse(response); + }); + + } +} diff --git a/tslint.json b/tslint.json index 7e4320f..08a680a 100644 --- a/tslint.json +++ b/tslint.json @@ -49,6 +49,7 @@ "check-separator", "check-type" ], - "max-classes-per-file": false + "max-classes-per-file": false, + "ordered-imports": true } }