on master help sign in
102 files changed+5949−1491
since splitsnapshots
unifiedsplit
.github/FUNDING.yml+1 .github/workflows/build.yaml+97 .gitignore+12−1 .gitmodules−6 .helix/languages.toml+3 README.md+32−8 backend-main.js+3 backend/BW.js+53−55 backend/BW.purs+37−31 backend/BW/Logic.purs+317−237 backend/BW/Types.purs+445−390 backend/Bridge.purs+128−1 backend/Data/EncString.js+1−2 backend/Data/SymmetricCryptoKey.js+1−2 backend/FFI.js+16−1 backend/Main.purs+220−122 backend/Storage.purs+7−1 bridge.yaml+118−84 build.sh+2−10 clickable.yaml+2−10 deps/bw−1 dev/build-backend.sh+7−1 dev/build-bw.sh+2 dev/build.sh+10 dev/elm-live.sh+5−1 dev/elm2nix.sh+8 dev/generate-bridge.sh+1−1 dev/record-test.sh+11 dev/spago2nix.sh+3 dev/start-chrome.sh+7−1 dev/test.sh+9 dev/update-tests.sh+9 elm-srcs.nix+62 elm.json+1 flake.lock+177 flake.nix+133 frontend.nix+49 frontend/Bridge.elm+186−5 frontend/GlobalEvents.elm+1 frontend/Logic/Cipher.elm+3−2 frontend/Logic/SecondFactor.elm+31 frontend/Main.elm+410−189 frontend/Page.elm+23−15 frontend/Pages/Captcha.elm+22−35 frontend/Pages/Cipher.elm+114−42 frontend/Pages/Ciphers.elm+151−84 frontend/Pages/EditCipher.elm+101−64 frontend/Pages/Login.elm+38−42 frontend/Pages/MasterPassword.elm+19−30 frontend/Pages/SecondFactor.elm+91 frontend/Pages/SecondFactorSelect.elm+164 frontend/Utils.elm+7 img/donate.png index.html+2−2 manifest.json+6−6 package-lock.json+959 packages.dhall+6−6 registry.dat scratchpad/password_generator.html+16−1 screenshots/1.png screenshots/2.png screenshots/3.png screenshots/4.png screenshots/5.png spago-packages.nix+1102 spago.dhall+5 style.css+42 tests/package-lock.json+73 tests/package.json+9 tests/playwright.config.ts+65 tests/src/test.spec.ts+257 tests/src/test.spec.ts-snapshots/Card-cipher-page-1-Pixel-3-linux.png tests/src/test.spec.ts-snapshots/Cards-view-1-Pixel-3-linux.png tests/src/test.spec.ts-snapshots/Contact-cipher-page-1-Pixel-3-linux.png tests/src/test.spec.ts-snapshots/Contacts-view-1-Pixel-3-linux.png tests/src/test.spec.ts-snapshots/Create-Card-entry-page-1-Pixel-3-linux.png tests/src/test.spec.ts-snapshots/Create-Card-entry-page-2-Pixel-3-linux.png tests/src/test.spec.ts-snapshots/Create-Contact-entry-page-1-Pixel-3-linux.png tests/src/test.spec.ts-snapshots/Create-Contact-entry-page-2-Pixel-3-linux.png tests/src/test.spec.ts-snapshots/Create-Note-entry-page-1-Pixel-3-linux.png tests/src/test.spec.ts-snapshots/Create-Note-entry-page-2-Pixel-3-linux.png tests/src/test.spec.ts-snapshots/Empty-ciphers-page-1-Pixel-3-linux.png tests/src/test.spec.ts-snapshots/Filled-ciphers-page-1-Pixel-3-linux.png tests/src/test.spec.ts-snapshots/Login-cipher-page-1-Pixel-3-linux.png tests/src/test.spec.ts-snapshots/Login-entry-page-1-Pixel-3-linux.png tests/src/test.spec.ts-snapshots/Login-entry-page-2-Pixel-3-linux.png tests/src/test.spec.ts-snapshots/Login-page-1-Pixel-3-linux.png tests/src/test.spec.ts-snapshots/Note-cipher-page-1-Pixel-3-linux.png tests/src/test.spec.ts-snapshots/Notes-view-1-Pixel-3-linux.png tests/src/test.spec.ts-snapshots/Passwords-view-1-Pixel-3-linux.png tests/src/test.spec.ts-snapshots/Side-bar-1-Pixel-3-linux.png tests/vaultwarden/.env+3 tests/vaultwarden/README.md+4 tests/vaultwarden/data/db.sqlite3 tests/vaultwarden/data/db.sqlite3-shm tests/vaultwarden/data/db.sqlite3-wal tests/vaultwarden/data/rsa_key.pem+27 tests/vaultwarden/data/rsa_key.pub.pem+9 tests/vaultwarden/run.sh+13 vanilla−1 appname.apparmor → warden.apparmor+1−1 appname.desktop → warden.desktop
.github/FUNDING.ymlnew file+1
@@ -0,0 +1 @@
1+custom: ["https://iko.soy/donate"]
.github/workflows/build.yamlnew file+97
@@ -0,0 +1,97 @@
1+name: CI
2+
3+on:
4+ push:
5+ branches: ["master"]
6+ pull_request:
7+ branches: ["master"]
8+ release:
9+ types: [prereleased]
10+
11+jobs:
12+ build:
13+ runs-on: ubuntu-latest
14+
15+ steps:
16+ - uses: cachix/install-nix-action@v19
17+
18+ - name: Cachix
19+ uses: cachix/cachix-action@v12
20+ with:
21+ name: iko
22+ authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
23+
24+ - uses: actions/checkout@v3
25+ with:
26+ submodules: recursive
27+
28+ - name: Install clickable
29+ run: |
30+ docker --version
31+ sudo apt install adb git python3 python3-pip python3-setuptools
32+ pip3 install --user clickable-ut==7.10.0 --upgrade
33+ echo 'export PATH="$PATH:$HOME/.local/bin"' >> ~/.bashrc
34+
35+ - name: Build artifacts
36+ run: ./dev/build.sh
37+
38+ - name: Build Warden
39+ run: clickable build --output click
40+
41+ - uses: actions/upload-artifact@v3
42+ with:
43+ name: click
44+ path: click/*
45+
46+ - uses: actions/upload-artifact@v3
47+ with:
48+ name: dist
49+ path: dist
50+
51+ # Tests
52+
53+ - uses: actions/setup-node@v3
54+ with:
55+ node-version: 16
56+
57+ - name: Install test dependencies
58+ working-directory: tests
59+ run: |
60+ npm ci
61+ npx playwright install --with-deps
62+
63+ - name: Run tests
64+ working-directory: tests
65+ run: npx playwright test
66+
67+ - uses: actions/upload-artifact@v3
68+ if: always()
69+ with:
70+ name: playwright-report
71+ path: tests/playwright-report/
72+
73+ # Upload release
74+
75+ - name: Zip Release Assets
76+ if: github.event_name == 'release'
77+ shell: bash
78+ run: |
79+ cd dist/
80+ zip -r ../../../../dist.zip .
81+
82+ - name: Upload dist to release
83+ if: github.event_name == 'release'
84+ uses: svenstaro/upload-release-action@2.4.1
85+ with:
86+ repo_token: ${{ secrets.GITHUB_TOKEN }}
87+ file: dist.zip
88+ tag: ${{ github.ref }}
89+
90+ - name: Upload click to release
91+ if: github.event_name == 'release'
92+ uses: svenstaro/upload-release-action@2.4.1
93+ with:
94+ repo_token: ${{ secrets.GITHUB_TOKEN }}
95+ file: build/all/app/*.click
96+ tag: ${{ github.ref }}
97+ file_glob: true
.gitignore+12−1
@@ -7,5 +7,16 @@ static/backend.js
77.spago
88output
99.psc-ide-port
10−backend.js
1110elm.js
11+output-es
12+elm-stuff
13+node_modules/
14+/tests/test-results/
15+/tests/playwright-report/
16+/tests/playwright/.cache/
17+generated-docs
18+backend.js.map
19+.spago2nix
20+backend.js
21+vanilla
22+dist
.gitmodulesdeleted−6
@@ -1,6 +0,0 @@
1−[submodule "deps/bw"]
2− path = deps/bw
3− url = https://github.com/bitwarden/clients.git
4−[submodule "vanilla"]
5− path = vanilla
6− url = https://github.com/ilyakooo0/vanilla-compiled.git
.helix/languages.tomlnew file+3
@@ -0,0 +1,3 @@
1+[[language]]
2+name = "nix"
3+auto-format = true
README.md+32−8
@@ -1,14 +1,38 @@
1−# App Title
1+<p align="center">
2+ <img src="logo.png" style="width: 100px;" />
3+</p>
24
3−A short description of your app
5+<h1 align="center">Warden</h1>
46
5−## License
7+<p align="center">
8+ <a href="https://open-store.io/app/soy.iko.warden"><img src="https://open-store.io/badges/en_US.svg" alt="OpenStore" style="width: 150px;" /></a>
9+</p>
610
7−Copyright (C) 2022 Your FullName
811
9−This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License version 3, as published
10−by the Free Software Foundation.
12+<p align="center">
13+ <img src="screenshots/1.png" style="width: 150px;" />
14+ <img src="screenshots/2.png" style="width: 150px;" />
15+ <img src="screenshots/3.png" style="width: 150px;" />
16+ <img src="screenshots/4.png" style="width: 150px;" />
17+ <img src="screenshots/5.png" style="width: 150px;" />
18+</p>
1119
12−This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
20+Warden is a [Bitwarden](https://bitwarden.com)/[Vaultwarden](https://github.com/dani-garcia/vaultwarden) client for [Ubuntu Touch](https://ubuntu-touch.io).
1321
14−You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>.
22+## Features
23+
24+1. View and edit: passwords, contact, secure notes and credit cards
25+2. Generate passwords
26+
27+## Support the project
28+
29+I fund the development of Warden out of my own pocket. If you find the project useful and don't want it to die, please consider donating what you think the app is worth to you.
30+
31+<p align="center">
32+ <a href="https://iko.soy/donate"><img src="img/donate.png" alt="Donate" style="width: 150px;" /></a>
33+</p>
34+
35+
36+## Porting
37+
38+Although Warden is intended for ubuntu touch it is written completely in web technologies. This means that, if there is demand, the app can be ported to virtually any platform. The biggest problem will be [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS).
backend-main.jsnew file+3
@@ -0,0 +1,3 @@
1+import * as Main from "./backend/Main/index.js"
2+
3+Main.main()
backend/BW.js+53−55
@@ -1,25 +1,29 @@
1−import { StateFactory } from "../../deps/bw/libs/shared/dist/src/factories/stateFactory";
2−import { GlobalState } from "../../deps/bw/libs/shared/dist/src/models/domain/globalState";
3−import { ApiService } from "../../deps/bw/libs/shared/dist/src/services/api.service";
4−import { AppIdService } from "../../deps/bw/libs/shared/dist/src/services/appId.service";
5−import { ConsoleLogService } from "../../deps/bw/libs/shared/dist/src/services/consoleLog.service";
6−import { EnvironmentService } from "../../deps/bw/libs/shared/dist/src/services/environment.service";
7−import { StateMigrationService } from "../../deps/bw/libs/shared/dist/src/services/stateMigration.service";
8−import { TokenService } from "../../deps/bw/libs/shared/dist/src/services/token.service";
9−import { WebCryptoFunctionService } from "../../deps/bw/libs/shared/dist/src/services/webCryptoFunction.service";
10−import { Account, AccountProfile, AccountTokens } from "../../deps/bw/libs/shared/dist/src/models/domain/account";
11−
12−// import BrowserStorageService from "../../../../deps/bw/libs/shared/dist/src/services/browserStorage.service";
13−import { CryptoService } from "../../deps/bw/libs/shared/dist/src/services/crypto.service";
14−
15−import { I18nService } from "../../deps/bw/libs/shared/dist/src/services/i18n.service";
16−import { StateService as BaseStateService } from "../../deps/bw/libs/shared/dist/src/services/state.service";
17−import { PasswordTokenRequest } from "../../deps/bw/libs/shared/dist/src/models/request/identityToken/passwordTokenRequest";
18−import { TokenRequestTwoFactor } from "../../deps/bw/libs/common/src/models/request/identityToken/tokenRequestTwoFactor";
19−import { CipherRequest } from "../../deps/bw/libs/common/src/models/request/cipherRequest";
20−import { Cipher } from "../../deps/bw/libs/common/src/models/domain/cipher";
21−import { CipherData } from "../../deps/bw/libs/common/src/models/data/cipherData";
22−import { PasswordGenerationService } from "../../deps/bw/libs/common/src/services/passwordGeneration.service";
1+import { StateFactory } from "../../bw/factories/stateFactory";
2+import { GlobalState } from "../../bw/models/domain/global-state";
3+import { ApiService } from "../../bw/services/api.service";
4+import { AppIdService } from "../../bw/services/appId.service";
5+import { ConsoleLogService } from "../../bw/services/consoleLog.service";
6+import { EnvironmentService } from "../../bw/services/environment.service";
7+import { StateMigrationService } from "../../bw/services/stateMigration.service";
8+import { TokenService } from "../../bw/services/token.service";
9+import { WebCryptoFunctionService } from "../../bw/services/webCryptoFunction.service";
10+import { Account, AccountProfile, AccountTokens } from "../../bw/models/domain/account";
11+import {MemoryStorageService} from "../../bw/services/memoryStorage.service"
12+
13+// import BrowserStorageService from "../../../../bw/services/browserStorage.service";
14+import { CryptoService } from "../../bw/services/crypto.service";
15+
16+import { I18nService } from "../../bw/services/i18n.service";
17+import { StateService as BaseStateService } from "../../bw/services/state.service";
18+import { PasswordTokenRequest } from "../../bw/models/request/identity-token/password-token.request";
19+import { TokenTwoFactorRequest} from "../../bw/models/request/identity-token/token-two-factor.request";
20+import { CipherRequest } from "../../bw/vault/models/request/cipher.request";
21+import { CipherCreateRequest } from "../../bw/vault/models/request/cipher-create.request";
22+import { Cipher } from "../../bw/vault/models/domain/cipher";
23+import { CipherData } from "../../bw/vault/models/data/cipher.data";
24+import { PasswordGenerationService } from "../../bw/services/passwordGeneration.service";
25+import { TotpService } from "../../bw/services/totp.service"
26+import { EncryptServiceImplementation } from "../../bw/services/cryptography/encrypt.service.implementation"
2327
2428function sanitize(obj) {
2529 return JSON.parse(JSON.stringify(obj));
@@ -34,7 +38,7 @@ export function getServices() {
3438
3539 await bg.bootstrap()
3640
37− bg.environmentService.setUrls(urls)
41+ await bg.environmentService.setUrls(urls)
3842
3943 if (tokenResponse != null) {
4044 const accountInformation = await bg.tokenService.decodeToken(tokenResponse.accessToken);
@@ -71,18 +75,25 @@ export function getServices() {
7175 req.email,
7276 req.masterPasswordHash,
7377 req.captchaResponse,
74− new TokenRequestTwoFactor(req.twoFactor),
78+ new TokenTwoFactorRequest(
79+ req.twoFactor.provider,
80+ req.twoFactor.token,
81+ req.twoFactor.remember
82+ ),
7583 req.device
7684 )
7785 ),
7886 getSync: () => api.getSync().then(sanitize),
79− postCipher: (cipher) => api.postCipher(makeCipherRequest(cipher)),
87+ postCipherCreate: (cipher) => api.postCipherCreate(makeCipherCreateRequest(cipher)),
8088 putCipher: (cipher) => api.putCipher(cipher.id, makeCipherRequest(cipher)),
89+ deleteCipher: (id) => api.deleteCipher(id),
90+ postTwoFactorEmail: (req) => api.postTwoFactorEmail(req),
8191 }
8292 },
8393 crypto: bg.cryptoService,
8494 cryptoFunctions: bg.cryptoFunctionService,
85− passwordGeneration: bg.passwordGenerationService
95+ passwordGeneration: bg.passwordGenerationService,
96+ totpService: bg.totpService,
8697 }
8798}
8899
@@ -90,6 +101,10 @@ function makeCipherRequest(cipher) {
90101 return new CipherRequest(new Cipher(new CipherData(cipher)))
91102}
92103
104+function makeCipherCreateRequest(cipher) {
105+ return new CipherCreateRequest(new Cipher(new CipherData(cipher)))
106+}
107+
93108class MainBackground {
94109 // messagingService; //: MessagingServiceAbstraction;
95110 // storageService; //: StorageServiceAbstraction;
@@ -118,6 +133,7 @@ class MainBackground {
118133 new StateFactory(GlobalState, Account)
119134 );
120135 this.stateService = new StateService(
136+ this.storageService,
121137 this.storageService,
122138 this.storageService,
123139 this.logService,
@@ -148,8 +164,13 @@ class MainBackground {
148164 );
149165 this.i18nService = new I18nService("en");
150166 this.cryptoFunctionService = new WebCryptoFunctionService(window);
167+ this.encryptService = new EncryptServiceImplementation(
168+ this.cryptoFunctionService,
169+ this.logService
170+ );
151171 this.cryptoService = new BrowserCryptoService(
152172 this.cryptoFunctionService,
173+ this.encryptService,
153174 this.platformUtilsService,
154175 this.logService,
155176 this.stateService
@@ -169,6 +190,11 @@ class MainBackground {
169190 this.policyService,
170191 this.stateService
171192 );
193+ this.totpService = new TotpService(
194+ this.cryptoFunctionService,
195+ this.logService,
196+ this.stateService
197+ );
172198 }
173199
174200 async bootstrap() {
@@ -188,34 +214,6 @@ class MainBackground {
188214 }
189215}
190216
191−class MemoryStorageService {
192−
193− constructor() {
194− this.storage = {};
195− }
196−
197− async get(key) {
198− return this.storage[key]
199− }
200−
201− async has(key) {
202− return key in this.storage;
203− }
204−
205− async save(key, obj) {
206− if (obj == null) {
207− this.remove(key)
208− return
209− }
210−
211− this.storage[key] = obj
212− }
213−
214− async remove(key) {
215− delete this.storage[key]
216− }
217−}
218−
219217class BrowserCryptoService extends CryptoService {
220218 async retrieveKeyFromStorage(keySuffix) {
221219 if (keySuffix === "biometric") {
@@ -296,7 +294,7 @@ class BrowserPlatformUtilsService {
296294 saveFile(win, blobData, blobOptions, fileName) {
297295 // BrowserApi.downloadFile(win, blobData, blobOptions, fileName);
298296 }
299− getApplicationVersion() {
297+ getApplicationVersionNumber() {
300298 // return Promise.resolve(BrowserApi.getApplicationVersion());
301299 }
302300 supportsWebAuthn(win) {
backend/BW.purs+37−31
@@ -17,24 +17,31 @@ import Effect (Effect)
1717import Effect.Unsafe (unsafePerformEffect)
1818import Untagged.Union (type (|+|))
1919
20−type CryptoService
21− = { makeKey :: Fn4 Password String KDF Int (Promise SymmetricCryptoKey)
22− , hashPassword :: Fn3 Password SymmetricCryptoKey (JNullable HashPurpose) (Promise StringHash)
23− , decryptToUtf8 :: Fn2 EncString SymmetricCryptoKey (Promise String)
24− , decryptToBytes :: Fn2 EncString SymmetricCryptoKey (Promise ArrayBuffer)
25− , encrypt :: Fn2 String SymmetricCryptoKey (Promise EncString)
26− , stretchKey :: SymmetricCryptoKey -> Promise SymmetricCryptoKey
27− }
28−
29−type ApiService
30− = { postPrelogin :: PreloginRequest -> Promise PreloginResponse
31− , getProfile :: Unit -> Promise ProfileResponse
32− -- TODO: There can really be more request and response types. Maybe handle this later.
33− , postIdentityToken :: PasswordTokenRequest -> Promise (IdentityCaptchaResponse |+| IdentityTokenResponse)
34− , getSync :: Unit -> Promise SyncResponse
35− , postCipher :: CipherResponse -> Promise CipherResponse
36− , putCipher :: CipherResponse -> Promise CipherResponse
37− }
20+type TotpService =
21+ { getCode :: String -> Promise String
22+ , getTimeInterval :: String -> Int
23+ }
24+
25+type CryptoService =
26+ { makeKey :: Fn4 Password String KDF KdfConfig (Promise SymmetricCryptoKey)
27+ , hashPassword :: Fn3 Password SymmetricCryptoKey (JNullable HashPurpose) (Promise StringHash)
28+ , decryptToUtf8 :: Fn2 EncString SymmetricCryptoKey (Promise String)
29+ , decryptToBytes :: Fn2 EncString SymmetricCryptoKey (Promise ArrayBuffer)
30+ , encrypt :: Fn2 String SymmetricCryptoKey (Promise EncString)
31+ , stretchKey :: SymmetricCryptoKey -> Promise SymmetricCryptoKey
32+ }
33+
34+type ApiService =
35+ { postPrelogin :: PreloginRequest -> Promise PreloginResponse
36+ , getProfile :: Unit -> Promise ProfileResponse
37+ -- TODO: There can really be more request and response types. Maybe handle this later.
38+ , postIdentityToken :: PasswordTokenRequest -> Promise (IdentityCaptchaResponse |+| IdentityTwoFactorResponse |+| IdentityTokenResponse)
39+ , getSync :: Unit -> Promise SyncResponse
40+ , postCipherCreate :: CipherResponse -> Promise CipherResponse
41+ , putCipher :: CipherResponse -> Promise CipherResponse
42+ , deleteCipher :: String -> Promise Unit
43+ , postTwoFactorEmail :: TwoFactorEmailRequest -> Promise Unit
44+ }
3845
3946type PasswordGeneration =
4047 { generatePassword :: Bridge.PasswordGeneratorConfig -> Promise String
@@ -48,15 +55,13 @@ cryptoFunctionsTypeSha512 = "sha512" :: CryptoFunctionsType
4855
4956cryptoFunctionsTypeMd5 = "md5" :: CryptoFunctionsType
5057
51−type CryptoFunctionsType
52− = String
58+type CryptoFunctionsType = String
5359
54−type CryptoFunctions
55− = { hash :: Fn2 String CryptoFunctionsType (Promise Hash)
56− }
60+type CryptoFunctions =
61+ { hash :: Fn2 String CryptoFunctionsType (Promise Hash)
62+ }
5763
58−newtype Hash
59− = Hash (ArrayBuffer)
64+newtype Hash = Hash (ArrayBuffer)
6065
6166hashToArray :: Hash -> Array Int
6267hashToArray (Hash buf) =
@@ -79,11 +84,12 @@ instance EncodeJson Hash where
7984instance DecodeJson Hash where
8085 decodeJson = decodeJson >>> map arrayToHash
8186
82−type Services
83− = { crypto :: CryptoService
84− , getApi :: Urls -> JNullable IdentityTokenResponse -> Promise ApiService
85− , cryptoFunctions :: CryptoFunctions
86− , passwordGeneration :: PasswordGeneration
87− }
87+type Services =
88+ { crypto :: CryptoService
89+ , getApi :: Urls -> JNullable IdentityTokenResponse -> Promise ApiService
90+ , cryptoFunctions :: CryptoFunctions
91+ , passwordGeneration :: PasswordGeneration
92+ , totpService :: TotpService
93+ }
8894
8995foreign import getServices :: Effect Services
backend/BW/Logic.purs+317−237
@@ -3,6 +3,7 @@ module BW.Logic where
33import BW
44import BW.Types
55import Prelude
6+
67import Bridge as Bridge
78import Control.Monad.Error.Class (throwError)
89import Control.Promise (Promise)
@@ -12,8 +13,9 @@ import Data.Function.Uncurried (runFn2, runFn3, runFn4)
1213import Data.JNullable (JNullable, fromJNullable, jnull, nullify)
1314import Data.JNullable as JNullable
1415import Data.JOpt (JOpt(..), fromJOpt)
15−import Data.Maybe (Maybe(..), fromMaybe)
16+import Data.Maybe (Maybe(..), fromMaybe, maybe)
1617import Data.Newtype (class Newtype, unwrap, wrap)
18+import Data.Number.Format as Number
1719import Data.String as String
1820import Data.SymmetricCryptoKey (SymmetricCryptoKey)
1921import Data.SymmetricCryptoKey as SymmetricCryptoKey
@@ -25,43 +27,51 @@ import Effect.Aff.Class (class MonadAff, liftAff)
2527import Effect.Class (liftEffect)
2628import Effect.Exception (error)
2729import Effect.Exception as Exc
30+import Effect.Random (random)
31+import Localstorage as Storage
2832import Run (Run)
2933import Run.Reader (Reader, askAt)
34+import Storage (DeviceIDKey(..))
3035import Type.Prelude (Proxy(..))
3136import Untagged.Union (type (|+|))
37+import Web.Storage.Storage (Storage)
3238
3339-- | Creates the master key
34−makePreloginKey ::
35− forall r.
36− PreloginResponse ->
37− Email ->
38− Password ->
39− Run
40− ( crypto ::
41− Reader CryptoService
42− , effect :: Effect
43− , aff :: Aff
44− | r
45− )
46− SymmetricCryptoKey
47−makePreloginKey { kdf, kdfIterations } (Email email') password = do
40+makePreloginKey
41+ :: forall r
42+ . PreloginResponse
43+ -> Email
44+ -> Password
45+ -> Run
46+ ( crypto :: Reader CryptoService
47+ , effect :: Effect
48+ , aff :: Aff
49+ | r
50+ )
51+ SymmetricCryptoKey
52+makePreloginKey { kdf, kdfIterations, kdfMemory, kdfParallelism } (Email email') password = do
4853 crypto <- askAt (Proxy :: _ "crypto")
4954 let
5055 email = (String.trim >>> String.toLower) email'
51− liftPromise $ runFn4 crypto.makeKey password email kdf kdfIterations
56+ liftPromise $ runFn4 crypto.makeKey password email kdf
57+ { iterations: kdfIterations
58+ , memory: kdfMemory
59+ , parallelism: kdfParallelism
60+ }
5261
53−makeDecryptionKey ::
54− forall r.
62+makeDecryptionKey
63+ :: forall r
64+ .
5565 -- | Matser key (prelogin key)
56− SymmetricCryptoKey ->
57− EncryptedString ->
58− Run
59− ( crypto :: Reader CryptoService
60− , effect :: Effect
61− , aff :: Aff
62− | r
63− )
64− SymmetricCryptoKey
66+ SymmetricCryptoKey
67+ -> EncryptedString
68+ -> Run
69+ ( crypto :: Reader CryptoService
70+ , effect :: Effect
71+ , aff :: Aff
72+ | r
73+ )
74+ SymmetricCryptoKey
6575makeDecryptionKey masterKey str = do
6676 crypto <- askAt (Proxy :: _ "crypto")
6777 let
@@ -78,107 +88,145 @@ makeDecryptionKey masterKey str = do
7888 liftEffect $ Exc.throw $ "Unsupported key encryption type: " <> show encType
7989 pure $ SymmetricCryptoKey.fromArrayBuffer key
8090
81−getLogInRequestToken ::
82− forall r.
83− PreloginResponse ->
84− Email ->
85− Password ->
86− Maybe String ->
87− Run
88− ( api :: Reader ApiService
89− , crypto :: Reader CryptoService
90− , effect :: Effect
91− , aff :: Aff
92− | r
93− )
94− (IdentityCaptchaResponse |+| IdentityTokenResponse)
95−getLogInRequestToken prelogin email password captchaResponse = do
91+getLogInRequestToken
92+ :: forall r
93+ . { prelogin :: PreloginResponse
94+ , email :: Email
95+ , password :: Password
96+ , captchaResponse :: Maybe String
97+ , secondFactor :: Bridge.Cmd_Login_secondFactor_Maybe
98+ }
99+ -> Run
100+ ( api :: Reader ApiService
101+ , crypto :: Reader CryptoService
102+ , effect :: Effect
103+ , aff :: Aff
104+ , storage :: Reader Storage
105+ | r
106+ )
107+ (IdentityCaptchaResponse |+| IdentityTwoFactorResponse |+| IdentityTokenResponse)
108+getLogInRequestToken
109+ { prelogin
110+ , email
111+ , password
112+ , captchaResponse
113+ , secondFactor: Bridge.Cmd_Login_secondFactor_Maybe secondFactor
114+ } = do
96115 key <- makePreloginKey prelogin email password
97116 crypto <- askAt (Proxy :: _ "crypto")
98− api :: ApiService <- askAt (Proxy :: _ "api")
117+ let
118+ twoFactor =
119+ maybe
120+ { provider: twoFactorProviderTypeEmail
121+ , token: ""
122+ , remember: false
123+ }
124+ ( \(Bridge.Cmd_Login_secondFactor x@{ provider }) ->
125+ x { provider = bridgeToSecondFactorType provider }
126+ )
127+ secondFactor
99128 _localHashedPassword <-
100129 liftPromise
101130 $ runFn3 crypto.hashPassword password key (nullify hashPurposeLocalAuthorization)
102131 StringHash hashedPassword <- liftPromise $ runFn3 crypto.hashPassword password key jnull
132+ deviceId <- getDeviceID
133+ api :: ApiService <- askAt (Proxy :: _ "api")
103134 liftPromise
104135 $ api.postIdentityToken
105136 { email: email
106137 , masterPasswordHash: hashedPassword
107138 , captchaResponse: fromMaybe "" captchaResponse
108− , twoFactor:
109− { provider: twoFactorProviderTypeEmail
110− , token: ""
111− , remember: false
112− }
139+ , twoFactor
113140 , device:
114141 { type: deviceTypeUnknownBrowser
115− , name: "Temporary device name"
116− , identifier: "42"
142+ , name: "Ubuntu Touch"
143+ , identifier: deviceId
117144 , pushToken: jnull
118145 }
119146 }
120147
121−hashPassword ::
122− forall r.
123− Password ->
124− Run
125− ( cryptoFunctions :: Reader CryptoFunctions
126− , aff :: Aff
127− , effect :: Effect
128− | r
129− )
130− Hash
148+bwPasswordStringHash
149+ :: forall r
150+ . PreloginResponse
151+ -> Email
152+ -> Password
153+ -> Run
154+ ( crypto :: Reader CryptoService
155+ , effect :: Effect
156+ , aff :: Aff
157+ | r
158+ )
159+ StringHash
160+bwPasswordStringHash prelogin email password = do
161+ key <- makePreloginKey prelogin email password
162+ crypto <- askAt (Proxy :: _ "crypto")
163+ _localHashedPassword <-
164+ liftPromise
165+ $ runFn3 crypto.hashPassword password key (nullify hashPurposeLocalAuthorization)
166+ liftPromise $ runFn3 crypto.hashPassword password key jnull
167+
168+hashPassword
169+ :: forall r
170+ . Password
171+ -> Run
172+ ( cryptoFunctions :: Reader CryptoFunctions
173+ , aff :: Aff
174+ , effect :: Effect
175+ | r
176+ )
177+ Hash
131178hashPassword (Password password) = do
132179 cryptoFunctions <- askAt (Proxy :: _ "cryptoFunctions")
133180 liftPromise $ runFn2 cryptoFunctions.hash password cryptoFunctionsTypeSha512
134181
135−decrypt ::
136− forall r.
137− EncryptedString ->
138− Run
139− ( key :: Reader SymmetricCryptoKey
140− , crypto :: Reader CryptoService
141− , aff :: Aff
142− , effect :: Effect
143− | r
144− )
145− String
182+decrypt
183+ :: forall r
184+ . EncryptedString
185+ -> Run
186+ ( key :: Reader SymmetricCryptoKey
187+ , crypto :: Reader CryptoService
188+ , aff :: Aff
189+ , effect :: Effect
190+ | r
191+ )
192+ String
146193decrypt input = do
147194 crypto <- askAt (Proxy :: _ "crypto")
148195 key <- askAt (Proxy :: _ "key")
149196 liftPromise $ runFn2 crypto.decryptToUtf8 (EncString.fromString input) key
150197
151−encrypt ::
152− forall r.
153− String ->
154− Run
155− ( key :: Reader SymmetricCryptoKey
156− , crypto :: Reader CryptoService
157− , aff :: Aff
158− , effect :: Effect
159− | r
160− )
161− EncryptedString
198+encrypt
199+ :: forall r
200+ . String
201+ -> Run
202+ ( key :: Reader SymmetricCryptoKey
203+ , crypto :: Reader CryptoService
204+ , aff :: Aff
205+ , effect :: Effect
206+ | r
207+ )
208+ EncryptedString
162209encrypt input = do
163210 crypto <- askAt (Proxy :: _ "crypto")
164211 key <- askAt (Proxy :: _ "key")
165212 map EncString.toString $ liftPromise $ runFn2 crypto.encrypt input key
166213
167−liftPromise ∷ forall m a. MonadAff m ⇒ Promise a → m a
214+liftPromise :: forall m a. MonadAff m => Promise a -> m a
168215liftPromise = liftAff <<< Promise.toAff
169216
170−encodeCipher ::
171− forall r.
172− Bridge.FullCipher ->
173− Run
174− ( key :: Reader SymmetricCryptoKey
175− , crypto :: Reader CryptoService
176− , aff :: Aff
177− , effect :: Effect
178− | r
179− )
180− CipherResponse
181−encodeCipher (Bridge.FullCipher { name, cipher, id, favorite, reprompt }) = do
217+encodeCipher
218+ :: forall r
219+ . Bridge.FullCipher
220+ -> Run
221+ ( key :: Reader SymmetricCryptoKey
222+ , crypto :: Reader CryptoService
223+ , aff :: Aff
224+ , effect :: Effect
225+ | r
226+ )
227+ CipherResponse
228+encodeCipher
229+ (Bridge.FullCipher { name, cipher, id, favorite, reprompt, collectionIds }) = do
182230 let
183231 f :: CipherResponse -> Run _ CipherResponse
184232 f x = case cipher of
@@ -192,14 +240,14 @@ encodeCipher (Bridge.FullCipher { name, cipher, id, favorite, reprompt }) = do
192240 pure
193241 x
194242 { card =
195− nullify
196− { cardholderName
197− , brand
198− , number
199− , expMonth
200− , expYear
201− , code
202− }
243+ nullify
244+ { cardholderName
245+ , brand
246+ , number
247+ , expMonth
248+ , expYear
249+ , code
250+ }
203251 , type = cipherTypeCard
204252 }
205253 Bridge.IdentityCipher (Bridge.Cipher_IdentityCipher identity) -> do
@@ -224,26 +272,26 @@ encodeCipher (Bridge.FullCipher { name, cipher, id, favorite, reprompt }) = do
224272 pure
225273 x
226274 { identity =
227− nullify
228− { address1
229− , address2
230− , address3
231− , city
232− , company
233− , country
234− , email
235− , firstName
236− , lastName
237− , licenseNumber
238− , middleName
239− , passportNumber
240− , phone
241− , postalCode
242− , ssn
243− , state
244− , title
245− , username
246− }
275+ nullify
276+ { address1
277+ , address2
278+ , address3
279+ , city
280+ , company
281+ , country
282+ , email
283+ , firstName
284+ , lastName
285+ , licenseNumber
286+ , middleName
287+ , passportNumber
288+ , phone
289+ , postalCode
290+ , ssn
291+ , state
292+ , title
293+ , username
294+ }
247295 , type = cipherTypeIdentity
248296 }
249297 Bridge.LoginCipher (Bridge.Cipher_LoginCipher login) -> do
@@ -251,22 +299,23 @@ encodeCipher (Bridge.FullCipher { name, cipher, id, favorite, reprompt }) = do
251299 uris <-
252300 traverse
253301 ( \uri -> do
254− uriEnc <- encrypt uri
302+ uriEnc <- nullify <$> encrypt uri
255303 pure { uri: uriEnc, match: jnull }
256304 )
257305 (unwrap login.uris)
258306 username <- encryptNullable login.username
307+ totp <- encryptNullable login.totp
259308 pure
260309 x
261310 { login =
262− nullify
263− { password
264− , uris: JOpt $ opt uris
265− , username
266− , passwordRevisionDate: jnull
267− , totp: jnull
268− , autofillOnPageLoad: JOpt undefined
269− }
311+ nullify
312+ { password
313+ , uris: JOpt $ opt uris
314+ , username
315+ , passwordRevisionDate: jnull
316+ , totp: totp
317+ , autofillOnPageLoad: JOpt undefined
318+ }
270319 , type = cipherTypeLogin
271320 }
272321 Bridge.NoteCipher note -> do
@@ -296,137 +345,168 @@ encodeCipher (Bridge.FullCipher { name, cipher, id, favorite, reprompt }) = do
296345 , revisionDate: jnull
297346 , attachments: jnull
298347 , passwordHistory: jnull
299− , collectionIds: jnull
348+ , collectionIds: nullify $ unwrap collectionIds
300349 , deletedDate: jnull
301350 , reprompt
302351 }
303352
304−decodeCipher ::
305− forall r.
306− CipherResponse ->
307− Run
308− ( key :: Reader SymmetricCryptoKey
309− , crypto :: Reader CryptoService
310− , aff :: Aff
311− , effect :: Effect
312− | r
313− )
314− Bridge.FullCipher
353+-- | Check if the cipher is supported by Warden.
354+supportedCipher :: CipherResponse -> Boolean
355+-- We do not support organizations since that requires managing a separate set of
356+-- encryption keys
357+supportedCipher { organizationId } | Just _ <- JNullable.toMaybe organizationId = false
358+supportedCipher _ = true
359+
360+decodeCipher
361+ :: forall r
362+ . CipherResponse
363+ -> Run
364+ ( key :: Reader SymmetricCryptoKey
365+ , crypto :: Reader CryptoService
366+ , aff :: Aff
367+ , effect :: Effect
368+ | r
369+ )
370+ (Maybe Bridge.FullCipher)
371+decodeCipher cipher | not (supportedCipher cipher) =
372+ pure $ Nothing
315373decodeCipher cipher = do
316374 name <- decrypt cipher.name
317375 cipherType <- case cipher.type of
318376 n
319377 | cipherTypeSecureNote == n -> do
320− note <- fromJNullable "" <$> (traverse decrypt cipher.notes)
321− pure $ Bridge.NoteCipher note
378+ note <- fromJNullable "" <$> (traverse decrypt cipher.notes)
379+ pure $ Bridge.NoteCipher note
322380 n
323381 | cipherTypeLogin == n -> do
324− case JNullable.toMaybe cipher.login of
325− Nothing -> liftEffect $ throwError $ error "Login data is missing"
326− Just login -> do
327− username <- decryptNullable login.username
328− password <- decryptNullable login.password
329− uris <- fromJOpt [] <$> ((traverse >>> traverse) (_.uri >>> decrypt) login.uris)
330− pure $ Bridge.LoginCipher
331− $ Bridge.Cipher_LoginCipher
332− { username
333− , password
334− , uris: wrap uris
335− }
382+ case JNullable.toMaybe cipher.login of
383+ Nothing -> liftEffect $ throwError $ error "Login data is missing"
384+ Just login -> do
385+ username <- decryptNullable login.username
386+ password <- decryptNullable login.password
387+ totp <- decryptNullable login.totp
388+ uris <- fromJOpt [] <$>
389+ ((traverse >>> traverse) (_.uri >>> JNullable.toMaybe >>> maybe (pure "") decrypt) login.uris)
390+ pure $ Bridge.LoginCipher
391+ $ Bridge.Cipher_LoginCipher
392+ { username
393+ , password
394+ , uris: wrap uris
395+ , totp
396+ }
336397 n
337398 | cipherTypeCard == n -> do
338− case JNullable.toMaybe cipher.card of
339− Nothing -> liftEffect $ throwError $ error "Card data is missing"
340− Just card -> do
341− cardholderName <- decryptNullable card.cardholderName
342− number <- decryptNullable card.number
343− code <- decryptNullable card.code
344− brand <- decryptNullable card.brand
345− expMonth <- decryptNullable card.expMonth
346− expYear <- decryptNullable card.expYear
347− pure $ Bridge.CardCipher
348− $ Bridge.Cipher_CardCipher
349− { brand, cardholderName, code, expMonth, expYear, number }
399+ case JNullable.toMaybe cipher.card of
400+ Nothing -> liftEffect $ throwError $ error "Card data is missing"
401+ Just card -> do
402+ cardholderName <- decryptNullable card.cardholderName
403+ number <- decryptNullable card.number
404+ code <- decryptNullable card.code
405+ brand <- decryptNullable card.brand
406+ expMonth <- decryptNullable card.expMonth
407+ expYear <- decryptNullable card.expYear
408+ pure $ Bridge.CardCipher
409+ $ Bridge.Cipher_CardCipher
410+ { brand, cardholderName, code, expMonth, expYear, number }
350411 n
351412 | cipherTypeIdentity == n -> do
352− case JNullable.toMaybe cipher.identity of
353− Nothing -> liftEffect $ throwError $ error "Identity data is missing"
354− Just identity -> do
355− firstName <- decryptNullable identity.firstName
356− middleName <- decryptNullable identity.middleName
357− lastName <- decryptNullable identity.lastName
358− address1 <- decryptNullable identity.address1
359− address2 <- decryptNullable identity.address2
360− address3 <- decryptNullable identity.address3
361− city <- decryptNullable identity.city
362− company <- decryptNullable identity.company
363− country <- decryptNullable identity.country
364− email <- decryptNullable identity.email
365− licenseNumber <- decryptNullable identity.licenseNumber
366− passportNumber <- decryptNullable identity.passportNumber
367− phone <- decryptNullable identity.phone
368− postalCode <- decryptNullable identity.postalCode
369− ssn <- decryptNullable identity.ssn
370− state <- decryptNullable identity.state
371− title <- decryptNullable identity.title
372− username <- decryptNullable identity.username
373− pure $ Bridge.IdentityCipher
374− $ wrap
375− { firstName
376− , middleName
377− , lastName
378− , address1
379− , address2
380− , address3
381− , city
382− , company
383− , country
384− , email
385− , licenseNumber
386− , passportNumber
387− , phone
388− , postalCode
389− , ssn
390− , state
391− , title
392− , username
393− }
413+ case JNullable.toMaybe cipher.identity of
414+ Nothing -> liftEffect $ throwError $ error "Identity data is missing"
415+ Just identity -> do
416+ firstName <- decryptNullable identity.firstName
417+ middleName <- decryptNullable identity.middleName
418+ lastName <- decryptNullable identity.lastName
419+ address1 <- decryptNullable identity.address1
420+ address2 <- decryptNullable identity.address2
421+ address3 <- decryptNullable identity.address3
422+ city <- decryptNullable identity.city
423+ company <- decryptNullable identity.company
424+ country <- decryptNullable identity.country
425+ email <- decryptNullable identity.email
426+ licenseNumber <- decryptNullable identity.licenseNumber
427+ passportNumber <- decryptNullable identity.passportNumber
428+ phone <- decryptNullable identity.phone
429+ postalCode <- decryptNullable identity.postalCode
430+ ssn <- decryptNullable identity.ssn
431+ state <- decryptNullable identity.state
432+ title <- decryptNullable identity.title
433+ username <- decryptNullable identity.username
434+ pure $ Bridge.IdentityCipher
435+ $ wrap
436+ { firstName
437+ , middleName
438+ , lastName
439+ , address1
440+ , address2
441+ , address3
442+ , city
443+ , company
444+ , country
445+ , email
446+ , licenseNumber
447+ , passportNumber
448+ , phone
449+ , postalCode
450+ , ssn
451+ , state
452+ , title
453+ , username
454+ }
394455 n -> liftEffect $ throwError $ error $ "Unsupported cipher type: " <> show n
395− pure
456+ pure $ Just
396457 $ wrap
397458 { cipher: cipherType
398459 , name
399460 , id: cipher.id
400461 , favorite: cipher.favorite
401462 , reprompt: cipher.reprompt
463+ , collectionIds: wrap <<< fromJNullable [] $ cipher.collectionIds
402464 }
403465
404−decryptNullable ::
405− forall x r.
406− Newtype x (Maybe String) =>
407− JNullable EncryptedString ->
408− Run
409− ( key :: Reader SymmetricCryptoKey
410− , crypto :: Reader CryptoService
411− , aff :: Aff
412− , effect :: Effect
413− | r
414− )
415− x
466+decryptNullable
467+ :: forall x r
468+ . Newtype x (Maybe String)
469+ => JNullable EncryptedString
470+ -> Run
471+ ( key :: Reader SymmetricCryptoKey
472+ , crypto :: Reader CryptoService
473+ , aff :: Aff
474+ , effect :: Effect
475+ | r
476+ )
477+ x
416478decryptNullable x = wrap <<< fromJNullable Nothing <<< map Just <$> (traverse decrypt x)
417479
418−encryptNullable ::
419− forall x r.
420− Newtype x (Maybe String) =>
421− x ->
422− Run
423− ( key :: Reader SymmetricCryptoKey
424− , crypto :: Reader CryptoService
425− , aff :: Aff
426− , effect :: Effect
427− | r
428− )
429− (JNullable EncryptedString)
480+encryptNullable
481+ :: forall x r
482+ . Newtype x (Maybe String)
483+ => x
484+ -> Run
485+ ( key :: Reader SymmetricCryptoKey
486+ , crypto :: Reader CryptoService
487+ , aff :: Aff
488+ , effect :: Effect
489+ | r
490+ )
491+ (JNullable EncryptedString)
430492encryptNullable x = case unwrap x of
431493 Nothing -> pure jnull
432494 Just y -> nullify <$> encrypt y
495+
496+getDeviceID ::
497+ forall r.
498+ Run
499+ ( effect :: Effect
500+ , storage :: Reader Storage
501+ | r
502+ )
503+ String
504+getDeviceID = do
505+ storage <- askAt (Proxy :: _ "storage")
506+ maybeId <- liftEffect $ Storage.get storage DeviceIDKey
507+ case maybeId of
508+ Just id -> pure id
509+ Nothing -> do
510+ id <- map Number.toString $ liftEffect $ random
511+ liftEffect $ Storage.store storage DeviceIDKey id
512+ pure id
backend/BW/Types.purs+445−390
@@ -2,14 +2,27 @@ module BW.Types where
22
33import Prelude
44
5+import Bridge as Bridge
56import Data.Argonaut (class DecodeJson, class EncodeJson)
7+import Data.Either (Either)
8+import Data.Either as Either
9+import Data.Int as Int
610import Data.JNullable (JNullable)
711import Data.JOpt (JOpt)
12+import Data.Maybe (Maybe(..))
13+import Data.Nullable (Nullable)
814import Data.ShowableJson (ShowableJson)
915import Data.Timestamp (Timestamp)
16+import Data.Traversable (for)
17+import Data.Tuple.Nested ((/\))
18+import Foreign as Foreign
19+import Literals.Undefined (Undefined)
20+import Type.Prelude (Proxy(..))
21+import Untagged.TypeCheck (class HasRuntimeType)
22+import Untagged.Union (type (|+|))
23+import Untagged.Union as Union
1024
11−newtype EncryptedString
12− = EncryptedString String
25+newtype EncryptedString = EncryptedString String
1326
1427derive newtype instance Show EncryptedString
1528derive newtype instance Ord EncryptedString
@@ -18,78 +31,86 @@ derive newtype instance EncodeJson EncryptedString
1831derive newtype instance DecodeJson EncryptedString
1932
2033kdfPBKDF2_SHA256 = 0 :: KDF
34+kdfArgon2id = 1 :: KDF
2135
22−type KDF
23− = Int
24−
25−type PreloginResponse
26− = { kdf :: KDF
27− , kdfIterations :: Int
28− }
29−
30−type PreloginRequest
31− = { email :: String
32− }
33−
34−type ProfileResponse
35− = { id :: String
36− , name :: String
37− , email :: String
38− , emailVerified :: Boolean
39− , masterPasswordHint :: JNullable String
40− , premiumPersonally :: Boolean
41− -- , premiumFromOrganization :: Boolean
42− , culture :: String
43− , twoFactorEnabled :: Boolean
44− , key :: EncryptedString
45− , privateKey :: EncryptedString
46− , securityStamp :: String
47− , forcePasswordReset :: Boolean
48− , usesKeyConnector :: Boolean
49− , organizations :: Array ProfileOrganizationResponse
50− , providers :: Array ProfileProviderResponse
51− , providerOrganizations :: Array ProfileProviderOrganizationResponse
52− }
53−
54−type ProfileOrganizationResponse
55− = { id :: String
56− , name :: String
57− , usePolicies :: Boolean
58− , useGroups :: Boolean
59− , useDirectory :: Boolean
60− , useEvents :: Boolean
61− , useTotp :: Boolean
62− , use2fa :: Boolean
63− , useApi :: Boolean
64− , useSso :: Boolean
65− , useKeyConnector :: Boolean
66− , useResetPassword :: Boolean
67− , selfHost :: Boolean
68− , usersGetPremium :: Boolean
69− , seats :: Int
70− , maxCollections :: Int
71− , maxStorageGb :: JNullable Int
72− , key :: String
73− , hasPublicAndPrivateKeys :: Boolean
74− , status :: OrganizationUserStatusType
75− , type :: OrganizationUserType
76− , enabled :: Boolean
77− , ssoBound :: Boolean
78− , identifier :: String
79− , permissions :: PermissionsApi
80− , resetPasswordEnrolled :: Boolean
81− , userId :: String
82− , providerId :: String
83− , providerName :: String
84− , familySponsorshipFriendlyName :: String
85− , familySponsorshipAvailable :: Boolean
86− , planProductType :: ProductType
87− , keyConnectorEnabled :: Boolean
88− , keyConnectorUrl :: String
89− , familySponsorshipLastSyncDate :: JNullable Timestamp
90− , familySponsorshipValidUntil :: JNullable Timestamp
91− , familySponsorshipToDelete :: JNullable Boolean
92− }
36+type KDF = Int
37+
38+type PreloginResponse =
39+ { kdf :: KDF
40+ , kdfIterations :: Int
41+ , kdfMemory :: JNullable Int
42+ , kdfParallelism :: JNullable Int
43+ }
44+
45+type KdfConfig =
46+ { iterations :: Int
47+ , memory :: JNullable Int
48+ , parallelism :: JNullable Int
49+ }
50+
51+type PreloginRequest =
52+ { email :: Email
53+ }
54+
55+type ProfileResponse =
56+ { id :: String
57+ , name :: JNullable String
58+ , email :: String
59+ , emailVerified :: Boolean
60+ , masterPasswordHint :: JNullable String
61+ -- , premiumPersonally :: Boolean
62+ -- , premiumFromOrganization :: Boolean
63+ , culture :: JNullable String
64+ , twoFactorEnabled :: Boolean
65+ , key :: EncryptedString
66+ , privateKey :: EncryptedString
67+ , securityStamp :: JNullable String
68+ , forcePasswordReset :: Boolean
69+ , usesKeyConnector :: Boolean
70+ -- , organizations :: Array ProfileOrganizationResponse
71+ , providers :: Array ProfileProviderResponse
72+ , providerOrganizations :: Array ProfileProviderOrganizationResponse
73+ }
74+
75+type ProfileOrganizationResponse =
76+ { id :: String
77+ , name :: JNullable String
78+ , usePolicies :: JNullable Boolean
79+ , useGroups :: JNullable Boolean
80+ , useDirectory :: JNullable Boolean
81+ , useEvents :: JNullable Boolean
82+ , useTotp :: JNullable Boolean
83+ , use2fa :: JNullable Boolean
84+ , useApi :: JNullable Boolean
85+ , useSso :: JNullable Boolean
86+ , useKeyConnector :: JNullable Boolean
87+ , useResetPassword :: JNullable Boolean
88+ , selfHost :: JNullable Boolean
89+ , usersGetPremium :: JNullable Boolean
90+ , seats :: JNullable Int
91+ , maxCollections :: JNullable Int
92+ , maxStorageGb :: JNullable Int
93+ , key :: JNullable String
94+ , hasPublicAndPrivateKeys :: JNullable Boolean
95+ , status :: JNullable OrganizationUserStatusType
96+ , type :: JNullable OrganizationUserType
97+ , enabled :: JNullable Boolean
98+ , ssoBound :: JNullable Boolean
99+ , identifier :: JNullable String
100+ , permissions :: JNullable PermissionsApi
101+ , resetPasswordEnrolled :: JNullable Boolean
102+ , userId :: JNullable String
103+ , providerId :: JNullable String
104+ , providerName :: JNullable String
105+ , familySponsorshipFriendlyName :: JNullable String
106+ , familySponsorshipAvailable :: JNullable Boolean
107+ , planProductType :: JNullable ProductType
108+ , keyConnectorEnabled :: JNullable Boolean
109+ , keyConnectorUrl :: JNullable String
110+ , familySponsorshipLastSyncDate :: JNullable Timestamp
111+ , familySponsorshipValidUntil :: JNullable Timestamp
112+ , familySponsorshipToDelete :: JNullable Boolean
113+ }
93114
94115organizationUserStatusTypeInvited = 0 :: OrganizationUserStatusType
95116
@@ -97,8 +118,7 @@ organizationUserStatusTypeAccepted = 0 :: OrganizationUserStatusType
97118
98119organizationUserStatusTypeConfirmed = 0 :: OrganizationUserStatusType
99120
100−type OrganizationUserStatusType
101− = Int
121+type OrganizationUserStatusType = Int
102122
103123organizationUserTypeOwner = 0 :: OrganizationUserType
104124
@@ -110,27 +130,26 @@ organizationUserTypeManager = 3 :: OrganizationUserType
110130
111131organizationUserTypeCustom = 4 :: OrganizationUserType
112132
113−type OrganizationUserType
114− = Int
115−
116−type PermissionsApi
117− = { accessEventLogs :: Boolean
118− , accessImportExport :: Boolean
119− , accessReports :: Boolean
120− , manageAllCollections :: Boolean
121− , createNewCollections :: Boolean
122− , editAnyCollection :: Boolean
123− , deleteAnyCollection :: Boolean
124− , manageAssignedCollections :: Boolean
125− , editAssignedCollections :: Boolean
126− , deleteAssignedCollections :: Boolean
127− , manageCiphers :: Boolean
128− , manageGroups :: Boolean
129− , manageSso :: Boolean
130− , managePolicies :: Boolean
131− , manageUsers :: Boolean
132− , manageResetPassword :: Boolean
133− }
133+type OrganizationUserType = Int
134+
135+type PermissionsApi =
136+ { accessEventLogs :: Boolean
137+ , accessImportExport :: Boolean
138+ , accessReports :: Boolean
139+ , manageAllCollections :: Boolean
140+ , createNewCollections :: Boolean
141+ , editAnyCollection :: Boolean
142+ , deleteAnyCollection :: Boolean
143+ , manageAssignedCollections :: Boolean
144+ , editAssignedCollections :: Boolean
145+ , deleteAssignedCollections :: Boolean
146+ , manageCiphers :: Boolean
147+ , manageGroups :: Boolean
148+ , manageSso :: Boolean
149+ , managePolicies :: Boolean
150+ , manageUsers :: Boolean
151+ , manageResetPassword :: Boolean
152+ }
134153
135154productTypeFree = 0 :: ProductType
136155
@@ -140,20 +159,19 @@ productTypeTeams = 2 :: ProductType
140159
141160productTypeEnterprise = 3 :: ProductType
142161
143−type ProductType
144− = Int
145−
146−type ProfileProviderResponse
147− = { id :: String
148− , name :: String
149− , key :: String
150− , status :: ProviderUserStatusType
151− , type :: ProviderUserType
152− , enabled :: Boolean
153− , permissions :: PermissionsApi
154− , userId :: String
155− , useEvents :: Boolean
156− }
162+type ProductType = Int
163+
164+type ProfileProviderResponse =
165+ { id :: String
166+ , name :: JNullable String
167+ , key :: JNullable String
168+ , status :: JNullable ProviderUserStatusType
169+ , type :: JNullable ProviderUserType
170+ , enabled :: Boolean
171+ , permissions :: PermissionsApi
172+ , userId :: JNullable String
173+ , useEvents :: Boolean
174+ }
157175
158176providerUserStatusTypeInvited = 0 :: ProviderUserStatusType
159177
@@ -161,81 +179,79 @@ providerUserStatusTypeAccepted = 1 :: ProviderUserStatusType
161179
162180providerUserStatusTypeConfirmed = 2 :: ProviderUserStatusType
163181
164−type ProviderUserStatusType
165− = Int
182+type ProviderUserStatusType = Int
166183
167184providerUserTypeProviderAdmin = 0 :: ProviderUserType
168185
169186providerUserTypeServiceUser = 1 :: ProviderUserType
170187
171−type ProviderUserType
172− = Int
173−
174−type ProfileProviderOrganizationResponse
175− = { id :: String
176− , name :: String
177− , usePolicies :: Boolean
178− , useGroups :: Boolean
179− , useDirectory :: Boolean
180− , useEvents :: Boolean
181− , useTotp :: Boolean
182− , use2fa :: Boolean
183− , useApi :: Boolean
184− , useSso :: Boolean
185− , useKeyConnector :: Boolean
186− , useResetPassword :: Boolean
187− , selfHost :: Boolean
188− , usersGetPremium :: Boolean
189− , seats :: Int
190− , maxCollections :: Int
191− , maxStorageGb :: JNullable Int
192− , key :: String
193− , hasPublicAndPrivateKeys :: Boolean
194− , status :: OrganizationUserStatusType
195− , type :: OrganizationUserType
196− , enabled :: Boolean
197− , ssoBound :: Boolean
198− , identifier :: String
199− , permissions :: PermissionsApi
200− , resetPasswordEnrolled :: Boolean
201− , userId :: String
202− , providerId :: String
203− , providerName :: String
204− , familySponsorshipFriendlyName :: String
205− , familySponsorshipAvailable :: Boolean
206− , planProductType :: ProductType
207− , keyConnectorEnabled :: Boolean
208− , keyConnectorUrl :: String
209− , familySponsorshipLastSyncDate :: JNullable Timestamp
210− , familySponsorshipValidUntil :: JNullable Timestamp
211− , familySponsorshipToDelete :: JNullable Boolean
212− }
213−
214−type Urls
215− = { base :: JNullable String
216− , webVault :: JNullable String
217− , api :: JNullable String
218− , identity :: JNullable String
219− , icons :: JNullable String
220− , notifications :: JNullable String
221− , events :: JNullable String
222− , keyConnector :: JNullable String
223− }
224−
225−type PasswordTokenRequest
226− = { email :: Email
227− , masterPasswordHash :: String
228− , captchaResponse :: String
229− , twoFactor :: TokenRequestTwoFactor
230− , device :: DeviceRequest
231− }
232−
233−type DeviceRequest
234− = { type :: DeviceType
235− , name :: String
236− , identifier :: String
237− , pushToken :: JNullable String
238− }
188+type ProviderUserType = Int
189+
190+type ProfileProviderOrganizationResponse =
191+ { id :: String
192+ , name :: JNullable String
193+ , usePolicies :: Boolean
194+ , useGroups :: Boolean
195+ , useDirectory :: Boolean
196+ , useEvents :: Boolean
197+ , useTotp :: Boolean
198+ , use2fa :: Boolean
199+ , useApi :: Boolean
200+ , useSso :: Boolean
201+ , useKeyConnector :: Boolean
202+ , useResetPassword :: Boolean
203+ , selfHost :: Boolean
204+ , usersGetPremium :: Boolean
205+ , seats :: Int
206+ , maxCollections :: Int
207+ , maxStorageGb :: JNullable Int
208+ , key :: JNullable String
209+ , hasPublicAndPrivateKeys :: Boolean
210+ , status :: OrganizationUserStatusType
211+ , type :: OrganizationUserType
212+ , enabled :: Boolean
213+ , ssoBound :: Boolean
214+ , identifier :: JNullable String
215+ , permissions :: PermissionsApi
216+ , resetPasswordEnrolled :: Boolean
217+ , userId :: JNullable String
218+ , providerId :: JNullable String
219+ , providerName :: JNullable String
220+ , familySponsorshipFriendlyName :: JNullable String
221+ , familySponsorshipAvailable :: Boolean
222+ , planProductType :: ProductType
223+ , keyConnectorEnabled :: Boolean
224+ , keyConnectorUrl :: JNullable String
225+ , familySponsorshipLastSyncDate :: JNullable Timestamp
226+ , familySponsorshipValidUntil :: JNullable Timestamp
227+ , familySponsorshipToDelete :: JNullable Boolean
228+ }
229+
230+type Urls =
231+ { base :: JNullable String
232+ , webVault :: JNullable String
233+ , api :: JNullable String
234+ , identity :: JNullable String
235+ , icons :: JNullable String
236+ , notifications :: JNullable String
237+ , events :: JNullable String
238+ , keyConnector :: JNullable String
239+ }
240+
241+type PasswordTokenRequest =
242+ { email :: Email
243+ , masterPasswordHash :: String
244+ , captchaResponse :: String
245+ , twoFactor :: TokenRequestTwoFactor
246+ , device :: DeviceRequest
247+ }
248+
249+type DeviceRequest =
250+ { type :: DeviceType
251+ , name :: String
252+ , identifier :: String
253+ , pushToken :: JNullable String
254+ }
239255
240256deviceTypeAndroid = 0 :: DeviceType
241257
@@ -279,14 +295,13 @@ deviceTypeVivaldiExtension = 19 :: DeviceType
279295
280296deviceTypeSafariExtension = 20 :: DeviceType
281297
282−type DeviceType
283− = Int
298+type DeviceType = Int
284299
285−type TokenRequestTwoFactor
286− = { provider :: TwoFactorProviderType
287− , token :: String
288− , remember :: Boolean
289− }
300+type TokenRequestTwoFactor =
301+ { provider :: TwoFactorProviderType
302+ , token :: String
303+ , remember :: Boolean
304+ }
290305
291306twoFactorProviderTypeAuthenticator = 0 :: TwoFactorProviderType
292307
@@ -304,24 +319,51 @@ twoFactorProviderTypeOrganizationDuo = 6 :: TwoFactorProviderType
304319
305320twoFactorProviderTypeWebAuthn = 7 :: TwoFactorProviderType
306321
307−type TwoFactorProviderType
308− = Int
309−
310−newtype Password
311− = Password String
322+type TwoFactorProviderType = Int
323+
324+secondFactorTypeToBridge
325+ :: TwoFactorProviderType
326+ -> Maybe Bridge.TwoFactorProviderType
327+secondFactorTypeToBridge x = case x of
328+ n
329+ | n == twoFactorProviderTypeAuthenticator -> Just Bridge.Authenticator
330+ n
331+ | n == twoFactorProviderTypeEmail -> Just Bridge.Email
332+ n
333+ | n == twoFactorProviderTypeDuo -> Just Bridge.Duo
334+ n
335+ | n == twoFactorProviderTypeYubikey -> Just Bridge.Yubikey
336+ n
337+ | n == twoFactorProviderTypeU2f -> Just Bridge.U2f
338+ n
339+ | n == twoFactorProviderTypeRemember -> Just Bridge.Remember
340+ n
341+ | n == twoFactorProviderTypeOrganizationDuo -> Just Bridge.OrganizationDuo
342+ n
343+ | n == twoFactorProviderTypeWebAuthn -> Just Bridge.WebAuthn
344+ _ -> Nothing
345+
346+bridgeToSecondFactorType :: Bridge.TwoFactorProviderType -> TwoFactorProviderType
347+bridgeToSecondFactorType Bridge.Authenticator = twoFactorProviderTypeAuthenticator
348+bridgeToSecondFactorType Bridge.Email = twoFactorProviderTypeEmail
349+bridgeToSecondFactorType Bridge.Duo = twoFactorProviderTypeDuo
350+bridgeToSecondFactorType Bridge.Yubikey = twoFactorProviderTypeYubikey
351+bridgeToSecondFactorType Bridge.U2f = twoFactorProviderTypeU2f
352+bridgeToSecondFactorType Bridge.Remember = twoFactorProviderTypeRemember
353+bridgeToSecondFactorType Bridge.OrganizationDuo = twoFactorProviderTypeOrganizationDuo
354+bridgeToSecondFactorType Bridge.WebAuthn = twoFactorProviderTypeWebAuthn
355+
356+newtype Password = Password String
312357
313358hashPurposeServerAuthorization = 1 :: HashPurpose
314359
315360hashPurposeLocalAuthorization = 2 :: HashPurpose
316361
317−type HashPurpose
318− = Int
362+type HashPurpose = Int
319363
320−newtype StringHash
321− = StringHash String
364+newtype StringHash = StringHash String
322365
323−newtype AccessToken
324− = AccessToken String
366+newtype AccessToken = AccessToken String
325367
326368derive newtype instance Show AccessToken
327369derive newtype instance Eq AccessToken
@@ -329,8 +371,7 @@ derive newtype instance Ord AccessToken
329371derive newtype instance EncodeJson AccessToken
330372derive newtype instance DecodeJson AccessToken
331373
332−newtype RefreshToken
333− = RefreshToken String
374+newtype RefreshToken = RefreshToken String
334375
335376derive newtype instance Show RefreshToken
336377derive newtype instance Eq RefreshToken
@@ -342,75 +383,91 @@ type IdentityCaptchaResponse =
342383 { siteKey :: String
343384 }
344385
345−type IdentityTokenResponse
346− = { accessToken :: AccessToken
347− , expiresIn :: Int
348− , refreshToken :: RefreshToken
349− , tokenType :: String
350− , resetMasterPassword :: Boolean
351− , privateKey :: EncryptedString
352− , key :: EncryptedString
353− , twoFactorToken :: JNullable String
354− , kdf :: KDF
355− , kdfIterations :: Int
356− , forcePasswordReset :: JNullable Boolean
357− , apiUseKeyConnector :: JNullable Boolean
358− , keyConnectorUrl :: JNullable String
359− }
360−
361−newtype Email
362− = Email String
363−
364−type SyncResponse
365− = { profile :: ProfileResponse
366− , folders :: Array FolderResponse
367− , collections :: Array CollectionDetailsResponse
368− , ciphers :: Array CipherResponse
369− , domains :: JNullable DomainsResponse
370− , policies :: JNullable (Array PolicyResponse)
371− , sends :: Array SendResponse
372− }
373−
374−type FolderResponse
375− = { id :: String
376− , name :: String
377− , revisionDate :: String
378− }
379−
380−type CollectionDetailsResponse
381− = { id :: String
382− , organizationId :: JNullable String
383− , name :: String
384− , externalId :: JNullable String
385− , readOnly :: Boolean
386− }
386+type IdentityTokenResponse =
387+ { accessToken :: AccessToken
388+ , expiresIn :: Int
389+ , refreshToken :: RefreshToken
390+ , tokenType :: String
391+ , resetMasterPassword :: Boolean
392+ , privateKey :: EncryptedString
393+ , key :: EncryptedString
394+ , twoFactorToken :: JNullable String
395+ , kdf :: KDF
396+ , kdfIterations :: Int
397+ , forcePasswordReset :: JNullable Boolean
398+ , apiUseKeyConnector :: JNullable Boolean
399+ , keyConnectorUrl :: JNullable String
400+ }
401+
402+newtype TwoFactorProviderTypes = TwoFactorProviderTypes (Array (Int |+| String))
403+
404+instance HasRuntimeType TwoFactorProviderTypes where
405+ hasRuntimeType Proxy = Foreign.isArray
406+
407+fromTwoFactorProviderType :: TwoFactorProviderTypes -> Either String (Array Bridge.TwoFactorProviderType)
408+fromTwoFactorProviderType (TwoFactorProviderTypes xs) =
409+ for xs $ \rawProvider -> do
410+ providerInt <- Union.reduce (Either.Right /\ (\x -> Either.note x $ Int.fromString x)) rawProvider
411+ Either.note (show providerInt) $ secondFactorTypeToBridge providerInt
412+
413+type IdentityTwoFactorResponse =
414+ { twoFactorProviders :: TwoFactorProviderTypes
415+ -- it is a Map type and untaged unions gets confused
416+ -- , twoFactorProviders2 :: Object (Object String)
417+ , captchaToken :: String |+| Undefined
418+ }
419+
420+newtype Email = Email String
421+
422+type SyncResponse =
423+ { profile :: ProfileResponse
424+ , folders :: Array FolderResponse
425+ , collections :: Array CollectionDetailsResponse
426+ , ciphers :: Array CipherResponse
427+ , domains :: JNullable DomainsResponse
428+ , policies :: JNullable (Array PolicyResponse)
429+ , sends :: Array SendResponse
430+ }
387431
432+type FolderResponse =
433+ { id :: String
434+ , name :: JNullable String
435+ , revisionDate :: JNullable String
436+ }
437+
438+type CollectionDetailsResponse =
439+ { id :: String
440+ , organizationId :: JNullable String
441+ , name :: JNullable String
442+ , externalId :: JNullable String
443+ , readOnly :: Boolean
444+ }
388445
389446type CipherId = String
390447
391−type CipherResponse
392− = { id :: CipherId
393− , organizationId :: JNullable String
394− , folderId :: JNullable String
395− , type :: CipherType
396− , name :: EncryptedString
397− , notes :: JNullable EncryptedString
398− , fields :: JNullable (Array FieldApi)
399− , login :: JNullable LoginApi
400− , card :: JNullable CardApi
401− , identity :: JNullable IdentityApi
402− , secureNote :: JNullable SecureNoteApi
403− , favorite :: Boolean
404− , edit :: JNullable Boolean
405− , viewPassword :: JNullable Boolean
406− , organizationUseTotp :: JNullable Boolean
407− , revisionDate :: JNullable Timestamp
408− , attachments :: JNullable (Array AttachmentResponse)
409− , passwordHistory :: JNullable (Array PasswordHistoryResponse)
410− , collectionIds :: JNullable (Array String)
411− , deletedDate :: JNullable String
412− , reprompt :: CipherRepromptType
413− }
448+type CipherResponse =
449+ { id :: CipherId
450+ , organizationId :: JNullable String
451+ , folderId :: JNullable String
452+ , type :: CipherType
453+ , name :: EncryptedString
454+ , notes :: JNullable EncryptedString
455+ , fields :: JNullable (Array FieldApi)
456+ , login :: JNullable LoginApi
457+ , card :: JNullable CardApi
458+ , identity :: JNullable IdentityApi
459+ , secureNote :: JNullable SecureNoteApi
460+ , favorite :: Boolean
461+ , edit :: JNullable Boolean
462+ , viewPassword :: JNullable Boolean
463+ , organizationUseTotp :: JNullable Boolean
464+ , revisionDate :: JNullable Timestamp
465+ , attachments :: JNullable (Array AttachmentResponse)
466+ , passwordHistory :: JNullable (Array PasswordHistoryResponse)
467+ , collectionIds :: JNullable (Array String)
468+ , deletedDate :: JNullable String
469+ , reprompt :: CipherRepromptType
470+ }
414471
415472cipherTypeLogin = 1 :: CipherType
416473
@@ -420,15 +477,14 @@ cipherTypeCard = 3 :: CipherType
420477
421478cipherTypeIdentity = 4 :: CipherType
422479
423−type CipherType
424− = Int
480+type CipherType = Int
425481
426−type FieldApi
427− = { name :: String
428− , value :: String
429− , type :: FieldType
430− , linkedId :: JNullable LinkedIdType
431− }
482+type FieldApi =
483+ { name :: JNullable String
484+ , value :: JNullable String
485+ , type :: FieldType
486+ , linkedId :: JNullable LinkedIdType
487+ }
432488
433489fieldTypeText = 0 :: FieldType
434490
@@ -438,8 +494,7 @@ fieldTypeBoolean = 2 :: FieldType
438494
439495fieldTypeLinked = 3 :: FieldType
440496
441−type FieldType
442− = Int
497+type FieldType = Int
443498
444499linkedIdTypeLoginLinkedIdUsername = 100 :: LinkedIdType
445500
@@ -495,23 +550,22 @@ linkedIdTypeIdentityLinkedIdLastName = 417 :: LinkedIdType
495550
496551linkedIdTypeIdentityLinkedIdFullName = 418 :: LinkedIdType
497552
498−type LinkedIdType
499− = Int
553+type LinkedIdType = Int
500554
501−type LoginApi
502− = { uris :: JOpt (Array LoginUriApi)
503− , username :: JNullable EncryptedString
504− , password :: JNullable EncryptedString
505− , passwordRevisionDate :: JNullable String
506− , totp :: JNullable EncryptedString
507− -- This thing is undefined in some cases for some reson. Just ignoring it
508− , autofillOnPageLoad :: JOpt (JNullable Boolean)
509− }
555+type LoginApi =
556+ { uris :: JOpt (Array LoginUriApi)
557+ , username :: JNullable EncryptedString
558+ , password :: JNullable EncryptedString
559+ , passwordRevisionDate :: JNullable String
560+ , totp :: JNullable EncryptedString
561+ -- This thing is undefined in some cases for some reson. Just ignoring it
562+ , autofillOnPageLoad :: JOpt (JNullable Boolean)
563+ }
510564
511−type LoginUriApi
512− = { uri :: EncryptedString
513− , match :: JNullable UriMatchType
514− }
565+type LoginUriApi =
566+ { uri :: JNullable EncryptedString
567+ , match :: JNullable UriMatchType
568+ }
515569
516570uriMatchTypeDomain = 0 :: UriMatchType
517571
@@ -525,87 +579,85 @@ uriMatchTypeRegularExpression = 4 :: UriMatchType
525579
526580uriMatchTypeNever = 5 :: UriMatchType
527581
528−type UriMatchType
529− = Int
530−
531−type CardApi
532− = { cardholderName :: JNullable EncryptedString
533− , brand :: JNullable EncryptedString
534− , number :: JNullable EncryptedString
535− , expMonth :: JNullable EncryptedString
536− , expYear :: JNullable EncryptedString
537− , code :: JNullable EncryptedString
538− }
539−
540−type IdentityApi
541− = { title :: JNullable EncryptedString
542− , firstName :: JNullable EncryptedString
543− , middleName :: JNullable EncryptedString
544− , lastName :: JNullable EncryptedString
545− , address1 :: JNullable EncryptedString
546− , address2 :: JNullable EncryptedString
547− , address3 :: JNullable EncryptedString
548− , city :: JNullable EncryptedString
549− , state :: JNullable EncryptedString
550− , postalCode :: JNullable EncryptedString
551− , country :: JNullable EncryptedString
552− , company :: JNullable EncryptedString
553− , email :: JNullable EncryptedString
554− , phone :: JNullable EncryptedString
555− , ssn :: JNullable EncryptedString
556− , username :: JNullable EncryptedString
557− , passportNumber :: JNullable EncryptedString
558− , licenseNumber :: JNullable EncryptedString
559− }
582+type UriMatchType = Int
583+
584+type CardApi =
585+ { cardholderName :: JNullable EncryptedString
586+ , brand :: JNullable EncryptedString
587+ , number :: JNullable EncryptedString
588+ , expMonth :: JNullable EncryptedString
589+ , expYear :: JNullable EncryptedString
590+ , code :: JNullable EncryptedString
591+ }
592+
593+type IdentityApi =
594+ { title :: JNullable EncryptedString
595+ , firstName :: JNullable EncryptedString
596+ , middleName :: JNullable EncryptedString
597+ , lastName :: JNullable EncryptedString
598+ , address1 :: JNullable EncryptedString
599+ , address2 :: JNullable EncryptedString
600+ , address3 :: JNullable EncryptedString
601+ , city :: JNullable EncryptedString
602+ , state :: JNullable EncryptedString
603+ , postalCode :: JNullable EncryptedString
604+ , country :: JNullable EncryptedString
605+ , company :: JNullable EncryptedString
606+ , email :: JNullable EncryptedString
607+ , phone :: JNullable EncryptedString
608+ , ssn :: JNullable EncryptedString
609+ , username :: JNullable EncryptedString
610+ , passportNumber :: JNullable EncryptedString
611+ , licenseNumber :: JNullable EncryptedString
612+ }
560613
561614secureNoteTypeGeneric = 0 :: SecureNoteType
562615
563−type SecureNoteType
564− = Int
616+type SecureNoteType = Int
565617
566−type SecureNoteApi = {
567− type :: SecureNoteType
568−}
618+type SecureNoteApi =
619+ {
620+ -- type :: SecureNoteType
621+ }
569622
570−type AttachmentResponse
571− = { id :: String
572− , url :: String
573− , fileName :: String
574− , key :: String
575− , size :: String
576− , sizeName :: String
577− }
623+type AttachmentResponse =
624+ { id :: String
625+ , url :: JNullable String
626+ , fileName :: JNullable String
627+ , key :: JNullable String
628+ , size :: JNullable String
629+ , sizeName :: JNullable String
630+ }
578631
579−type PasswordHistoryResponse
580− = { password :: String
581− , lastUsedDate :: String
582− }
632+type PasswordHistoryResponse =
633+ { password :: String
634+ , lastUsedDate :: JNullable String
635+ }
583636
584637cipherRepromptTypeNone = 0 :: CipherRepromptType
585638
586639cipherRepromptTypePassword = 1 :: CipherRepromptType
587640
588−type CipherRepromptType
589− = Int
641+type CipherRepromptType = Int
590642
591−type DomainsResponse
592− = { equivalentDomains :: Array (Array String)
593− , globalEquivalentDomains :: Array GlobalDomainResponse
594− }
643+type DomainsResponse =
644+ { equivalentDomains :: Array (Array String)
645+ , globalEquivalentDomains :: Array GlobalDomainResponse
646+ }
595647
596−type GlobalDomainResponse
597− = { type :: Int
598− , domains :: Array String
599− , excluded :: Boolean
600− }
648+type GlobalDomainResponse =
649+ { type :: Int
650+ , domains :: Array String
651+ , excluded :: Boolean
652+ }
601653
602−type PolicyResponse
603− = { id :: String
604− , organizationId :: JNullable String
605− , type :: PolicyType
606− , data :: ShowableJson
607− , enabled :: Boolean
608− }
654+type PolicyResponse =
655+ { id :: String
656+ , organizationId :: JNullable String
657+ , type :: PolicyType
658+ , data :: ShowableJson
659+ , enabled :: Boolean
660+ }
609661
610662policyTypeTwoFactorAuthentication = 0 :: PolicyType -- Requires users to have 2fa enabled
611663
@@ -629,43 +681,46 @@ policyTypeMaximumVaultTimeout = 9 :: PolicyType -- Sets the maximum allowed vaul
629681
630682policyTypeDisablePersonalVaultExport = 10 :: PolicyType -- Disable personal vault export
631683
632−type PolicyType
633− = Int
634−
635−type SendResponse
636− = { id :: String
637− , accessId :: String
638− , type :: SendType
639− , name :: String
640− , notes :: JNullable String
641− , file :: JNullable SendFileApi
642− , text :: JNullable SendTextApi
643− , key :: String
644− , maxAccessCount :: JNullable Int
645− , accessCount :: Int
646− , revisionDate :: String
647− , expirationDate :: String
648− , deletionDate :: JNullable String
649− , password :: String
650− , disable :: Boolean
651− , hideEmail :: Boolean
652− }
684+type PolicyType = Int
685+
686+type SendResponse =
687+ { id :: String
688+ , accessId :: JNullable String
689+ , type :: SendType
690+ , name :: JNullable String
691+ , notes :: JNullable String
692+ , file :: JNullable SendFileApi
693+ , text :: JNullable SendTextApi
694+ , key :: JNullable String
695+ , maxAccessCount :: JNullable Int
696+ , accessCount :: JNullable Int
697+ , revisionDate :: JNullable String
698+ , expirationDate :: JNullable String
699+ , deletionDate :: JNullable String
700+ , password :: JNullable String
701+ , disable :: Boolean
702+ , hideEmail :: Boolean
703+ }
653704
654705sendTypeText = 0 :: SendType
655706
656707sendTypeFile = 1 :: SendType
657708
658−type SendType
659− = Int
709+type SendType = Int
710+
711+type SendFileApi =
712+ { id :: String
713+ , fileName :: JNullable String
714+ , size :: JNullable String
715+ , sizeName :: JNullable String
716+ }
660717
661−type SendFileApi
662− = { id :: String
663− , fileName :: String
664− , size :: String
665− , sizeName :: String
666− }
718+type SendTextApi =
719+ { text :: JNullable String
720+ , hidden :: Boolean
721+ }
667722
668−type SendTextApi
669− = { text :: String
670− , hidden :: Boolean
671− }
723+type TwoFactorEmailRequest =
724+ { email :: Email
725+ , masterPasswordHash :: StringHash
726+ }
backend/Bridge.purs+128−1
@@ -362,6 +362,18 @@ derive instance genericCipher_LoginCipher_password_Maybe :: Generic Cipher_Login
362362derive instance eqCipher_LoginCipher_password_Maybe :: Eq Cipher_LoginCipher_password_Maybe
363363derive instance ordCipher_LoginCipher_password_Maybe :: Ord Cipher_LoginCipher_password_Maybe
364364
365+newtype Cipher_LoginCipher_totp_Maybe =
366+ Cipher_LoginCipher_totp_Maybe (Maybe String)
367+
368+derive instance newtypeCipher_LoginCipher_totp_Maybe :: Newtype Cipher_LoginCipher_totp_Maybe _
369+instance encodeJsonCipher_LoginCipher_totp_Maybe :: EncodeJson Cipher_LoginCipher_totp_Maybe where
370+ encodeJson = genericEncodeAeson Argonaut.defaultOptions
371+instance decodeJsonCipher_LoginCipher_totp_Maybe :: DecodeJson Cipher_LoginCipher_totp_Maybe where
372+ decodeJson = genericDecodeAeson Argonaut.defaultOptions
373+derive instance genericCipher_LoginCipher_totp_Maybe :: Generic Cipher_LoginCipher_totp_Maybe _
374+derive instance eqCipher_LoginCipher_totp_Maybe :: Eq Cipher_LoginCipher_totp_Maybe
375+derive instance ordCipher_LoginCipher_totp_Maybe :: Ord Cipher_LoginCipher_totp_Maybe
376+
365377newtype Cipher_LoginCipher_uris_List =
366378 Cipher_LoginCipher_uris_List (Array String)
367379
@@ -389,6 +401,7 @@ derive instance ordCipher_LoginCipher_username_Maybe :: Ord Cipher_LoginCipher_u
389401newtype Cipher_LoginCipher =
390402 Cipher_LoginCipher {
391403 password :: Cipher_LoginCipher_password_Maybe
404+ , totp :: Cipher_LoginCipher_totp_Maybe
392405 , uris :: Cipher_LoginCipher_uris_List
393406 , username :: Cipher_LoginCipher_username_Maybe
394407 }
@@ -430,10 +443,57 @@ derive instance genericCipherType :: Generic CipherType _
430443derive instance eqCipherType :: Eq CipherType
431444derive instance ordCipherType :: Ord CipherType
432445
446+newtype Cmd_ChooseSecondFactor =
447+ Cmd_ChooseSecondFactor {
448+ email :: String
449+ , factor :: TwoFactorProviderType
450+ , password :: String
451+ , requestFromServer :: Boolean
452+ , server :: String
453+ }
454+
455+derive instance newtypeCmd_ChooseSecondFactor :: Newtype Cmd_ChooseSecondFactor _
456+instance encodeJsonCmd_ChooseSecondFactor :: EncodeJson Cmd_ChooseSecondFactor where
457+ encodeJson = genericEncodeAeson Argonaut.defaultOptions
458+instance decodeJsonCmd_ChooseSecondFactor :: DecodeJson Cmd_ChooseSecondFactor where
459+ decodeJson = genericDecodeAeson Argonaut.defaultOptions
460+derive instance genericCmd_ChooseSecondFactor :: Generic Cmd_ChooseSecondFactor _
461+derive instance eqCmd_ChooseSecondFactor :: Eq Cmd_ChooseSecondFactor
462+derive instance ordCmd_ChooseSecondFactor :: Ord Cmd_ChooseSecondFactor
463+
464+newtype Cmd_Login_secondFactor =
465+ Cmd_Login_secondFactor {
466+ provider :: TwoFactorProviderType
467+ , remember :: Boolean
468+ , token :: String
469+ }
470+
471+derive instance newtypeCmd_Login_secondFactor :: Newtype Cmd_Login_secondFactor _
472+instance encodeJsonCmd_Login_secondFactor :: EncodeJson Cmd_Login_secondFactor where
473+ encodeJson = genericEncodeAeson Argonaut.defaultOptions
474+instance decodeJsonCmd_Login_secondFactor :: DecodeJson Cmd_Login_secondFactor where
475+ decodeJson = genericDecodeAeson Argonaut.defaultOptions
476+derive instance genericCmd_Login_secondFactor :: Generic Cmd_Login_secondFactor _
477+derive instance eqCmd_Login_secondFactor :: Eq Cmd_Login_secondFactor
478+derive instance ordCmd_Login_secondFactor :: Ord Cmd_Login_secondFactor
479+
480+newtype Cmd_Login_secondFactor_Maybe =
481+ Cmd_Login_secondFactor_Maybe (Maybe Cmd_Login_secondFactor)
482+
483+derive instance newtypeCmd_Login_secondFactor_Maybe :: Newtype Cmd_Login_secondFactor_Maybe _
484+instance encodeJsonCmd_Login_secondFactor_Maybe :: EncodeJson Cmd_Login_secondFactor_Maybe where
485+ encodeJson = genericEncodeAeson Argonaut.defaultOptions
486+instance decodeJsonCmd_Login_secondFactor_Maybe :: DecodeJson Cmd_Login_secondFactor_Maybe where
487+ decodeJson = genericDecodeAeson Argonaut.defaultOptions
488+derive instance genericCmd_Login_secondFactor_Maybe :: Generic Cmd_Login_secondFactor_Maybe _
489+derive instance eqCmd_Login_secondFactor_Maybe :: Eq Cmd_Login_secondFactor_Maybe
490+derive instance ordCmd_Login_secondFactor_Maybe :: Ord Cmd_Login_secondFactor_Maybe
491+
433492newtype Cmd_Login =
434493 Cmd_Login {
435494 email :: String
436495 , password :: String
496+ , secondFactor :: Cmd_Login_secondFactor_Maybe
437497 , server :: String
438498 }
439499
@@ -447,7 +507,10 @@ derive instance eqCmd_Login :: Eq Cmd_Login
447507derive instance ordCmd_Login :: Ord Cmd_Login
448508
449509data Cmd =
450− Copy String
510+ ChooseSecondFactor Cmd_ChooseSecondFactor
511+ | Copy String
512+ | CreateCipher FullCipher
513+ | DeleteCipher FullCipher
451514 | GeneratePassword PasswordGeneratorConfig
452515 | Init
453516 | Login Cmd_Login
@@ -456,6 +519,7 @@ data Cmd =
456519 | NeedsReset
457520 | Open String
458521 | RequestCipher String
522+ | RequestTotp String
459523 | SendMasterPassword String
460524 | UpdateCipher FullCipher
461525
@@ -467,9 +531,22 @@ derive instance genericCmd :: Generic Cmd _
467531derive instance eqCmd :: Eq Cmd
468532derive instance ordCmd :: Ord Cmd
469533
534+newtype FullCipher_collectionIds_List =
535+ FullCipher_collectionIds_List (Array String)
536+
537+derive instance newtypeFullCipher_collectionIds_List :: Newtype FullCipher_collectionIds_List _
538+instance encodeJsonFullCipher_collectionIds_List :: EncodeJson FullCipher_collectionIds_List where
539+ encodeJson = genericEncodeAeson Argonaut.defaultOptions
540+instance decodeJsonFullCipher_collectionIds_List :: DecodeJson FullCipher_collectionIds_List where
541+ decodeJson = genericDecodeAeson Argonaut.defaultOptions
542+derive instance genericFullCipher_collectionIds_List :: Generic FullCipher_collectionIds_List _
543+derive instance eqFullCipher_collectionIds_List :: Eq FullCipher_collectionIds_List
544+derive instance ordFullCipher_collectionIds_List :: Ord FullCipher_collectionIds_List
545+
470546newtype FullCipher =
471547 FullCipher {
472548 cipher :: Cipher
549+ , collectionIds :: FullCipher_collectionIds_List
473550 , favorite :: Boolean
474551 , id :: String
475552 , name :: String
@@ -557,9 +634,38 @@ derive instance genericSub_NeedsMasterPassword :: Generic Sub_NeedsMasterPasswor
557634derive instance eqSub_NeedsMasterPassword :: Eq Sub_NeedsMasterPassword
558635derive instance ordSub_NeedsMasterPassword :: Ord Sub_NeedsMasterPassword
559636
637+newtype Sub_NeedsSecondFactor_List =
638+ Sub_NeedsSecondFactor_List (Array TwoFactorProviderType)
639+
640+derive instance newtypeSub_NeedsSecondFactor_List :: Newtype Sub_NeedsSecondFactor_List _
641+instance encodeJsonSub_NeedsSecondFactor_List :: EncodeJson Sub_NeedsSecondFactor_List where
642+ encodeJson = genericEncodeAeson Argonaut.defaultOptions
643+instance decodeJsonSub_NeedsSecondFactor_List :: DecodeJson Sub_NeedsSecondFactor_List where
644+ decodeJson = genericDecodeAeson Argonaut.defaultOptions
645+derive instance genericSub_NeedsSecondFactor_List :: Generic Sub_NeedsSecondFactor_List _
646+derive instance eqSub_NeedsSecondFactor_List :: Eq Sub_NeedsSecondFactor_List
647+derive instance ordSub_NeedsSecondFactor_List :: Ord Sub_NeedsSecondFactor_List
648+
649+newtype Sub_Totp =
650+ Sub_Totp {
651+ code :: String
652+ , interval :: Int
653+ , source :: String
654+ }
655+
656+derive instance newtypeSub_Totp :: Newtype Sub_Totp _
657+instance encodeJsonSub_Totp :: EncodeJson Sub_Totp where
658+ encodeJson = genericEncodeAeson Argonaut.defaultOptions
659+instance decodeJsonSub_Totp :: DecodeJson Sub_Totp where
660+ decodeJson = genericDecodeAeson Argonaut.defaultOptions
661+derive instance genericSub_Totp :: Generic Sub_Totp _
662+derive instance eqSub_Totp :: Eq Sub_Totp
663+derive instance ordSub_Totp :: Ord Sub_Totp
664+
560665data Sub =
561666 CaptchaDone
562667 | CipherChanged FullCipher
668+ | CipherDeleted FullCipher
563669 | Error String
564670 | GeneratedPassword String
565671 | LoadCipher FullCipher
@@ -568,8 +674,11 @@ data Sub =
568674 | NeedsCaptcha String
569675 | NeedsLogin
570676 | NeedsMasterPassword Sub_NeedsMasterPassword
677+ | NeedsSecondFactor Sub_NeedsSecondFactor_List
571678 | RecieveEmail String
572679 | Reset
680+ | Totp Sub_Totp
681+ | WrongPassword
573682
574683instance encodeJsonSub :: EncodeJson Sub where
575684 encodeJson = genericEncodeAeson Argonaut.defaultOptions
@@ -579,3 +688,21 @@ derive instance genericSub :: Generic Sub _
579688derive instance eqSub :: Eq Sub
580689derive instance ordSub :: Ord Sub
581690
691+data TwoFactorProviderType =
692+ Authenticator
693+ | Duo
694+ | Email
695+ | OrganizationDuo
696+ | Remember
697+ | U2f
698+ | WebAuthn
699+ | Yubikey
700+
701+instance encodeJsonTwoFactorProviderType :: EncodeJson TwoFactorProviderType where
702+ encodeJson = genericEncodeAeson Argonaut.defaultOptions
703+instance decodeJsonTwoFactorProviderType :: DecodeJson TwoFactorProviderType where
704+ decodeJson = genericDecodeAeson Argonaut.defaultOptions
705+derive instance genericTwoFactorProviderType :: Generic TwoFactorProviderType _
706+derive instance eqTwoFactorProviderType :: Eq TwoFactorProviderType
707+derive instance ordTwoFactorProviderType :: Ord TwoFactorProviderType
708+
backend/Data/EncString.js+1−2
@@ -1,7 +1,6 @@
1−import { EncString } from "../../deps/bw/libs/shared/dist/src/models/domain/encString"
1+import { EncString } from "../../bw/models/domain/enc-string"
22
33export function fromString(str) {
4− console.log("fromString " + str)
54 const x = new EncString(str)
65 return x
76}
backend/Data/SymmetricCryptoKey.js+1−2
@@ -1,6 +1,5 @@
1−import { SymmetricCryptoKey } from '../../deps/bw/libs/shared/dist/src/models/domain/symmetricCryptoKey.js';
1+import { SymmetricCryptoKey } from '../../bw/models/domain/symmetric-crypto-key.js'
22
33export function fromArrayBuffer(arr) {
4− console.log("fromArrayBuffer " + arr)
54 return new SymmetricCryptoKey(arr)
65}
backend/FFI.js+16−1
@@ -1,6 +1,21 @@
1+function searchForInit(obj) {
2+ if (obj.init) {
3+ return obj.init
4+ } else {
5+ for (let key in obj) {
6+ if (obj.hasOwnProperty(key)) {
7+ const result = searchForInit(obj[key])
8+ if (result) {
9+ return result
10+ }
11+ }
12+ }
13+ }
14+}
15+
116export function startElm(element) {
217 return function () {
3− return Elm.Main.init({
18+ return searchForInit(Elm)({
419 node: document.getElementById(element)
520 })
621 }
backend/Main.purs+220−122
@@ -3,18 +3,20 @@ module Main
33 ) where
44
55import Prelude
6+
67import BW (ApiService, CryptoFunctions, Services, CryptoService)
78import BW as WB
8−import BW.Logic (decodeCipher, decrypt, encodeCipher, hashPassword, liftPromise)
9+import BW.Logic (bwPasswordStringHash, decodeCipher, decrypt, encodeCipher, hashPassword, liftPromise, supportedCipher)
910import BW.Logic as Logic
10−import BW.Types (CipherResponse, Email(..), Password(..), Urls, cipherTypeCard, cipherTypeIdentity, cipherTypeLogin, cipherTypeSecureNote)
11+import BW.Types (CipherResponse, Email(..), Password(..), Urls, cipherTypeCard, cipherTypeIdentity, cipherTypeLogin, cipherTypeSecureNote, fromTwoFactorProviderType)
1112import Bridge as Bridge
12−import Control.Monad.Error.Class (catchError, throwError)
13+import Control.Monad.Error.Class (catchError)
1314import Data.Argonaut (class DecodeJson)
1415import Data.Array as Array
1516import Data.Clipboard as Clipboard
1617import Data.DateTime (DateTime)
1718import Data.Either (Either(..))
19+import Data.Foldable (for_)
1820import Data.HCaptcha (bindHCaptchToken)
1921import Data.JNullable (jnull, nullify)
2022import Data.JNullable as JNullable
@@ -28,7 +30,7 @@ import Effect (Effect)
2830import Effect.Aff (Aff, runAff_, try)
2931import Effect.Class (liftEffect)
3032import Effect.Class.Console (log)
31−import Effect.Exception (error)
33+import Effect.Exception (throw)
3234import Effect.Exception as Exc
3335import Effect.Ref as Ref
3436import Elm as Elm
@@ -60,19 +62,19 @@ main = do
6062 let
6163 runAff act = runElmAff app $ run $ act
6264
63− run ::
64− forall a.
65− Run
66− ( app :: Reader Elm
67− , aff :: Aff
68− , effect :: Effect
69− , crypto :: Reader CryptoService
70− , storage :: Reader Storage
71− , services :: Reader Services
72− , cryptoFunctions :: Reader CryptoFunctions
73− )
74− a ->
75− Run (EFFECT + AFF + ()) a
65+ run
66+ :: forall a
67+ . Run
68+ ( app :: Reader Elm
69+ , aff :: Aff
70+ , effect :: Effect
71+ , crypto :: Reader CryptoService
72+ , storage :: Reader Storage
73+ , services :: Reader Services
74+ , cryptoFunctions :: Reader CryptoFunctions
75+ )
76+ a
77+ -> Run (EFFECT + AFF + ()) a
7678 run act =
7779 runReaderAt (Proxy :: _ "app") app
7880 $ runReaderAt (Proxy :: _ "crypto") services.crypto
@@ -81,19 +83,19 @@ main = do
8183 $ runReaderAt (Proxy :: _ "cryptoFunctions") services.cryptoFunctions
8284 $ act
8385
84− runWithDecryptionKey ::
85− Run
86− ( app :: Reader Elm
87− , aff :: Aff
88− , effect :: Effect
89− , key :: Reader SymmetricCryptoKey
90− , crypto :: Reader CryptoService
91− , storage :: Reader Storage
92− , services :: Reader Services
93− , cryptoFunctions :: Reader CryptoFunctions
94− )
95− Unit ->
96− Effect Unit
86+ runWithDecryptionKey
87+ :: Run
88+ ( app :: Reader Elm
89+ , aff :: Aff
90+ , effect :: Effect
91+ , key :: Reader SymmetricCryptoKey
92+ , crypto :: Reader CryptoService
93+ , storage :: Reader Storage
94+ , services :: Reader Services
95+ , cryptoFunctions :: Reader CryptoFunctions
96+ )
97+ Unit
98+ -> Effect Unit
9799 runWithDecryptionKey act = do
98100 maybeKey <- Ref.read masterKeyRef
99101 runAff
@@ -104,8 +106,10 @@ main = do
104106 runReaderAt (Proxy :: _ "key") key $ act
105107 Nothing -> requestMasterPassword
106108 Elm.subscribe app \cmd -> case cmd of
107− Bridge.Login (Bridge.Cmd_Login { email, server, password }) -> do
108− captchaToken <- Ref.read hCaptchaTokenRef
109+ Bridge.Login (Bridge.Cmd_Login { email: email', server, password, secondFactor }) -> do
110+ let
111+ email = Email email'
112+ captchaResponse <- Ref.read hCaptchaTokenRef
109113 Ref.write Nothing hCaptchaTokenRef
110114 let
111115 urls = baseUrl server
@@ -116,18 +120,27 @@ main = do
116120 loginResponse <-
117121 liftAff
118122 $ catchError
119− ( runBaseAff' $ run $ runReaderAt (Proxy :: _ "api") unauthedApi
120− $ Logic.getLogInRequestToken prelogin (Email email) (Password password) captchaToken
123+ ( do
124+ resp <-
125+ runBaseAff' $ run $ runReaderAt (Proxy :: _ "api") unauthedApi
126+ $ Logic.getLogInRequestToken
127+ { prelogin
128+ , email
129+ , password: Password password
130+ , captchaResponse
131+ , secondFactor
132+ }
133+ pure $ Right resp
121134 )
122− ( const
123− $ liftEffect do
124− Elm.send app Bridge.NeedsLogin
125− Exc.throw "Could not log in. Please check your data and try again."
135+ ( \err -> do
136+ log $ show err
137+ pure $ Left unit
126138 )
127− case toEither1 loginResponse of
128− Left { siteKey } -> send $ Bridge.NeedsCaptcha siteKey
129− Right token -> do
130− masterKey <- Logic.makePreloginKey prelogin (Email email) (Password password)
139+ case map (toEither1 >>> map toEither1) $ loginResponse of
140+ Left _ -> send Bridge.WrongPassword
141+ Right (Left { siteKey }) -> send $ Bridge.NeedsCaptcha siteKey
142+ Right (Right (Right token)) -> do
143+ masterKey <- Logic.makePreloginKey prelogin email (Password password)
131144 api <- liftPromise $ services.getApi urls (nullify token)
132145 sync <- liftPromise $ api.getSync unit
133146 hash <- hashPassword (Password password)
@@ -140,7 +153,20 @@ main = do
140153 Storage.store storage SyncKey sync
141154 Storage.store storage TokenKey token
142155 send Bridge.LoginSuccessful
143− Bridge.NeedCiphersList -> runWithDecryptionKey performSync
156+ Right (Right (Left { twoFactorProviders, captchaToken })) -> do
157+ case toEither1 captchaToken of
158+ Left captchaTokenString ->
159+ liftEffect
160+ $ Ref.write (Just captchaTokenString) hCaptchaTokenRef
161+ Right _ -> pure unit
162+ providers <- case fromTwoFactorProviderType twoFactorProviders of
163+ Left x -> liftEffect $ throw $ "Unknown second factor provider: " <> x
164+ Right x -> pure x
165+ send $ Bridge.NeedsSecondFactor $ Bridge.Sub_NeedsSecondFactor_List providers
166+ Bridge.NeedCiphersList ->
167+ runWithDecryptionKey do
168+ sendCiphers
169+ performSync
144170 Bridge.NeedsReset -> do
145171 WebStorage.clear storage
146172 Elm.send app Bridge.Reset
@@ -160,9 +186,9 @@ main = do
160186 Bridge.Init -> do
161187 Storage.get storage TokenKey
162188 >>= \x ->
163− Elm.send app case x of
164− Just _ -> Bridge.LoginSuccessful
165− Nothing -> Bridge.NeedsLogin
189+ Elm.send app case x of
190+ Just _ -> Bridge.LoginSuccessful
191+ Nothing -> Bridge.NeedsLogin
166192 Bridge.RequestCipher id ->
167193 runWithDecryptionKey do
168194 sync <- getOrReset SyncKey
@@ -170,8 +196,7 @@ main = do
170196 Nothing -> pure unit
171197 Just cipherResponse -> do
172198 cipher <- decodeCipher cipherResponse
173− send $ Bridge.LoadCipher cipher
174− pure unit
199+ for_ cipher $ send <<< Bridge.LoadCipher
175200 pure unit
176201 Bridge.NeedEmail ->
177202 runWithDecryptionKey do
@@ -187,7 +212,7 @@ main = do
187212 api <- getAuthedApi
188213 cipher <- encodeCipher fullCipher
189214 newCipher <- liftPromise (api.putCipher cipher) >>= decodeCipher
190− send $ Bridge.CipherChanged newCipher
215+ for_ newCipher $ send <<< Bridge.CipherChanged
191216 performSync
192217 pure unit
193218 Bridge.GeneratePassword cfg ->
@@ -195,18 +220,71 @@ main = do
195220 password <- liftPromise $ services.passwordGeneration.generatePassword cfg
196221 send $ Bridge.GeneratedPassword password
197222 pure unit
223+ Bridge.CreateCipher fullCipher ->
224+ runWithDecryptionKey do
225+ api <- getAuthedApi
226+ cipher <- encodeCipher fullCipher
227+ newCipher <- liftPromise (api.postCipherCreate cipher) >>= decodeCipher
228+ for_ newCipher $ send <<< Bridge.CipherChanged
229+ performSync
230+ pure unit
231+ Bridge.DeleteCipher c@(Bridge.FullCipher { id }) ->
232+ runWithDecryptionKey do
233+ api <- getAuthedApi
234+ liftPromise $ api.deleteCipher id
235+ send $ Bridge.CipherDeleted c
236+ performSync
237+ pure unit
238+ Bridge.RequestTotp totp ->
239+ runWithDecryptionKey do
240+ { totpService } <- askAt (Proxy :: _ "services")
241+ code <- liftPromise $ totpService.getCode totp
242+ let
243+ interval = totpService.getTimeInterval totp
244+ send $ Bridge.Totp
245+ $ Bridge.Sub_Totp
246+ { interval
247+ , code
248+ , source: totp
249+ }
250+ pure unit
251+ Bridge.ChooseSecondFactor
252+ ( Bridge.Cmd_ChooseSecondFactor
253+ { email: email'
254+ , factor
255+ , server
256+ , password
257+ , requestFromServer
258+ }
259+ ) ->
260+ when requestFromServer
261+ let
262+ email = Email email'
263+ in
264+ case factor of
265+ Bridge.Email ->
266+ runAff do
267+ let
268+ urls = baseUrl server
269+ unauthedApi <- liftPromise $ services.getApi urls jnull
270+ prelogin <- liftPromise $ unauthedApi.postPrelogin { email }
271+ masterPasswordHash <- bwPasswordStringHash prelogin email (Password password)
272+ liftPromise $ unauthedApi.postTwoFactorEmail { email, masterPasswordHash }
273+ pure unit
274+ _ -> pure unit
198275
199−processCipher ::
200− forall r.
201− CipherResponse ->
202− Run
203− ( crypto :: Reader CryptoService
204− , key :: Reader SymmetricCryptoKey
205− , aff :: Aff
206− , effect :: Effect
207− | r
208− )
209− { cipher :: Bridge.Sub_LoadCiphers, date :: DateTime }
276+processCipher
277+ :: forall r
278+ . CipherResponse
279+ -> Run
280+ ( crypto :: Reader CryptoService
281+ , key :: Reader SymmetricCryptoKey
282+ , aff :: Aff
283+ , effect :: Effect
284+ | r
285+ )
286+ (Maybe { cipher :: Bridge.Sub_LoadCiphers, date :: DateTime })
287+processCipher cipher | not (supportedCipher cipher) = pure Nothing
210288processCipher cipher = do
211289 name <- decrypt cipher.name
212290 cipherType <- case cipher.type of
@@ -218,10 +296,12 @@ processCipher cipher = do
218296 | n == cipherTypeCard -> pure Bridge.CardType
219297 n
220298 | n == cipherTypeIdentity -> pure Bridge.IdentityType
221− n -> liftEffect $ throwError $ error $ "Unsupported cipher type: " <> show n
299+ n -> liftEffect $ throw $ "Unsupported cipher type: " <> show n
222300 date <- liftEffect $ maybe (pure bottom) (Timestamp.toDateTime) $ JNullable.toMaybe cipher.revisionDate
223301 pure
224− $ { cipher:
302+ $ Just
303+ $
304+ { cipher:
225305 Bridge.Sub_LoadCiphers
226306 { name: name
227307 , date: maybe "" Timestamp.toLocalDateTimeString $ JNullable.toMaybe cipher.revisionDate
@@ -255,18 +335,18 @@ baseUrl server =
255335 , keyConnector: jnull
256336 }
257337
258−getOrReset ::
259− forall k t r.
260− StorageKey k t =>
261− DecodeJson t =>
262− k ->
263− Run
264− ( storage :: Reader Storage
265− , app :: Reader Elm
266− , effect :: Effect
267− | r
268− )
269− t
338+getOrReset
339+ :: forall k t r
340+ . StorageKey k t
341+ => DecodeJson t
342+ => k
343+ -> Run
344+ ( storage :: Reader Storage
345+ , app :: Reader Elm
346+ , effect :: Effect
347+ | r
348+ )
349+ t
270350getOrReset key = do
271351 storage <- askAt (Proxy :: _ "storage")
272352 app <- askAt (Proxy :: _ "app")
@@ -277,75 +357,93 @@ getOrReset key = do
277357 Exc.throw "Encountered an internal error. Resetting the app."
278358 liftEffect $ try (Storage.get storage key)
279359 >>= \x -> case x of
280− Left err -> do
281− log $ show err
282− handleError
283− Right Nothing -> handleError
284− Right (Just y) -> pure y
360+ Left err -> do
361+ log $ show err
362+ handleError
363+ Right Nothing -> handleError
364+ Right (Just y) -> pure y
285365
286−getAuthedApi ::
287− forall r.
288− Run
289− ( storage :: Reader Storage
290− , app :: Reader Elm
291− , aff :: Aff
292− , effect :: Effect
293− , services :: Reader Services
294− | r
295− )
296− ApiService
366+getAuthedApi
367+ :: forall r
368+ . Run
369+ ( storage :: Reader Storage
370+ , app :: Reader Elm
371+ , aff :: Aff
372+ , effect :: Effect
373+ , services :: Reader Services
374+ | r
375+ )
376+ ApiService
297377getAuthedApi = do
298378 services <- askAt (Proxy :: _ "services")
299379 token <- getOrReset TokenKey
300380 url <- getOrReset UrlsKey
301381 liftPromise $ services.getApi (baseUrl url) (nullify token)
302382
303−requestMasterPassword ::
304− forall r.
305− Run
306− ( storage :: Reader Storage
307− , app :: Reader Elm
308− , effect :: Effect
309− | r
310− )
311− Unit
383+requestMasterPassword
384+ :: forall r
385+ . Run
386+ ( storage :: Reader Storage
387+ , app :: Reader Elm
388+ , effect :: Effect
389+ | r
390+ )
391+ Unit
312392requestMasterPassword = do
313393 url <- getOrReset UrlsKey
314394 sync <- getOrReset SyncKey
315395 send $ Bridge.NeedsMasterPassword $ Bridge.Sub_NeedsMasterPassword { server: url, login: sync.profile.email }
316396
317−send ::
318− forall r.
319− Bridge.Sub ->
320− Run
321− ( app :: Reader Elm
322− , effect :: Effect
323− | r
324− )
325− Unit
397+send
398+ :: forall r
399+ . Bridge.Sub
400+ -> Run
401+ ( app :: Reader Elm
402+ , effect :: Effect
403+ | r
404+ )
405+ Unit
326406send sub = do
327407 app <- askAt (Proxy :: _ "app")
328408 liftEffect $ Elm.send app sub
329409
330−performSync ::
331− forall r.
332− Run
333− ( storage :: Reader Storage
334− , app :: Reader Elm
335− , aff :: Aff
336− , effect :: Effect
337− , services :: Reader Services
338− , crypto :: Reader CryptoService
339− , key :: Reader SymmetricCryptoKey
340− | r
341− )
342− Unit
410+performSync
411+ :: forall r
412+ . Run
413+ ( storage :: Reader Storage
414+ , app :: Reader Elm
415+ , aff :: Aff
416+ , effect :: Effect
417+ , services :: Reader Services
418+ , crypto :: Reader CryptoService
419+ , key :: Reader SymmetricCryptoKey
420+ | r
421+ )
422+ Unit
343423performSync = do
344424 api <- getAuthedApi
345425 storage <- askAt (Proxy :: _ "storage")
346426 newSync <- liftPromise $ api.getSync unit
347427 liftEffect $ Storage.store storage SyncKey newSync
348− ciphers <- traverse processCipher newSync.ciphers
428+ sendCiphers
429+
430+sendCiphers
431+ :: forall r
432+ . Run
433+ ( storage :: Reader Storage
434+ , app :: Reader Elm
435+ , aff :: Aff
436+ , effect :: Effect
437+ , services :: Reader Services
438+ , crypto :: Reader CryptoService
439+ , key :: Reader SymmetricCryptoKey
440+ | r
441+ )
442+ Unit
443+sendCiphers = do
444+ sync <- getOrReset SyncKey
445+ ciphers <- Array.catMaybes <$> traverse processCipher sync.ciphers
349446 let
350447 sortedCiphers = Array.sortWith (_.date >>> Down) ciphers
351448 send $ Bridge.LoadCiphers $ Bridge.Sub_LoadCiphers_List $ map _.cipher sortedCiphers
449+ pure unit
backend/Storage.purs+7−1
@@ -2,7 +2,7 @@ module Storage where
22
33import Prelude
44
5−import BW (Hash(..))
5+import BW (Hash)
66import BW.Types (IdentityTokenResponse, SyncResponse, PreloginResponse)
77import Localstorage (class StorageKey)
88
@@ -35,3 +35,9 @@ data MasterPasswordHashKey
3535
3636instance StorageKey MasterPasswordHashKey Hash where
3737 storageKey MasterPasswordHashKey = "master-password-hash"
38+
39+data DeviceIDKey
40+ = DeviceIDKey
41+
42+instance StorageKey DeviceIDKey String where
43+ storageKey DeviceIDKey = "device-uuid"
bridge.yaml+118−84
@@ -1,125 +1,159 @@
11Sub:
2−- NeedsLogin
3−- NeedsCaptcha: String
4−- CaptchaDone
5−- Error: String
6−- LoadCiphers:
7− List:
2+ - NeedsLogin
3+ - NeedsCaptcha: String
4+ - NeedsSecondFactor: { List: TwoFactorProviderType }
5+ - CaptchaDone
6+ - Error: String
7+ - LoadCiphers:
8+ List:
89 name: String
910 date: String
1011 id: String
1112 cipherType: CipherType
12−- LoginSuccessful
13−- NeedsMasterPassword:
14− server: String
15− login: String
16−- Reset
17−- LoadCipher: FullCipher
18−- RecieveEmail: String
19−- CipherChanged: FullCipher
20−- GeneratedPassword: String
13+ - LoginSuccessful
14+ - WrongPassword
15+ - NeedsMasterPassword:
16+ server: String
17+ login: String
18+ - Reset
19+ - LoadCipher: FullCipher
20+ - RecieveEmail: String
21+ - CipherChanged: FullCipher
22+ - GeneratedPassword: String
23+ - CipherDeleted: FullCipher
24+ - Totp:
25+ source: String
26+ code: String
27+ interval: Int
2128
2229Cmd:
23−- Init
24−- Login:
25− email: String
26− server: String
27− password: String
28−- NeedCiphersList
29−- SendMasterPassword: String
30−- NeedsReset
31−- RequestCipher: String
32−- NeedEmail
33−- Copy: String
34−- Open: String
35−- UpdateCipher: FullCipher
36−- GeneratePassword: PasswordGeneratorConfig
30+ - Init
31+ - Login:
32+ email: String
33+ server: String
34+ password: String
35+ secondFactor:
36+ Maybe:
37+ provider: TwoFactorProviderType
38+ token: String
39+ remember: Bool
40+ - ChooseSecondFactor:
41+ factor: TwoFactorProviderType
42+ email: String
43+ server: String
44+ password: String
45+ requestFromServer: Bool
46+ - NeedCiphersList
47+ - SendMasterPassword: String
48+ - NeedsReset
49+ - RequestCipher: String
50+ - NeedEmail
51+ - Copy: String
52+ - Open: String
53+ - UpdateCipher: FullCipher
54+ - CreateCipher: FullCipher
55+ - GeneratePassword: PasswordGeneratorConfig
56+ - DeleteCipher: FullCipher
57+ - RequestTotp: String
3758
3859FullCipher:
39− reprompt: Int
40− favorite: Bool
41− id: String
42− name: String
43− cipher: Cipher
60+ reprompt: Int
61+ favorite: Bool
62+ id: String
63+ name: String
64+ cipher: Cipher
65+ collectionIds: { List: String }
4466
4567Cipher:
46−- LoginCipher:
47− uris: {List: String}
48− username:
68+ - LoginCipher:
69+ uris: { List: String }
70+ username:
4971 Maybe: String
50− password:
72+ password:
5173 Maybe: String
52−- NoteCipher: String
53−- CardCipher:
54− cardholderName:
74+ totp:
5575 Maybe: String
56− brand:
76+ - NoteCipher: String
77+ - CardCipher:
78+ cardholderName:
5779 Maybe: String
58− number:
80+ brand:
5981 Maybe: String
60− expMonth:
82+ number:
6183 Maybe: String
62− expYear:
84+ expMonth:
6385 Maybe: String
64− code:
86+ expYear:
6587 Maybe: String
66−- IdentityCipher:
67− title:
88+ code:
6889 Maybe: String
69− firstName:
90+ - IdentityCipher:
91+ title:
7092 Maybe: String
71− middleName:
93+ firstName:
7294 Maybe: String
73− lastName:
95+ middleName:
7496 Maybe: String
75− address1:
97+ lastName:
7698 Maybe: String
77− address2:
99+ address1:
78100 Maybe: String
79− address3:
101+ address2:
80102 Maybe: String
81− city:
103+ address3:
82104 Maybe: String
83− state:
105+ city:
84106 Maybe: String
85− postalCode:
107+ state:
86108 Maybe: String
87− country:
109+ postalCode:
88110 Maybe: String
89− company:
111+ country:
90112 Maybe: String
91− email:
113+ company:
92114 Maybe: String
93− phone:
115+ email:
94116 Maybe: String
95− ssn:
117+ phone:
96118 Maybe: String
97− username:
119+ ssn:
98120 Maybe: String
99− passportNumber:
121+ username:
100122 Maybe: String
101− licenseNumber:
123+ passportNumber:
124+ Maybe: String
125+ licenseNumber:
102126 Maybe: String
103127
104128CipherType:
105−- LoginType
106−- CardType
107−- NoteType
108−- IdentityType
129+ - LoginType
130+ - CardType
131+ - NoteType
132+ - IdentityType
109133
110134PasswordGeneratorConfig:
111− length: Int
112− ambiguous: Bool
113− number: Bool
114− minNumber: Int
115− uppercase: Bool
116− minUppercase: Int
117− lowercase: Bool
118− minLowercase: Int
119− special: Bool
120− minSpecial: Int
121− type: String
122− numWords: Int
123− wordSeparator: String
124− capitalize: Bool
125− includeNumber: Bool
135+ length: Int
136+ ambiguous: Bool
137+ number: Bool
138+ minNumber: Int
139+ uppercase: Bool
140+ minUppercase: Int
141+ lowercase: Bool
142+ minLowercase: Int
143+ special: Bool
144+ minSpecial: Int
145+ type: String
146+ numWords: Int
147+ wordSeparator: String
148+ capitalize: Bool
149+ includeNumber: Bool
150+
151+TwoFactorProviderType:
152+ - Authenticator
153+ - Email
154+ - Duo
155+ - Yubikey
156+ - U2f
157+ - Remember
158+ - OrganizationDuo
159+ - WebAuthn
build.sh+2−10
@@ -1,13 +1,5 @@
1−#!/bin/bash
1+#!/bin/sh
22
33set -e
44
5−TEMP=$(mktemp -d)
6−
7−cp -r "$SRC_DIR"/{vanilla,appname.apparmor,appname.desktop,manifest.json,logo.png,index.html,style.css} "$INSTALL_DIR"
8−
9−elm make "$SRC_DIR/frontend/Main.elm" --optimize --output="$TEMP/elm.js"
10−
11−terser "$TEMP/elm.js" --ecma 2018 --compress 'pure_funcs=[F2,F3,F4,F5,F6,F7,F8,F9,A2,A3,A4,A5,A6,A7,A8,A9],pure_getters,keep_fargs=false,unsafe_comps,unsafe' | terser --ecma 2018 --mangle --compress --output "$INSTALL_DIR/elm.js"
12−
13−(cd "$SRC_DIR" && spago bundle-app --minify --to "$INSTALL_DIR/backend.js")
5+cp -r "$SRC_DIR/dist"/* "$INSTALL_DIR"
clickable.yaml+2−10
@@ -1,16 +1,8 @@
11clickable_minimum_required: 7.1.2
2+framework: ubuntu-sdk-20.04
23builder: custom
34build: $SRC_DIR/build.sh
45kill: webapp-container*
5−image_setup:
6− env: {}
7− # DEB_URL: http://archive.ubuntu.com/ubuntu
8− # HOST_ARCH: amd64
9−
10− # PATH: /opt/someprogram/bin:$PATH
11− run:
12−
13− - (cd /tmp && curl -L -o elm.gz https://github.com/elm/compiler/releases/download/0.19.1/binary-for-linux-64-bit.gz && gunzip elm.gz && chmod +x elm && mv elm /usr/local/bin/)
14− - npm install -g spago esbuild purescript terser --unsafe-perm=true
6+docker_image: clickable/amd64-20.04-amd64
157
168restrict_arch: all
deps/bwdeleted−1

Submodule changed.

dev/build-backend.sh+7−1
@@ -1,3 +1,9 @@
11#!/bin/sh
22
3−spago bundle-app --to backend.js
3+set -e
4+
5+spago build
6+
7+export out="$(pwd)/backend.js"
8+
9+$(nix build .#backendBuildScript --print-out-paths) "$(pwd)/output"
dev/build-bw.sh+2
@@ -1,5 +1,7 @@
11#!/bin/sh
22
3+set -e
4+
35(cd deps/bw && npm install)
46
57(cd deps/bw/libs/common/ && npm run clean && npm exec -c "tsc -m es2020 -t es2018")
dev/build.shnew file+10
@@ -0,0 +1,10 @@
1+#!/bin/sh
2+
3+
4+rm -r dist
5+
6+mkdir -p dist
7+
8+cp -r "$(nix build ".#warden-click-src" --print-out-paths)"/* dist
9+
10+chmod -R 777 dist
dev/elm-live.sh+5−1
@@ -1,3 +1,7 @@
11#!/bin/sh
22
3−elm-live frontend/Main.elm --start-page=index.html --proxy-prefix=/api --proxy-host=http://localhost:6009 --pushstate -- --debug --output=elm.js
3+set -e
4+
5+nix build .#vanilla -o vanilla
6+
7+nix run github:NixOS/nixpkgs?rev=d9a1414346059619d9e13ab93e749bbb82e5252a#elmPackages.elm-live -- $1 --start-page=index.html --proxy-prefix=/api --proxy-host=http://localhost:6009 -- --debug --output=elm.js
dev/elm2nix.shnew file+8
@@ -0,0 +1,8 @@
1+#!/bin/sh
2+
3+set -e
4+
5+cd "$(dirname "$0")/.."
6+
7+nix run github:NixOS/nixpkgs#elm2nix -- snapshot
8+nix run github:NixOS/nixpkgs#elm2nix -- convert > elm-srcs.nix
dev/generate-bridge.sh+1−1
@@ -1,3 +1,3 @@
11#!/bin/sh
22
3−elm-ps-bridge --input bridge.yaml --elmOutput=Frontend/Bridge.elm --psOutput=backend/Bridge.purs
3+nix run github:ilyakooo0/elm-ps-bridge -- --input bridge.yaml --elmOutput=frontend/Bridge.elm --psOutput=backend/Bridge.purs
dev/record-test.shnew file+11
@@ -0,0 +1,11 @@
1+#!/bin/sh
2+
3+set -e
4+
5+cd tests
6+
7+npm install
8+
9+export RECORD_TEST=true
10+
11+npx playwright test
dev/spago2nix.shnew file+3
@@ -0,0 +1,3 @@
1+#!/bin/sh
2+
3+nix run "github:justinwoo/spago2nix#spago2nix" -- generate
dev/start-chrome.sh+7−1
@@ -1,3 +1,9 @@
11#!/bin/sh
22
3−/Applications/Chromium.app/Contents/MacOS/Chromium --args --user-data-dir="/tmp/chrome_dev_test" --disable-web-security
3+if test -x "$(which chromium)"; then
4+ chromium --args --user-data-dir="/tmp/chrome_dev_test" --disable-web-security
5+elif test -x /Applications/Chromium.app/Contents/MacOS/Chromium; then
6+ /Applications/Chromium.app/Contents/MacOS/Chromium --args --user-data-dir="/tmp/chrome_dev_test" --disable-web-security
7+else
8+ flatpak run com.github.Eloston.UngoogledChromium --args --user-data-dir="/tmp/chrome_dev_test" --disable-web-security
9+fi
dev/test.shnew file+9
@@ -0,0 +1,9 @@
1+#!/bin/sh
2+
3+set -e
4+
5+cd tests
6+
7+npm install
8+
9+npx playwright test
dev/update-tests.shnew file+9
@@ -0,0 +1,9 @@
1+#!/bin/sh
2+
3+set -e
4+
5+cd tests
6+
7+npm install
8+
9+npx playwright test --update-snapshots
elm-srcs.nixnew file+62
@@ -0,0 +1,62 @@
1+{
2+
3+ "FabienHenon/elm-infinite-scroll" = {
4+ sha256 = "0vhh7j3lna05l5hwjazag52lbkjv11s80jj62m9wb53qxkarw948";
5+ version = "3.0.3";
6+ };
7+
8+ "bartavelle/json-helpers" = {
9+ sha256 = "0k96qra2nq1j4j4ahfl98dkpkc6f2831mq5d5xxg27mp31qwq5nn";
10+ version = "2.0.2";
11+ };
12+
13+ "elm/browser" = {
14+ sha256 = "0nagb9ajacxbbg985r4k9h0jadqpp0gp84nm94kcgbr5sf8i9x13";
15+ version = "1.0.2";
16+ };
17+
18+ "elm/core" = {
19+ sha256 = "19w0iisdd66ywjayyga4kv2p1v9rxzqjaxhckp8ni6n8i0fb2dvf";
20+ version = "1.0.5";
21+ };
22+
23+ "elm/html" = {
24+ sha256 = "1n3gpzmpqqdsldys4ipgyl1zacn0kbpc3g4v3hdpiyfjlgh8bf3k";
25+ version = "1.0.0";
26+ };
27+
28+ "elm/json" = {
29+ sha256 = "0kjwrz195z84kwywaxhhlnpl3p251qlbm5iz6byd6jky2crmyqyh";
30+ version = "1.1.3";
31+ };
32+
33+ "elm/time" = {
34+ sha256 = "0vch7i86vn0x8b850w1p69vplll1bnbkp8s383z7pinyg94cm2z1";
35+ version = "1.0.0";
36+ };
37+
38+ "mdgriffith/elm-ui" = {
39+ sha256 = "0ffcqv4a4ad400hwp824m3qq4jy82cqp5ghmhp1m0q7n004z6kgv";
40+ version = "1.1.8";
41+ };
42+
43+ "mgold/elm-nonempty-list" = {
44+ sha256 = "02d3xgasgy5knh02rgl403z9hwlbyrsnp9wavc2lhkxxa2misrgl";
45+ version = "4.2.0";
46+ };
47+
48+ "elm/random" = {
49+ sha256 = "138n2455wdjwa657w6sjq18wx2r0k60ibpc4frhbqr50sncxrfdl";
50+ version = "1.0.0";
51+ };
52+
53+ "elm/url" = {
54+ sha256 = "0av8x5syid40sgpl5vd7pry2rq0q4pga28b4yykn9gd9v12rs3l4";
55+ version = "1.0.0";
56+ };
57+
58+ "elm/virtual-dom" = {
59+ sha256 = "1yvb8px2z62xd578ag2q0r5hd1vkz9y7dfkx05355iiy1d7jwq4v";
60+ version = "1.0.3";
61+ };
62+}
elm.json+1
@@ -13,6 +13,7 @@
1313 "elm/html": "1.0.0",
1414 "elm/json": "1.1.3",
1515 "elm/time": "1.0.0",
16+ "mdgriffith/elm-ui": "1.1.8",
1617 "mgold/elm-nonempty-list": "4.2.0"
1718 },
1819 "indirect": {
flake.locknew file+177
@@ -0,0 +1,177 @@
1+{
2+ "nodes": {
3+ "bw": {
4+ "flake": false,
5+ "locked": {
6+ "lastModified": 1675446731,
7+ "narHash": "sha256-IbFjYNYZep6W9c+dztJ9CXrwQ0cep3EKb6qnEvv4pj4=",
8+ "owner": "bitwarden",
9+ "repo": "clients",
10+ "rev": "ff143760d4f527f2349333ee4b3004eeda68fac5",
11+ "type": "github"
12+ },
13+ "original": {
14+ "owner": "bitwarden",
15+ "repo": "clients",
16+ "rev": "ff143760d4f527f2349333ee4b3004eeda68fac5",
17+ "type": "github"
18+ }
19+ },
20+ "easy-ps": {
21+ "flake": false,
22+ "locked": {
23+ "lastModified": 1671011575,
24+ "narHash": "sha256-tESal32bcqqdZO+aKnBzc1GoL2mtnaDtj2y7ociCRGA=",
25+ "owner": "justinwoo",
26+ "repo": "easy-purescript-nix",
27+ "rev": "11d3bd58ce6e32703bf69cec04dc7c38eabe14ba",
28+ "type": "github"
29+ },
30+ "original": {
31+ "owner": "justinwoo",
32+ "repo": "easy-purescript-nix",
33+ "type": "github"
34+ }
35+ },
36+ "easy-purescript-nix": {
37+ "flake": false,
38+ "locked": {
39+ "lastModified": 1670198178,
40+ "narHash": "sha256-5KkyNpPakv4xIP2ba0S5GX+dcmd3AcO9kPhwa482BbA=",
41+ "owner": "justinwoo",
42+ "repo": "easy-purescript-nix",
43+ "rev": "7a4cb3cd6ca53566ea1675692eab0aa13907ff09",
44+ "type": "github"
45+ },
46+ "original": {
47+ "owner": "justinwoo",
48+ "repo": "easy-purescript-nix",
49+ "type": "github"
50+ }
51+ },
52+ "flake-utils": {
53+ "locked": {
54+ "lastModified": 1676283394,
55+ "narHash": "sha256-XX2f9c3iySLCw54rJ/CZs+ZK6IQy7GXNY4nSOyu2QG4=",
56+ "owner": "numtide",
57+ "repo": "flake-utils",
58+ "rev": "3db36a8b464d0c4532ba1c7dda728f4576d6d073",
59+ "type": "github"
60+ },
61+ "original": {
62+ "owner": "numtide",
63+ "repo": "flake-utils",
64+ "type": "github"
65+ }
66+ },
67+ "flake-utils_2": {
68+ "locked": {
69+ "lastModified": 1667395993,
70+ "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
71+ "owner": "numtide",
72+ "repo": "flake-utils",
73+ "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
74+ "type": "github"
75+ },
76+ "original": {
77+ "owner": "numtide",
78+ "repo": "flake-utils",
79+ "type": "github"
80+ }
81+ },
82+ "nix-filter": {
83+ "locked": {
84+ "lastModified": 1676294984,
85+ "narHash": "sha256-hdLUa/3RH1VJ+gMUysQE0JGM4F2Q/tIIFbtoxAOurJQ=",
86+ "owner": "numtide",
87+ "repo": "nix-filter",
88+ "rev": "fc282c5478e4141842f9644c239a41cfe9586732",
89+ "type": "github"
90+ },
91+ "original": {
92+ "owner": "numtide",
93+ "repo": "nix-filter",
94+ "type": "github"
95+ }
96+ },
97+ "nixpkgs": {
98+ "locked": {
99+ "lastModified": 1677080879,
100+ "narHash": "sha256-0SjW4/d3Rkw6C7hHZ5lxT4r6Pw9vzQb6Il6zYWwe2Bo=",
101+ "owner": "NixOS",
102+ "repo": "nixpkgs",
103+ "rev": "f5dad40450d272a1ea2413f4a67ac08760649e89",
104+ "type": "github"
105+ },
106+ "original": {
107+ "id": "nixpkgs",
108+ "type": "indirect"
109+ }
110+ },
111+ "nixpkgs_2": {
112+ "locked": {
113+ "lastModified": 1670507980,
114+ "narHash": "sha256-riNZa0xzM1it3pzxciwALeMs+0CsBMWIW2FqulzK8vM=",
115+ "owner": "NixOS",
116+ "repo": "nixpkgs",
117+ "rev": "2787fc7d1e51404678614bf0fe92fc296746eec0",
118+ "type": "github"
119+ },
120+ "original": {
121+ "owner": "NixOS",
122+ "ref": "nixos-unstable",
123+ "repo": "nixpkgs",
124+ "type": "github"
125+ }
126+ },
127+ "root": {
128+ "inputs": {
129+ "bw": "bw",
130+ "easy-ps": "easy-ps",
131+ "flake-utils": "flake-utils",
132+ "nix-filter": "nix-filter",
133+ "nixpkgs": "nixpkgs",
134+ "spago2nix": "spago2nix",
135+ "vanilla": "vanilla"
136+ }
137+ },
138+ "spago2nix": {
139+ "inputs": {
140+ "easy-purescript-nix": "easy-purescript-nix",
141+ "flake-utils": "flake-utils_2",
142+ "nixpkgs": "nixpkgs_2"
143+ },
144+ "locked": {
145+ "lastModified": 1671225584,
146+ "narHash": "sha256-h9r67pmvDuA3TV9299L4CN60XSm8RRtX1EwUoKu9Pyw=",
147+ "owner": "justinwoo",
148+ "repo": "spago2nix",
149+ "rev": "1b8ec352bc7eac077b934d6b9f6efa0129926e59",
150+ "type": "github"
151+ },
152+ "original": {
153+ "owner": "justinwoo",
154+ "repo": "spago2nix",
155+ "type": "github"
156+ }
157+ },
158+ "vanilla": {
159+ "flake": false,
160+ "locked": {
161+ "lastModified": 1663430347,
162+ "narHash": "sha256-J+KyCcCMdC8SYINKAtUsNfM0/lnbwOnsY8TKOP+yY0s=",
163+ "owner": "ilyakooo0",
164+ "repo": "vanilla-compiled",
165+ "rev": "7a0ee46ce9b958b51c57b37c46e86a0a1868bfa9",
166+ "type": "github"
167+ },
168+ "original": {
169+ "owner": "ilyakooo0",
170+ "repo": "vanilla-compiled",
171+ "type": "github"
172+ }
173+ }
174+ },
175+ "root": "root",
176+ "version": 7
177+}
flake.nixnew file+133
@@ -0,0 +1,133 @@
1+{
2+ inputs = {
3+ flake-utils.url = "github:numtide/flake-utils";
4+ bw = {
5+ url =
6+ # Afer this commit they started requiring path at the top level and I can't deal with this right now
7+ "github:bitwarden/clients?rev=ff143760d4f527f2349333ee4b3004eeda68fac5";
8+ flake = false;
9+ };
10+ spago2nix.url = "github:justinwoo/spago2nix";
11+ easy-ps = {
12+ url = "github:justinwoo/easy-purescript-nix";
13+ flake = false;
14+ };
15+ vanilla = {
16+ url = "github:ilyakooo0/vanilla-compiled";
17+ flake = false;
18+ };
19+ nix-filter.url = "github:numtide/nix-filter";
20+ };
21+
22+ outputs = inputs@{ self, nixpkgs, flake-utils, spago2nix, ... }:
23+ flake-utils.lib.eachDefaultSystem (system:
24+ let
25+ pkgs = import nixpkgs { inherit system; };
26+ spagoPkgs = pkgs.callPackage ./spago-packages.nix { };
27+ easy-ps = pkgs.callPackage inputs.easy-ps { };
28+ nix-filter = import inputs.nix-filter;
29+
30+ bw = pkgs.buildNpmPackage {
31+ pname = "bw";
32+ version = "latest";
33+ npmDepsHash = "sha256-jQvL7/SzXqVk03ncU6Mxq8lqFFy+lIrF2QheheXrQ0w=";
34+ makeCacheWritable = true;
35+ src = inputs.bw;
36+ nativeBuildInputs = [ pkgs.python3 ];
37+ buildPhase = ''
38+ (cd libs/common/ && rm -r spec && find . -name "*.spec.ts" -exec rm {} \; && npm exec -c "tsc -m es2020 -t es2018") '';
39+ installPhase = ''
40+ mkdir -p $out
41+ cp -r libs/shared/dist/* $out
42+ cp -r node_modules $out/node_modules
43+ cp package.json $out/package.json
44+ '';
45+ ELECTRON_SKIP_BINARY_DOWNLOAD = 1;
46+ };
47+
48+ backendBuildScript = pkgs.writeScript "build-backend.sh" ''
49+ workdir=$(mktemp -d)
50+ (
51+ cd $workdir
52+ cp -r $1 ./backend
53+ cp -r ${bw} ./bw
54+ cp ${./backend-main.js} backend.js
55+ ${pkgs.esbuild}/bin/esbuild backend.js --bundle --define:ENVIRONMENT_IS_WEB=true --define:ENVIRONMENT_IS_WORKER=false --define:ENVIRONMENT_IS_NODE=false --define:ENVIRONMENT_IS_SHELL=false --external:fs --external:path --loader:.wasm=base64 --outfile=$out
56+ )
57+
58+ '';
59+ backend-js = pkgs.stdenv.mkDerivation {
60+ name = "backend-js";
61+ buildInputs =
62+ [ spagoPkgs.installSpagoStyle spagoPkgs.buildSpagoStyle ];
63+ nativeBuildInputs = [ easy-ps.purs-0_15_7 easy-ps.spago ];
64+ src = nix-filter {
65+ root = ./.;
66+ include = [
67+ ./spago.dhall
68+ ./packages.dhall
69+ (nix-filter.inDirectory ./backend)
70+ ];
71+ };
72+ unpackPhase = ''
73+ cp $src/spago.dhall .
74+ cp $src/packages.dhall .
75+ cp -r $src/backend .
76+ install-spago-style
77+ '';
78+ buildPhase = ''
79+ build-spago-style "./backend/**/*.purs"
80+ '';
81+ installPhase = ''
82+ mkdir $out
83+ cp -r output/* $out/
84+ '';
85+ };
86+
87+ backend =
88+ pkgs.runCommand "backend.js" { buildInputs = [ pkgs.esbuild ]; } ''
89+ ${backendBuildScript} ${backend-js}
90+ '';
91+
92+ frontend = pkgs.callPackage ./frontend.nix { inherit nix-filter; };
93+
94+ warden-src = pkgs.runCommand "warden-src" { } ''
95+ mkdir -p $out
96+
97+ cp ${backend} $out/backend.js
98+ cp -r ${frontend}/Main.min.js $out/elm.js
99+ cp ${./index.html} $out/index.html
100+ cp ${./style.css} $out/style.css
101+ cp -r ${inputs.vanilla} $out/vanilla
102+ '';
103+
104+ warden-click-src = pkgs.runCommand "warden-click-src" { } ''
105+ mkdir -p $out
106+
107+ cp ${./warden.apparmor} $out/warden.apparmor
108+ cp ${./warden.desktop} $out/warden.desktop
109+ cp ${./manifest.json} $out/manifest.json
110+ cp ${./logo.png} $out/logo.png
111+ cp -r ${warden-src}/* $out/
112+ '';
113+
114+ warden = pkgs.writeScript "warden" ''
115+ ${pkgs.electron}/bin/electron ${warden-src}/index.html
116+ '';
117+
118+ in {
119+ packages = {
120+ inherit bw frontend warden-src backendBuildScript warden-click-src;
121+ vanilla = inputs.vanilla.outPath;
122+ };
123+ apps = {
124+ inherit (spago2nix.apps.${system}) spago2nix;
125+ warden = {
126+ type = "app";
127+ program = warden.outPath;
128+ };
129+ };
130+
131+ devShells.default = import ./shell.nix { inherit pkgs; };
132+ });
133+}
frontend.nixnew file+49
@@ -0,0 +1,49 @@
1+{ pkgs, nix-filter }:
2+with pkgs;
3+
4+let
5+ mkDerivation = { srcs ? ./elm-srcs.nix, src, name, srcdir ? "./frontend"
6+ , targets ? [ ], registryDat ? ./registry.dat, outputJavaScript ? true }:
7+ stdenv.mkDerivation {
8+ inherit name src;
9+
10+ buildInputs = [ elmPackages.elm ]
11+ ++ lib.optional outputJavaScript nodePackages.uglify-js;
12+
13+ buildPhase = pkgs.elmPackages.fetchElmDeps {
14+ elmPackages = import srcs;
15+ elmVersion = "0.19.1";
16+ inherit registryDat;
17+ };
18+
19+ installPhase = let
20+ elmfile = module:
21+ "${srcdir}/${builtins.replaceStrings [ "." ] [ "/" ] module}.elm";
22+ extension = if outputJavaScript then "js" else "html";
23+ in ''
24+ mkdir -p $out/share/doc
25+ ${lib.concatStrings (map (module: ''
26+ echo "compiling ${elmfile module}"
27+ elm make ${
28+ elmfile module
29+ } --output $out/${module}.${extension} --docs $out/share/doc/${module}.json
30+ ${lib.optionalString outputJavaScript ''
31+ echo "minifying ${elmfile module}"
32+ uglifyjs $out/${module}.${extension} --compress 'pure_funcs="F2,F3,F4,F5,F6,F7,F8,F9,A2,A3,A4,A5,A6,A7,A8,A9",pure_getters,keep_fargs=false,unsafe_comps,unsafe' \
33+ | uglifyjs --mangle --output $out/${module}.min.${extension}
34+ ''}
35+ '') targets)}
36+ '';
37+ };
38+in mkDerivation {
39+ name = "frontend";
40+ srcs = ./elm-srcs.nix;
41+ src = nix-filter {
42+ root = ./.;
43+ include = [ (nix-filter.inDirectory ./frontend) ./elm.json ];
44+ };
45+ targets = [ "Main" ];
46+ srcdir = "./frontend";
47+ outputJavaScript = true;
48+}
49+
frontend/Bridge.elm+186−5
@@ -383,6 +383,17 @@ jsonEncCipher_LoginCipher_password_Maybe val = (maybeEncode (Json.Encode.string
383383
384384
385385
386+type alias Cipher_LoginCipher_totp_Maybe = (Maybe String)
387+
388+jsonDecCipher_LoginCipher_totp_Maybe : Json.Decode.Decoder ( Cipher_LoginCipher_totp_Maybe )
389+jsonDecCipher_LoginCipher_totp_Maybe =
390+ Json.Decode.maybe (Json.Decode.string)
391+
392+jsonEncCipher_LoginCipher_totp_Maybe : Cipher_LoginCipher_totp_Maybe -> Value
393+jsonEncCipher_LoginCipher_totp_Maybe val = (maybeEncode (Json.Encode.string)) val
394+
395+
396+
386397type alias Cipher_LoginCipher_uris_List = (List String)
387398
388399jsonDecCipher_LoginCipher_uris_List : Json.Decode.Decoder ( Cipher_LoginCipher_uris_List )
@@ -407,14 +418,16 @@ jsonEncCipher_LoginCipher_username_Maybe val = (maybeEncode (Json.Encode.string
407418
408419type alias Cipher_LoginCipher =
409420 { password: Cipher_LoginCipher_password_Maybe
421+ , totp: Cipher_LoginCipher_totp_Maybe
410422 , uris: Cipher_LoginCipher_uris_List
411423 , username: Cipher_LoginCipher_username_Maybe
412424 }
413425
414426jsonDecCipher_LoginCipher : Json.Decode.Decoder ( Cipher_LoginCipher )
415427jsonDecCipher_LoginCipher =
416− Json.Decode.succeed (\ppassword puris pusername -> {password = ppassword, uris = puris, username = pusername})
428+ Json.Decode.succeed (\ppassword ptotp puris pusername -> {password = ppassword, totp = ptotp, uris = puris, username = pusername})
417429 |> required "password" (jsonDecCipher_LoginCipher_password_Maybe)
430+ |> required "totp" (jsonDecCipher_LoginCipher_totp_Maybe)
418431 |> required "uris" (jsonDecCipher_LoginCipher_uris_List)
419432 |> required "username" (jsonDecCipher_LoginCipher_username_Maybe)
420433
@@ -422,6 +435,7 @@ jsonEncCipher_LoginCipher : Cipher_LoginCipher -> Value
422435jsonEncCipher_LoginCipher val =
423436 Json.Encode.object
424437 [ ("password", jsonEncCipher_LoginCipher_password_Maybe val.password)
438+ , ("totp", jsonEncCipher_LoginCipher_totp_Maybe val.totp)
425439 , ("uris", jsonEncCipher_LoginCipher_uris_List val.uris)
426440 , ("username", jsonEncCipher_LoginCipher_username_Maybe val.username)
427441 ]
@@ -477,17 +491,82 @@ jsonEncCipherType val =
477491
478492
479493
494+type alias Cmd_ChooseSecondFactor =
495+ { email: String
496+ , factor: TwoFactorProviderType
497+ , password: String
498+ , requestFromServer: Bool
499+ , server: String
500+ }
501+
502+jsonDecCmd_ChooseSecondFactor : Json.Decode.Decoder ( Cmd_ChooseSecondFactor )
503+jsonDecCmd_ChooseSecondFactor =
504+ Json.Decode.succeed (\pemail pfactor ppassword prequestFromServer pserver -> {email = pemail, factor = pfactor, password = ppassword, requestFromServer = prequestFromServer, server = pserver})
505+ |> required "email" (Json.Decode.string)
506+ |> required "factor" (jsonDecTwoFactorProviderType)
507+ |> required "password" (Json.Decode.string)
508+ |> required "requestFromServer" (Json.Decode.bool)
509+ |> required "server" (Json.Decode.string)
510+
511+jsonEncCmd_ChooseSecondFactor : Cmd_ChooseSecondFactor -> Value
512+jsonEncCmd_ChooseSecondFactor val =
513+ Json.Encode.object
514+ [ ("email", Json.Encode.string val.email)
515+ , ("factor", jsonEncTwoFactorProviderType val.factor)
516+ , ("password", Json.Encode.string val.password)
517+ , ("requestFromServer", Json.Encode.bool val.requestFromServer)
518+ , ("server", Json.Encode.string val.server)
519+ ]
520+
521+
522+
523+type alias Cmd_Login_secondFactor =
524+ { provider: TwoFactorProviderType
525+ , remember: Bool
526+ , token: String
527+ }
528+
529+jsonDecCmd_Login_secondFactor : Json.Decode.Decoder ( Cmd_Login_secondFactor )
530+jsonDecCmd_Login_secondFactor =
531+ Json.Decode.succeed (\pprovider premember ptoken -> {provider = pprovider, remember = premember, token = ptoken})
532+ |> required "provider" (jsonDecTwoFactorProviderType)
533+ |> required "remember" (Json.Decode.bool)
534+ |> required "token" (Json.Decode.string)
535+
536+jsonEncCmd_Login_secondFactor : Cmd_Login_secondFactor -> Value
537+jsonEncCmd_Login_secondFactor val =
538+ Json.Encode.object
539+ [ ("provider", jsonEncTwoFactorProviderType val.provider)
540+ , ("remember", Json.Encode.bool val.remember)
541+ , ("token", Json.Encode.string val.token)
542+ ]
543+
544+
545+
546+type alias Cmd_Login_secondFactor_Maybe = (Maybe Cmd_Login_secondFactor)
547+
548+jsonDecCmd_Login_secondFactor_Maybe : Json.Decode.Decoder ( Cmd_Login_secondFactor_Maybe )
549+jsonDecCmd_Login_secondFactor_Maybe =
550+ Json.Decode.maybe (jsonDecCmd_Login_secondFactor)
551+
552+jsonEncCmd_Login_secondFactor_Maybe : Cmd_Login_secondFactor_Maybe -> Value
553+jsonEncCmd_Login_secondFactor_Maybe val = (maybeEncode (jsonEncCmd_Login_secondFactor)) val
554+
555+
556+
480557type alias Cmd_Login =
481558 { email: String
482559 , password: String
560+ , secondFactor: Cmd_Login_secondFactor_Maybe
483561 , server: String
484562 }
485563
486564jsonDecCmd_Login : Json.Decode.Decoder ( Cmd_Login )
487565jsonDecCmd_Login =
488− Json.Decode.succeed (\pemail ppassword pserver -> {email = pemail, password = ppassword, server = pserver})
566+ Json.Decode.succeed (\pemail ppassword psecondFactor pserver -> {email = pemail, password = ppassword, secondFactor = psecondFactor, server = pserver})
489567 |> required "email" (Json.Decode.string)
490568 |> required "password" (Json.Decode.string)
569+ |> required "secondFactor" (jsonDecCmd_Login_secondFactor_Maybe)
491570 |> required "server" (Json.Decode.string)
492571
493572jsonEncCmd_Login : Cmd_Login -> Value
@@ -495,13 +574,17 @@ jsonEncCmd_Login val =
495574 Json.Encode.object
496575 [ ("email", Json.Encode.string val.email)
497576 , ("password", Json.Encode.string val.password)
577+ , ("secondFactor", jsonEncCmd_Login_secondFactor_Maybe val.secondFactor)
498578 , ("server", Json.Encode.string val.server)
499579 ]
500580
501581
502582
503583type Cmd =
504− Copy String
584+ ChooseSecondFactor Cmd_ChooseSecondFactor
585+ | Copy String
586+ | CreateCipher FullCipher
587+ | DeleteCipher FullCipher
505588 | GeneratePassword PasswordGeneratorConfig
506589 | Init
507590 | Login Cmd_Login
@@ -510,13 +593,17 @@ type Cmd =
510593 | NeedsReset
511594 | Open String
512595 | RequestCipher String
596+ | RequestTotp String
513597 | SendMasterPassword String
514598 | UpdateCipher FullCipher
515599
516600jsonDecCmd : Json.Decode.Decoder ( Cmd )
517601jsonDecCmd =
518602 let jsonDecDictCmd = Dict.fromList
519− [ ("Copy", Json.Decode.lazy (\_ -> Json.Decode.map Copy (Json.Decode.string)))
603+ [ ("ChooseSecondFactor", Json.Decode.lazy (\_ -> Json.Decode.map ChooseSecondFactor (jsonDecCmd_ChooseSecondFactor)))
604+ , ("Copy", Json.Decode.lazy (\_ -> Json.Decode.map Copy (Json.Decode.string)))
605+ , ("CreateCipher", Json.Decode.lazy (\_ -> Json.Decode.map CreateCipher (jsonDecFullCipher)))
606+ , ("DeleteCipher", Json.Decode.lazy (\_ -> Json.Decode.map DeleteCipher (jsonDecFullCipher)))
520607 , ("GeneratePassword", Json.Decode.lazy (\_ -> Json.Decode.map GeneratePassword (jsonDecPasswordGeneratorConfig)))
521608 , ("Init", Json.Decode.lazy (\_ -> Json.Decode.succeed Init))
522609 , ("Login", Json.Decode.lazy (\_ -> Json.Decode.map Login (jsonDecCmd_Login)))
@@ -525,6 +612,7 @@ jsonDecCmd =
525612 , ("NeedsReset", Json.Decode.lazy (\_ -> Json.Decode.succeed NeedsReset))
526613 , ("Open", Json.Decode.lazy (\_ -> Json.Decode.map Open (Json.Decode.string)))
527614 , ("RequestCipher", Json.Decode.lazy (\_ -> Json.Decode.map RequestCipher (Json.Decode.string)))
615+ , ("RequestTotp", Json.Decode.lazy (\_ -> Json.Decode.map RequestTotp (Json.Decode.string)))
528616 , ("SendMasterPassword", Json.Decode.lazy (\_ -> Json.Decode.map SendMasterPassword (Json.Decode.string)))
529617 , ("UpdateCipher", Json.Decode.lazy (\_ -> Json.Decode.map UpdateCipher (jsonDecFullCipher)))
530618 ]
@@ -534,7 +622,10 @@ jsonDecCmd =
534622jsonEncCmd : Cmd -> Value
535623jsonEncCmd val =
536624 let keyval v = case v of
625+ ChooseSecondFactor v1 -> ("ChooseSecondFactor", encodeValue (jsonEncCmd_ChooseSecondFactor v1))
537626 Copy v1 -> ("Copy", encodeValue (Json.Encode.string v1))
627+ CreateCipher v1 -> ("CreateCipher", encodeValue (jsonEncFullCipher v1))
628+ DeleteCipher v1 -> ("DeleteCipher", encodeValue (jsonEncFullCipher v1))
538629 GeneratePassword v1 -> ("GeneratePassword", encodeValue (jsonEncPasswordGeneratorConfig v1))
539630 Init -> ("Init", encodeValue (Json.Encode.list identity []))
540631 Login v1 -> ("Login", encodeValue (jsonEncCmd_Login v1))
@@ -543,14 +634,27 @@ jsonEncCmd val =
543634 NeedsReset -> ("NeedsReset", encodeValue (Json.Encode.list identity []))
544635 Open v1 -> ("Open", encodeValue (Json.Encode.string v1))
545636 RequestCipher v1 -> ("RequestCipher", encodeValue (Json.Encode.string v1))
637+ RequestTotp v1 -> ("RequestTotp", encodeValue (Json.Encode.string v1))
546638 SendMasterPassword v1 -> ("SendMasterPassword", encodeValue (Json.Encode.string v1))
547639 UpdateCipher v1 -> ("UpdateCipher", encodeValue (jsonEncFullCipher v1))
548640 in encodeSumTaggedObject "tag" "contents" keyval val
549641
550642
551643
644+type alias FullCipher_collectionIds_List = (List String)
645+
646+jsonDecFullCipher_collectionIds_List : Json.Decode.Decoder ( FullCipher_collectionIds_List )
647+jsonDecFullCipher_collectionIds_List =
648+ Json.Decode.list (Json.Decode.string)
649+
650+jsonEncFullCipher_collectionIds_List : FullCipher_collectionIds_List -> Value
651+jsonEncFullCipher_collectionIds_List val = (Json.Encode.list Json.Encode.string) val
652+
653+
654+
552655type alias FullCipher =
553656 { cipher: Cipher
657+ , collectionIds: FullCipher_collectionIds_List
554658 , favorite: Bool
555659 , id: String
556660 , name: String
@@ -559,8 +663,9 @@ type alias FullCipher =
559663
560664jsonDecFullCipher : Json.Decode.Decoder ( FullCipher )
561665jsonDecFullCipher =
562− Json.Decode.succeed (\pcipher pfavorite pid pname preprompt -> {cipher = pcipher, favorite = pfavorite, id = pid, name = pname, reprompt = preprompt})
666+ Json.Decode.succeed (\pcipher pcollectionIds pfavorite pid pname preprompt -> {cipher = pcipher, collectionIds = pcollectionIds, favorite = pfavorite, id = pid, name = pname, reprompt = preprompt})
563667 |> required "cipher" (jsonDecCipher)
668+ |> required "collectionIds" (jsonDecFullCipher_collectionIds_List)
564669 |> required "favorite" (Json.Decode.bool)
565670 |> required "id" (Json.Decode.string)
566671 |> required "name" (Json.Decode.string)
@@ -570,6 +675,7 @@ jsonEncFullCipher : FullCipher -> Value
570675jsonEncFullCipher val =
571676 Json.Encode.object
572677 [ ("cipher", jsonEncCipher val.cipher)
678+ , ("collectionIds", jsonEncFullCipher_collectionIds_List val.collectionIds)
573679 , ("favorite", Json.Encode.bool val.favorite)
574680 , ("id", Json.Encode.string val.id)
575681 , ("name", Json.Encode.string val.name)
@@ -694,9 +800,44 @@ jsonEncSub_NeedsMasterPassword val =
694800
695801
696802
803+type alias Sub_NeedsSecondFactor_List = (List TwoFactorProviderType)
804+
805+jsonDecSub_NeedsSecondFactor_List : Json.Decode.Decoder ( Sub_NeedsSecondFactor_List )
806+jsonDecSub_NeedsSecondFactor_List =
807+ Json.Decode.list (jsonDecTwoFactorProviderType)
808+
809+jsonEncSub_NeedsSecondFactor_List : Sub_NeedsSecondFactor_List -> Value
810+jsonEncSub_NeedsSecondFactor_List val = (Json.Encode.list jsonEncTwoFactorProviderType) val
811+
812+
813+
814+type alias Sub_Totp =
815+ { code: String
816+ , interval: Int
817+ , source: String
818+ }
819+
820+jsonDecSub_Totp : Json.Decode.Decoder ( Sub_Totp )
821+jsonDecSub_Totp =
822+ Json.Decode.succeed (\pcode pinterval psource -> {code = pcode, interval = pinterval, source = psource})
823+ |> required "code" (Json.Decode.string)
824+ |> required "interval" (Json.Decode.int)
825+ |> required "source" (Json.Decode.string)
826+
827+jsonEncSub_Totp : Sub_Totp -> Value
828+jsonEncSub_Totp val =
829+ Json.Encode.object
830+ [ ("code", Json.Encode.string val.code)
831+ , ("interval", Json.Encode.int val.interval)
832+ , ("source", Json.Encode.string val.source)
833+ ]
834+
835+
836+
697837type Sub =
698838 CaptchaDone
699839 | CipherChanged FullCipher
840+ | CipherDeleted FullCipher
700841 | Error String
701842 | GeneratedPassword String
702843 | LoadCipher FullCipher
@@ -705,14 +846,18 @@ type Sub =
705846 | NeedsCaptcha String
706847 | NeedsLogin
707848 | NeedsMasterPassword Sub_NeedsMasterPassword
849+ | NeedsSecondFactor Sub_NeedsSecondFactor_List
708850 | RecieveEmail String
709851 | Reset
852+ | Totp Sub_Totp
853+ | WrongPassword
710854
711855jsonDecSub : Json.Decode.Decoder ( Sub )
712856jsonDecSub =
713857 let jsonDecDictSub = Dict.fromList
714858 [ ("CaptchaDone", Json.Decode.lazy (\_ -> Json.Decode.succeed CaptchaDone))
715859 , ("CipherChanged", Json.Decode.lazy (\_ -> Json.Decode.map CipherChanged (jsonDecFullCipher)))
860+ , ("CipherDeleted", Json.Decode.lazy (\_ -> Json.Decode.map CipherDeleted (jsonDecFullCipher)))
716861 , ("Error", Json.Decode.lazy (\_ -> Json.Decode.map Error (Json.Decode.string)))
717862 , ("GeneratedPassword", Json.Decode.lazy (\_ -> Json.Decode.map GeneratedPassword (Json.Decode.string)))
718863 , ("LoadCipher", Json.Decode.lazy (\_ -> Json.Decode.map LoadCipher (jsonDecFullCipher)))
@@ -721,8 +866,11 @@ jsonDecSub =
721866 , ("NeedsCaptcha", Json.Decode.lazy (\_ -> Json.Decode.map NeedsCaptcha (Json.Decode.string)))
722867 , ("NeedsLogin", Json.Decode.lazy (\_ -> Json.Decode.succeed NeedsLogin))
723868 , ("NeedsMasterPassword", Json.Decode.lazy (\_ -> Json.Decode.map NeedsMasterPassword (jsonDecSub_NeedsMasterPassword)))
869+ , ("NeedsSecondFactor", Json.Decode.lazy (\_ -> Json.Decode.map NeedsSecondFactor (jsonDecSub_NeedsSecondFactor_List)))
724870 , ("RecieveEmail", Json.Decode.lazy (\_ -> Json.Decode.map RecieveEmail (Json.Decode.string)))
725871 , ("Reset", Json.Decode.lazy (\_ -> Json.Decode.succeed Reset))
872+ , ("Totp", Json.Decode.lazy (\_ -> Json.Decode.map Totp (jsonDecSub_Totp)))
873+ , ("WrongPassword", Json.Decode.lazy (\_ -> Json.Decode.succeed WrongPassword))
726874 ]
727875 jsonDecObjectSetSub = Set.fromList []
728876 in decodeSumTaggedObject "Sub" "tag" "contents" jsonDecDictSub jsonDecObjectSetSub
@@ -732,6 +880,7 @@ jsonEncSub val =
732880 let keyval v = case v of
733881 CaptchaDone -> ("CaptchaDone", encodeValue (Json.Encode.list identity []))
734882 CipherChanged v1 -> ("CipherChanged", encodeValue (jsonEncFullCipher v1))
883+ CipherDeleted v1 -> ("CipherDeleted", encodeValue (jsonEncFullCipher v1))
735884 Error v1 -> ("Error", encodeValue (Json.Encode.string v1))
736885 GeneratedPassword v1 -> ("GeneratedPassword", encodeValue (Json.Encode.string v1))
737886 LoadCipher v1 -> ("LoadCipher", encodeValue (jsonEncFullCipher v1))
@@ -740,7 +889,39 @@ jsonEncSub val =
740889 NeedsCaptcha v1 -> ("NeedsCaptcha", encodeValue (Json.Encode.string v1))
741890 NeedsLogin -> ("NeedsLogin", encodeValue (Json.Encode.list identity []))
742891 NeedsMasterPassword v1 -> ("NeedsMasterPassword", encodeValue (jsonEncSub_NeedsMasterPassword v1))
892+ NeedsSecondFactor v1 -> ("NeedsSecondFactor", encodeValue (jsonEncSub_NeedsSecondFactor_List v1))
743893 RecieveEmail v1 -> ("RecieveEmail", encodeValue (Json.Encode.string v1))
744894 Reset -> ("Reset", encodeValue (Json.Encode.list identity []))
895+ Totp v1 -> ("Totp", encodeValue (jsonEncSub_Totp v1))
896+ WrongPassword -> ("WrongPassword", encodeValue (Json.Encode.list identity []))
745897 in encodeSumTaggedObject "tag" "contents" keyval val
746898
899+
900+
901+type TwoFactorProviderType =
902+ Authenticator
903+ | Duo
904+ | Email
905+ | OrganizationDuo
906+ | Remember
907+ | U2f
908+ | WebAuthn
909+ | Yubikey
910+
911+jsonDecTwoFactorProviderType : Json.Decode.Decoder ( TwoFactorProviderType )
912+jsonDecTwoFactorProviderType =
913+ let jsonDecDictTwoFactorProviderType = Dict.fromList [("Authenticator", Authenticator), ("Duo", Duo), ("Email", Email), ("OrganizationDuo", OrganizationDuo), ("Remember", Remember), ("U2f", U2f), ("WebAuthn", WebAuthn), ("Yubikey", Yubikey)]
914+ in decodeSumUnaries "TwoFactorProviderType" jsonDecDictTwoFactorProviderType
915+
916+jsonEncTwoFactorProviderType : TwoFactorProviderType -> Value
917+jsonEncTwoFactorProviderType val =
918+ case val of
919+ Authenticator -> Json.Encode.string "Authenticator"
920+ Duo -> Json.Encode.string "Duo"
921+ Email -> Json.Encode.string "Email"
922+ OrganizationDuo -> Json.Encode.string "OrganizationDuo"
923+ Remember -> Json.Encode.string "Remember"
924+ U2f -> Json.Encode.string "U2f"
925+ WebAuthn -> Json.Encode.string "WebAuthn"
926+ Yubikey -> Json.Encode.string "Yubikey"
927+
frontend/GlobalEvents.elm+1
@@ -6,3 +6,4 @@ import Bridge
66type Event
77 = UpdateCipher Bridge.FullCipher
88 | GeneratedPassword String
9+ | DecodedTotp Bridge.Sub_Totp
frontend/Logic/Cipher.elm+3−2
@@ -49,14 +49,15 @@ normalizeIdentityCipher { address1, address2, address3, city, company, country,
4949 , ssn = normalizeStringMaybe ssn
5050 , state = normalizeStringMaybe state
5151 , title = normalizeStringMaybe title
52− , username = username
52+ , username = normalizeStringMaybe username
5353 }
5454
5555
5656normalizeLoginCipher : Bridge.Cipher_LoginCipher -> Bridge.Cipher_LoginCipher
57−normalizeLoginCipher { password, uris, username } =
57+normalizeLoginCipher { password, totp, uris, username } =
5858 { password = password
5959 , uris = uris |> List.map String.trim |> List.filter String.isEmpty
60+ , totp = normalizeStringMaybe totp
6061 , username = username
6162 }
6263
frontend/Logic/SecondFactor.elmnew file+31
@@ -0,0 +1,31 @@
1+module Logic.SecondFactor exposing (secondFactorName)
2+
3+import Bridge
4+
5+
6+secondFactorName : Bridge.TwoFactorProviderType -> String
7+secondFactorName factor =
8+ case factor of
9+ Bridge.Authenticator ->
10+ "Authenticator"
11+
12+ Bridge.Duo ->
13+ "Duo"
14+
15+ Bridge.Email ->
16+ "Email"
17+
18+ Bridge.OrganizationDuo ->
19+ "Organization Duo"
20+
21+ Bridge.Remember ->
22+ "Remember (IDK what this is)"
23+
24+ Bridge.U2f ->
25+ "U2f"
26+
27+ Bridge.WebAuthn ->
28+ "WebAuthn"
29+
30+ Bridge.Yubikey ->
31+ "Yubikey"
frontend/Main.elm+410−189
@@ -1,4 +1,4 @@
1−module Main exposing (..)
1+module Main exposing (main)
22
33import Bridge
44import Browser
@@ -16,6 +16,8 @@ import Pages.Loader exposing (loader)
1616import Pages.Login as Login
1717import Pages.MasterPassword as MasterPassword
1818import Pages.Navigation as Navigation exposing (TopButton(..))
19+import Pages.SecondFactor as SecondFactor
20+import Pages.SecondFactorSelect as SecondFactorSelect
1921import Task
2022import Time
2123import Types exposing (CipherId)
@@ -28,38 +30,51 @@ notificationLingerSeconds =
2830
2931
3032type Msg
31− = Noop
32− | RecieveMessage String
33− | ShowLoginPage
34− | ShowError String
33+ = ShowLoginPage
34+ | ShowError String String
3535 | ShowInfo String String
3636 | CloseNotification
37− | SubmitLogin { email : String, password : String, server : String }
38− | LoginMsg Login.Msg
39− | CiphersMsg Ciphers.Msg
37+ | SubmitLogin Bridge.Cmd_Login
4038 | LoadCiphers Bridge.Sub_LoadCiphers_List
41− | OpenCiphersScreen
39+ | LoginSuccessful
4240 | Reset
4341 | NeedsReset
44− | MasterPasswordMsg MasterPassword.Msg
4542 | SendMasterPassword { password : String }
4643 | ShowMasterPasswordPage { server : String, login : String }
47− | CipherMsg Cipher.Msg
4844 | ShowCipherPage Bridge.FullCipher
4945 | RequestCipher CipherId
5046 | PopView
5147 | RecieveEmail String
5248 | Copy String
5349 | Open String
54− | CaptchaMsg Captcha.Msg
55− | EditCipherMsg EditCipher.Msg
5650 | ShowCaptcha Captcha.HCaptchSiteKey
5751 | ClearNotification { currentTime : Time.Posix }
5852 | UpdateLastNotificatioTime Time.Posix
5953 | EditCipher Bridge.FullCipher
6054 | UpdateCipher Bridge.FullCipher
55+ | CreateCipher Bridge.FullCipher
6156 | FireGlobalEvent Event
6257 | GeneratePassword Bridge.PasswordGeneratorConfig
58+ | OpenNewCipherEditPage Bridge.CipherType
59+ | WrongPassword
60+ | DeleteCipher Bridge.FullCipher
61+ | RequestTotp String
62+ | ShowSecondFactorSelect (List Bridge.TwoFactorProviderType)
63+ | SelectSecondFactor
64+ { provider : Bridge.TwoFactorProviderType
65+ , requestFromServer : Bool
66+ }
67+ | PageMsg PageMsg
68+
69+
70+type PageMsg
71+ = LoginMsg Login.Msg
72+ | CiphersMsg Ciphers.Msg
73+ | MasterPasswordMsg MasterPassword.Msg
74+ | CipherMsg Cipher.Msg
75+ | EditCipherMsg EditCipher.Msg
76+ | SecondFactorSelectMsg SecondFactorSelect.Msg
77+ | SecondFactorMsg SecondFactor.Msg
6378
6479
6580type alias Model =
@@ -70,19 +85,45 @@ type alias Model =
7085 }
7186
7287
88+findLoginDetails :
89+ List PageModel
90+ ->
91+ Maybe
92+ { server : String
93+ , email : String
94+ , password : String
95+ }
96+findLoginDetails xs =
97+ case xs of
98+ (LoginModel { server, email, password }) :: _ ->
99+ Just
100+ { server = server
101+ , email = email
102+ , password = password
103+ }
104+
105+ _ :: rest ->
106+ findLoginDetails rest
107+
108+ [] ->
109+ Nothing
110+
111+
73112appendNotification : Notification.Config -> Model -> ( Model, Cmd Msg )
74113appendNotification cfg model =
75114 ( { model | notifications = cfg :: model.notifications }, Task.perform UpdateLastNotificatioTime Time.now )
76115
77116
78117type PageModel
79− = LoginModel Login.Model
118+ = LoginModel (Login.Model Msg)
80119 | LoadingPage
81− | CiphersModel Ciphers.Model
82− | EditCipherModel EditCipher.Model
83− | MasterPasswordModel MasterPassword.Model
84− | CipherModel Cipher.Model
120+ | CiphersModel (Ciphers.Model Msg)
121+ | EditCipherModel (EditCipher.Model Msg)
122+ | MasterPasswordModel (MasterPassword.Model Msg)
123+ | CipherModel (Cipher.Model Msg)
85124 | CaptchaModel Captcha.Model
125+ | SecondFactorSelectModel (SecondFactorSelect.Model Msg)
126+ | SecondFactorModel (SecondFactor.Model Msg)
86127
87128
88129showPage :
@@ -97,31 +138,43 @@ showPage email page =
97138 let
98139 simpleBackButton =
99140 BackButton { action = PopView, icon = Nothing }
141+
142+ liftPageWith :
143+ (pageMsg -> PageMsg)
144+ ->
145+ { title : List (Html pageMsg)
146+ , body : List (Html pageMsg)
147+ , topButton : Maybe (Navigation.TopButton Msg)
148+ }
149+ ->
150+ { title : List (Html Msg)
151+ , body : List (Html Msg)
152+ , topButton : Maybe (Navigation.TopButton Msg)
153+ }
154+ liftPageWith lift { title, body, topButton } =
155+ { title = title |> List.map (Html.map (lift >> PageMsg))
156+ , body = body |> List.map (Html.map (lift >> PageMsg))
157+ , topButton = topButton
158+ }
100159 in
101160 case page of
102161 LoginModel model ->
103− let
104− p =
105− Login.page loginCallbacks LoginMsg
106− in
107− { title = p.title model
108− , body = p.view model
109− , topButton = Nothing
110− }
162+ liftPageWith LoginMsg
163+ { title = [ text Login.title ]
164+ , body = Login.view model
165+ , topButton = Nothing
166+ }
111167
112168 CiphersModel model ->
113− let
114− p =
115− Ciphers.page ciphersCallbacks CiphersMsg
116− in
117− { title = p.title model
118− , body = p.view model
119− , topButton =
120− Ciphers.menuConfig email model
121− |> Navigation.mapMenuConfig CiphersMsg
122− |> Navigation.MenuButton
123− |> Just
124− }
169+ liftPageWith CiphersMsg
170+ { title = Ciphers.title model
171+ , body = Ciphers.view model
172+ , topButton =
173+ Ciphers.menuConfig email model
174+ |> Navigation.mapMenuConfig (CiphersMsg >> PageMsg)
175+ |> Navigation.MenuButton
176+ |> Just
177+ }
125178
126179 LoadingPage ->
127180 { title = []
@@ -130,44 +183,45 @@ showPage email page =
130183 }
131184
132185 MasterPasswordModel model ->
133− let
134− p =
135− MasterPassword.page masterPasswordCallbacks MasterPasswordMsg
136− in
137− { title = p.title model
138− , body = p.view model
139− , topButton = Nothing
140− }
186+ liftPageWith MasterPasswordMsg
187+ { title = [ text MasterPassword.title ]
188+ , body = MasterPassword.view model
189+ , topButton = Nothing
190+ }
141191
142192 CipherModel model ->
143− let
144− p =
145− Cipher.page cipherCallbacks CipherMsg
146− in
147− { title = p.title model
148− , body = p.view model
149− , topButton = Just simpleBackButton
150− }
193+ liftPageWith CipherMsg
194+ { title = Cipher.title model
195+ , body = Cipher.view model
196+ , topButton = Just simpleBackButton
197+ }
151198
152199 CaptchaModel model ->
153− let
154− p =
155− Captcha.page captchaCallbacks CaptchaMsg
156− in
157− { title = p.title model
158− , body = p.view model
200+ { title = [ text Captcha.title ]
201+ , body = Captcha.view model
159202 , topButton = Just simpleBackButton
160203 }
161204
162205 EditCipherModel model ->
163− let
164− p =
165− EditCipher.page editCipherCallbacks EditCipherMsg
166− in
167− { title = p.title model
168− , body = p.view model
169− , topButton = Just (BackButton { action = PopView, icon = Just "close" })
170− }
206+ liftPageWith EditCipherMsg
207+ { title = EditCipher.title model
208+ , body = EditCipher.view model
209+ , topButton = Just (BackButton { action = PopView, icon = Just "close" })
210+ }
211+
212+ SecondFactorSelectModel model ->
213+ liftPageWith SecondFactorSelectMsg
214+ { title = [ text SecondFactorSelect.title ]
215+ , body = SecondFactorSelect.view model
216+ , topButton = Just simpleBackButton
217+ }
218+
219+ SecondFactorModel model ->
220+ liftPageWith SecondFactorMsg
221+ { title = [ SecondFactor.title model |> text ]
222+ , body = [ SecondFactor.view model ]
223+ , topButton = Just simpleBackButton
224+ }
171225
172226
173227main : Program () Model Msg
@@ -183,20 +237,20 @@ main =
183237subscriptions : Model -> Sub Msg
184238subscriptions model =
185239 Sub.batch
186− ([ getBridge ShowError
240+ ([ getBridge (ShowError "Something went wrong!")
187241 (\msg ->
188242 case msg of
189243 Bridge.NeedsLogin ->
190244 ShowLoginPage
191245
192246 Bridge.Error err ->
193− ShowError err
247+ ShowError "Something went wrong!" err
194248
195249 Bridge.LoadCiphers ciphers ->
196250 LoadCiphers ciphers
197251
198252 Bridge.LoginSuccessful ->
199− OpenCiphersScreen
253+ LoginSuccessful
200254
201255 Bridge.Reset ->
202256 Reset
@@ -217,13 +271,66 @@ subscriptions model =
217271 PopView
218272
219273 Bridge.CipherChanged c ->
220− ShowInfo "Entry updated" ("The entry \"" ++ c.name ++ "\" has been successfully updated.")
274+ ShowInfo "Entry updated" ("The entry “" ++ c.name ++ "” has been successfully updated.")
221275
222276 Bridge.GeneratedPassword password ->
223277 GlobalEvents.GeneratedPassword password |> FireGlobalEvent
278+
279+ Bridge.WrongPassword ->
280+ WrongPassword
281+
282+ Bridge.CipherDeleted c ->
283+ ShowInfo "Entry deleted" ("The entry “" ++ c.name ++ "” has been successfully deleted.")
284+
285+ Bridge.Totp totp ->
286+ GlobalEvents.DecodedTotp totp |> FireGlobalEvent
287+
288+ Bridge.NeedsSecondFactor secondFactors ->
289+ case secondFactors of
290+ [ provider ] ->
291+ SelectSecondFactor
292+ { provider = provider
293+ , requestFromServer = False
294+ }
295+
296+ _ ->
297+ ShowSecondFactorSelect secondFactors
224298 )
225299 ]
226300 ++ optional (List.isEmpty model.notifications |> not) (Time.every 1000 (\t -> ClearNotification { currentTime = t }))
301+ ++ (model.pageStack
302+ |> Nonempty.toList
303+ |> List.map
304+ (\x ->
305+ case x of
306+ LoginModel _ ->
307+ Sub.none
308+
309+ LoadingPage ->
310+ Sub.none
311+
312+ CiphersModel _ ->
313+ Sub.none
314+
315+ EditCipherModel _ ->
316+ Sub.none
317+
318+ MasterPasswordModel _ ->
319+ Sub.none
320+
321+ CipherModel m ->
322+ Cipher.subscriptions m
323+
324+ CaptchaModel _ ->
325+ Sub.none
326+
327+ SecondFactorSelectModel _ ->
328+ Sub.none
329+
330+ SecondFactorModel _ ->
331+ Sub.none
332+ )
333+ )
227334 )
228335
229336
@@ -242,7 +349,10 @@ view : Model -> Html Msg
242349view model =
243350 div []
244351 (maybeList (List.head model.notifications) (Lazy.lazy2 Notification.notification CloseNotification)
245− ++ [ Lazy.lazy2 Navigation.showNavigationView model.pageStack (showPage (Maybe.withDefault "" model.userEmail)) ]
352+ ++ [ Lazy.lazy2 Navigation.showNavigationView
353+ model.pageStack
354+ (showPage (Maybe.withDefault "" model.userEmail))
355+ ]
246356 )
247357
248358
@@ -254,12 +364,6 @@ mapHead f (Nonempty x xx) =
254364doNotStoreInHistory : PageModel -> Bool
255365doNotStoreInHistory page =
256366 case page of
257− LoginModel _ ->
258− True
259−
260− MasterPasswordModel _ ->
261− True
262−
263367 LoadingPage ->
264368 True
265369
@@ -270,19 +374,9 @@ doNotStoreInHistory page =
270374update : Msg -> Model -> ( Model, Cmd Msg )
271375update msg model =
272376 let
273− processPage : (pageModel -> PageModel) -> ( Result String pageModel, Cmd Msg ) -> ( Model, Cmd Msg )
274− processPage liftModel ( resultModel, cmd ) =
275− case resultModel of
276− Ok mdl ->
277− ( { model | pageStack = mapHead (always (liftModel mdl)) model.pageStack }, cmd )
278−
279− Err err ->
280− appendNotification
281− { title = "An error had occured"
282− , message = err
283− , severity = Notification.Error
284− }
285− model
377+ updatePageStackHead : PageModel -> Model
378+ updatePageStackHead mdl =
379+ { model | pageStack = mapHead (always mdl) model.pageStack }
286380
287381 appendPageStack : PageModel -> Model
288382 appendPageStack mdl =
@@ -291,22 +385,11 @@ update msg model =
291385 keepStackWith : PageModel -> Model
292386 keepStackWith mdl =
293387 { model | pageStack = Nonempty.cons mdl model.pageStack }
294−
295− currentPage =
296− model.pageStack |> Nonempty.head
297388 in
298389 case msg of
299− RecieveMessage x ->
390+ ShowError title err ->
300391 appendNotification
301− { title = "Got a message!"
302− , message = x
303− , severity = Notification.Info
304− }
305− model
306−
307− ShowError err ->
308− appendNotification
309− { title = "Something went wrong"
392+ { title = title
310393 , message = err
311394 , severity = Notification.Error
312395 }
@@ -326,29 +409,14 @@ update msg model =
326409 )
327410
328411 SubmitLogin data ->
329− ( appendPageStack <| LoadingPage
412+ ( keepStackWith <| LoadingPage
330413 , FFI.sendBridge (Bridge.Login data)
331414 )
332415
333− LoginMsg imsg ->
334− case currentPage of
335− LoginModel page ->
336− (Login.page loginCallbacks LoginMsg).update imsg page |> processPage LoginModel
337−
338− _ ->
339− ( model, Cmd.none )
340−
341− CiphersMsg imsg ->
342− case currentPage of
343− CiphersModel page ->
344− (Ciphers.page ciphersCallbacks CiphersMsg).update imsg page |> processPage CiphersModel
345−
346− _ ->
347− ( model, Cmd.none )
348−
349416 ShowLoginPage ->
350− (Login.page loginCallbacks LoginMsg).init ()
351− |> Tuple.mapFirst (\pageModel -> appendPageStack <| LoginModel pageModel)
417+ ( Login.init loginCallbacks |> LoginModel |> appendPageStack
418+ , Cmd.none
419+ )
352420
353421 LoadCiphers ciphers ->
354422 let
@@ -364,9 +432,6 @@ update msg model =
364432 _ ->
365433 False
366434 )
367−
368− ( pageModel, cmd ) =
369− (Ciphers.page ciphersCallbacks CiphersMsg).init ciphers
370435 in
371436 ( if ciphersShown then
372437 { model
@@ -375,8 +440,8 @@ update msg model =
375440 |> Nonempty.map
376441 (\x ->
377442 case x of
378− CiphersModel _ ->
379− CiphersModel pageModel
443+ CiphersModel pageModel ->
444+ CiphersModel (Ciphers.updateCiphers ciphers pageModel)
380445
381446 other ->
382447 other
@@ -384,21 +449,24 @@ update msg model =
384449 }
385450
386451 else
387− appendPageStack <| CiphersModel pageModel
388− , cmd
452+ appendPageStack <| CiphersModel (Ciphers.init ciphersCallbacks ciphers)
453+ , Cmd.none
389454 )
390455
391− -- (Ciphers.page ciphersCallbacks CiphersMsg).init ciphers
392− -- |> Tuple.mapFirst (\pageModel -> appendPageStack <| CiphersModel pageModel)
393− OpenCiphersScreen ->
394− ( appendPageStack <| LoadingPage, Cmd.batch [ sendBridge Bridge.NeedCiphersList, sendBridge Bridge.NeedEmail ] )
456+ LoginSuccessful ->
457+ ( { model | pageStack = Nonempty.singleton LoadingPage }
458+ , Cmd.batch [ sendBridge Bridge.NeedCiphersList, sendBridge Bridge.NeedEmail ]
459+ )
395460
396461 Reset ->
397462 init
398463
399464 ShowMasterPasswordPage { server, login } ->
400− (MasterPassword.page masterPasswordCallbacks MasterPasswordMsg).init { server = server, login = login }
401− |> Tuple.mapFirst (\pageModel -> appendPageStack <| MasterPasswordModel pageModel)
465+ ( MasterPassword.init masterPasswordCallbacks { server = server, login = login }
466+ |> MasterPasswordModel
467+ |> appendPageStack
468+ , Cmd.none
469+ )
402470
403471 NeedsReset ->
404472 ( appendPageStack <| LoadingPage, sendBridge Bridge.NeedsReset )
@@ -406,32 +474,8 @@ update msg model =
406474 SendMasterPassword { password } ->
407475 ( appendPageStack <| LoadingPage, sendBridge (Bridge.SendMasterPassword password) )
408476
409− MasterPasswordMsg imsg ->
410− case currentPage of
411− MasterPasswordModel page ->
412− (MasterPassword.page masterPasswordCallbacks MasterPasswordMsg).update imsg page |> processPage MasterPasswordModel
413−
414− _ ->
415− ( model, Cmd.none )
416−
417− CipherMsg imsg ->
418− case currentPage of
419− CipherModel page ->
420− (Cipher.page cipherCallbacks CipherMsg).update imsg page |> processPage CipherModel
421−
422− _ ->
423− ( model, Cmd.none )
424−
425− EditCipherMsg imsg ->
426− case currentPage of
427− EditCipherModel page ->
428− (EditCipher.page editCipherCallbacks EditCipherMsg).update imsg page |> processPage EditCipherModel
429−
430− _ ->
431− ( model, Cmd.none )
432−
433477 ShowCipherPage cipher ->
434− (Cipher.page cipherCallbacks CipherMsg).init cipher
478+ Cipher.init cipherCallbacks cipher
435479 |> Tuple.mapFirst (\pageModel -> appendPageStack <| CipherModel pageModel)
436480
437481 RequestCipher id ->
@@ -452,16 +496,7 @@ update msg model =
452496 ( model, FFI.sendBridge (Bridge.Open uri) )
453497
454498 ShowCaptcha uri ->
455− (Captcha.page captchaCallbacks CaptchaMsg).init uri
456− |> Tuple.mapFirst (\pageModel -> keepStackWith <| CaptchaModel pageModel)
457−
458− CaptchaMsg imsg ->
459− case currentPage of
460− CaptchaModel page ->
461− (Captcha.page captchaCallbacks CaptchaMsg).update imsg page |> processPage CaptchaModel
462−
463− _ ->
464− ( model, Cmd.none )
499+ ( Captcha.init uri |> CaptchaModel |> appendPageStack, Cmd.none )
465500
466501 ClearNotification { currentTime } ->
467502 if Time.posixToMillis model.lastNotificationTime + notificationLingerSeconds * 1000 > Time.posixToMillis currentTime then
@@ -475,11 +510,8 @@ update msg model =
475510 UpdateLastNotificatioTime time ->
476511 ( { model | lastNotificationTime = time }, Cmd.none )
477512
478− Noop ->
479− ( model, Cmd.none )
480−
481513 EditCipher cipher ->
482− (EditCipher.page editCipherCallbacks EditCipherMsg).init cipher
514+ EditCipher.init { fullCipher = cipher, callbacks = editCipherCallbacks }
483515 |> Tuple.mapFirst (\pageModel -> keepStackWith <| EditCipherModel pageModel)
484516
485517 UpdateCipher cipher ->
@@ -491,6 +523,14 @@ update msg model =
491523 ]
492524 )
493525
526+ CreateCipher cipher ->
527+ ( model
528+ , Cmd.batch
529+ [ Bridge.CreateCipher cipher |> sendBridge
530+ , PopView |> pureCmd
531+ ]
532+ )
533+
494534 FireGlobalEvent ev ->
495535 ( { model
496536 | pageStack =
@@ -502,22 +542,28 @@ update msg model =
502542 LoadingPage
503543
504544 LoginModel m ->
505− (Login.page loginCallbacks LoginMsg).event m ev |> LoginModel
545+ LoginModel m
506546
507547 CiphersModel m ->
508− (Ciphers.page ciphersCallbacks CiphersMsg).event m ev |> CiphersModel
548+ Ciphers.event m ev |> CiphersModel
509549
510550 MasterPasswordModel m ->
511− (MasterPassword.page masterPasswordCallbacks MasterPasswordMsg).event m ev |> MasterPasswordModel
551+ MasterPasswordModel m
512552
513553 CipherModel m ->
514− (Cipher.page cipherCallbacks CipherMsg).event m ev |> CipherModel
554+ Cipher.event m ev |> CipherModel
515555
516556 EditCipherModel m ->
517− (EditCipher.page editCipherCallbacks EditCipherMsg).event m ev |> EditCipherModel
557+ EditCipher.event m ev |> EditCipherModel
518558
519559 CaptchaModel m ->
520− (Captcha.page captchaCallbacks CaptchaMsg).event m ev |> CaptchaModel
560+ CaptchaModel m
561+
562+ SecondFactorSelectModel m ->
563+ SecondFactorSelectModel m
564+
565+ SecondFactorModel m ->
566+ SecondFactorModel m
521567 )
522568 }
523569 , Cmd.none
@@ -526,30 +572,197 @@ update msg model =
526572 GeneratePassword cfg ->
527573 ( model, FFI.sendBridge (Bridge.GeneratePassword cfg) )
528574
575+ OpenNewCipherEditPage t ->
576+ EditCipher.init
577+ { callbacks = createCipherCallbacks
578+ , fullCipher =
579+ { reprompt = 0
580+ , favorite = False
581+ , id = ""
582+ , name = ""
583+ , collectionIds = []
584+ , cipher =
585+ case t of
586+ Bridge.LoginType ->
587+ Bridge.LoginCipher
588+ { uris = []
589+ , username = Nothing
590+ , password = Nothing
591+ , totp = Nothing
592+ }
593+
594+ Bridge.NoteType ->
595+ Bridge.NoteCipher ""
596+
597+ Bridge.CardType ->
598+ Bridge.CardCipher
599+ { cardholderName = Nothing
600+ , brand = Nothing
601+ , number = Nothing
602+ , expMonth = Nothing
603+ , expYear = Nothing
604+ , code = Nothing
605+ }
606+
607+ Bridge.IdentityType ->
608+ Bridge.IdentityCipher
609+ { title = Nothing
610+ , firstName = Nothing
611+ , middleName = Nothing
612+ , lastName = Nothing
613+ , address1 = Nothing
614+ , address2 = Nothing
615+ , address3 = Nothing
616+ , city = Nothing
617+ , state = Nothing
618+ , postalCode = Nothing
619+ , country = Nothing
620+ , company = Nothing
621+ , email = Nothing
622+ , phone = Nothing
623+ , ssn = Nothing
624+ , username = Nothing
625+ , passportNumber = Nothing
626+ , licenseNumber = Nothing
627+ }
628+ }
629+ }
630+ |> Tuple.mapFirst (\pageModel -> keepStackWith <| EditCipherModel pageModel)
631+
632+ WrongPassword ->
633+ ( model
634+ , Cmd.batch
635+ [ pureCmd PopView
636+ , pureCmd (ShowError "Login failed" "The login details you provided are not valid. Please try again.")
637+ ]
638+ )
639+
640+ DeleteCipher cipher ->
641+ ( model
642+ , Cmd.batch
643+ [ pureCmd PopView
644+ , pureCmd PopView
645+ , FFI.sendBridge (Bridge.DeleteCipher cipher)
646+ ]
647+ )
648+
649+ RequestTotp totp ->
650+ ( model, FFI.sendBridge (Bridge.RequestTotp totp) )
651+
652+ ShowSecondFactorSelect secondFactors ->
653+ ( SecondFactorSelect.init secondFactorSelectCallbacks secondFactors
654+ |> SecondFactorSelectModel
655+ |> appendPageStack
656+ , Cmd.none
657+ )
658+
659+ SelectSecondFactor { provider, requestFromServer } ->
660+ case Nonempty.toList model.pageStack |> findLoginDetails of
661+ Just { email, password, server } ->
662+ ( SecondFactor.init
663+ { provider = provider
664+ , callbacks =
665+ { submit =
666+ \{ token, remember } ->
667+ SubmitLogin
668+ { email = email
669+ , password = password
670+ , server = server
671+ , secondFactor =
672+ Just
673+ { provider = provider
674+ , token = token
675+ , remember = remember
676+ }
677+ }
678+ }
679+ }
680+ |> SecondFactorModel
681+ |> appendPageStack
682+ , Bridge.ChooseSecondFactor
683+ { factor = provider
684+ , email = email
685+ , password = password
686+ , server = server
687+ , requestFromServer = requestFromServer
688+ }
689+ |> FFI.sendBridge
690+ )
691+
692+ Nothing ->
693+ ( model, Cmd.none )
694+
695+ PageMsg pageMsg ->
696+ case ( pageMsg, model.pageStack |> Nonempty.head ) of
697+ ( SecondFactorMsg imsg, SecondFactorModel m ) ->
698+ SecondFactor.update m imsg
699+ |> Tuple.mapBoth
700+ (SecondFactorModel >> updatePageStackHead)
701+ (Maybe.map pureCmd >> Maybe.withDefault Cmd.none)
702+
703+ ( LoginMsg imsg, LoginModel page ) ->
704+ Login.update imsg page
705+ |> Tuple.mapFirst (LoginModel >> updatePageStackHead)
706+
707+ ( CiphersMsg imsg, CiphersModel page ) ->
708+ Ciphers.update imsg page
709+ |> Tuple.mapFirst (CiphersModel >> updatePageStackHead)
710+
711+ ( MasterPasswordMsg imsg, MasterPasswordModel page ) ->
712+ MasterPassword.update imsg page
713+ |> Tuple.mapFirst (MasterPasswordModel >> updatePageStackHead)
714+
715+ ( CipherMsg imsg, CipherModel page ) ->
716+ Cipher.update imsg page
717+ |> Tuple.mapFirst (CipherModel >> updatePageStackHead)
718+
719+ ( EditCipherMsg imsg, EditCipherModel page ) ->
720+ EditCipher.update imsg page
721+ |> Tuple.mapFirst (EditCipherModel >> updatePageStackHead)
722+
723+ ( SecondFactorSelectMsg imsg, SecondFactorSelectModel m ) ->
724+ SecondFactorSelect.update imsg m
725+ |> Tuple.mapFirst (SecondFactorSelectModel >> updatePageStackHead)
726+
727+ _ ->
728+ ( model, Cmd.none )
729+
529730
530731loginCallbacks : Login.Callbacks Msg
531732loginCallbacks =
532− { submit = SubmitLogin }
733+ { submit =
734+ \{ email, server, password } ->
735+ SubmitLogin
736+ { email = email
737+ , password = password
738+ , server = server
739+ , secondFactor = Nothing
740+ }
741+ }
533742
534743
535744ciphersCallbacks : Ciphers.Callbacks Msg
536745ciphersCallbacks =
537− { selected = RequestCipher, logOut = NeedsReset }
746+ { selected = RequestCipher
747+ , logOut = NeedsReset
748+ , createNewCipher = OpenNewCipherEditPage
749+ , lift = CiphersMsg >> PageMsg
750+ }
538751
539752
540753cipherCallbacks : Cipher.Callbacks Msg
541754cipherCallbacks =
542− { copy = Copy, open = Open, edit = EditCipher }
755+ { copy = Copy, open = Open, edit = EditCipher, needTotp = RequestTotp }
543756
544757
545758editCipherCallbacks : EditCipher.Callbacks Msg
546759editCipherCallbacks =
547− { save = UpdateCipher, generatePassword = GeneratePassword }
760+ { save = UpdateCipher, generatePassword = GeneratePassword, delete = Just DeleteCipher }
548761
549762
550−captchaCallbacks : Captcha.Callbacks
551−captchaCallbacks =
552− {}
763+createCipherCallbacks : EditCipher.Callbacks Msg
764+createCipherCallbacks =
765+ { save = CreateCipher, generatePassword = GeneratePassword, delete = Nothing }
553766
554767
555768masterPasswordCallbacks : MasterPassword.Callbacks Msg
@@ -557,3 +770,11 @@ masterPasswordCallbacks =
557770 { submit = SendMasterPassword
558771 , reset = NeedsReset
559772 }
773+
774+
775+secondFactorSelectCallbacks : SecondFactorSelect.Callbacks Msg
776+secondFactorSelectCallbacks =
777+ { selectFactor =
778+ \provider ->
779+ SelectSecondFactor { provider = provider, requestFromServer = True }
780+ }
frontend/Page.elm+23−15
@@ -1,24 +1,32 @@
11module Page exposing (..)
22
3−import GlobalEvents exposing (Event)
3+import Browser
44import Html exposing (..)
55import Html.Events exposing (..)
66
77
8−type alias Env =
9− {}
10−
8+page :
9+ { init : model
10+ , view : model -> List (Html msg)
11+ , update : msg -> model -> ( model, Cmd (Maybe msg) )
12+ }
13+ -> Program () model (Maybe msg)
14+page { init, view, update } =
15+ Browser.element
16+ { subscriptions = always Sub.none
17+ , init = \_ -> ( init, Cmd.none )
18+ , view = view >> Html.div [] >> Html.map Just
19+ , update =
20+ \mmsg model ->
21+ case mmsg of
22+ Nothing ->
23+ ( model, Cmd.none )
1124
12−type alias Page init model imsg emsg =
13− (imsg -> emsg)
14− -> InitializedPage init model imsg emsg
25+ Just msg ->
26+ update msg model
27+ }
1528
1629
17−type alias InitializedPage init model imsg emsg =
18− { init : init -> ( model, Cmd emsg )
19− , view : model -> List (Html emsg)
20− , update : imsg -> model -> ( Result String model, Cmd emsg )
21− , subscriptions : model -> Sub emsg
22− , title : model -> List (Html emsg)
23− , event : model -> Event -> model
24− }
30+noUpdate : a -> model -> ( model, Cmd msg )
31+noUpdate _ model =
32+ ( model, Cmd.none )
frontend/Pages/Captcha.elm+22−35
@@ -1,4 +1,4 @@
1−module Pages.Captcha exposing (Callbacks, HCaptchSiteKey, Model, Msg, page)
1+module Pages.Captcha exposing (HCaptchSiteKey, Model, init, main, title, view)
22
33import Html exposing (..)
44import Html.Attributes as Attr
@@ -6,6 +6,15 @@ import Page exposing (..)
66import Utils exposing (..)
77
88
9+main : Program () Model (Maybe (Maybe ()))
10+main =
11+ page
12+ { init = init "10000000-ffff-ffff-ffff-000000000001"
13+ , view = view
14+ , update = noUpdate
15+ }
16+
17+
918type alias HCaptchSiteKey =
1019 String
1120
@@ -15,44 +24,22 @@ type alias Model =
1524 }
1625
1726
18−type Msg
19− = Noop
20−
27+title : String
28+title =
29+ "Captcha"
2130
22−type alias Callbacks =
23− {}
24−
25−
26−page : Callbacks -> Page HCaptchSiteKey Model Msg emsg
27−page callbacks liftMsg =
28− { init = \siteKey -> Tuple.mapSecond (Cmd.map liftMsg) (init siteKey)
29− , view = \model -> view model |> List.map (Html.map liftMsg)
30− , update = \msg model -> update callbacks liftMsg msg model
31− , subscriptions = \model -> subscriptions model |> Sub.map liftMsg
32− , title = always [ text "Captcha" ]
33− , event = \model _ -> model
34− }
3531
36−
37−init : HCaptchSiteKey -> ( Model, Cmd Msg )
32+init : HCaptchSiteKey -> Model
3833init siteKey =
39− ( { siteKey = siteKey }, Cmd.none )
40−
41−
42−update : Callbacks -> (Msg -> emsg) -> Msg -> Model -> ( Result String Model, Cmd emsg )
43−update _ _ msg model =
44− case msg of
45− Noop ->
46− ( Ok model, Cmd.none )
47−
48−
49−subscriptions : Model -> Sub Msg
50−subscriptions model =
51− Sub.none
34+ { siteKey = siteKey }
5235
5336
54−view : Model -> List (Html Msg)
37+view : Model -> List (Html msg)
5538view { siteKey } =
56− [ div [ Attr.class "u-embedded-media" ]
57− [ iframe [ Attr.src ("https://iko.soy/warden/hcaptcha.html?" ++ siteKey) ] [] ]
39+ [ iframe
40+ [ Attr.src ("https://iko.soy/warden/hcaptcha.html?" ++ siteKey)
41+ , Attr.style "width" "100%"
42+ , Attr.style "height" "80vh"
43+ ]
44+ []
5845 ]
frontend/Pages/Cipher.elm+114−42
@@ -1,4 +1,14 @@
1−module Pages.Cipher exposing (..)
1+module Pages.Cipher exposing
2+ ( Callbacks
3+ , Model
4+ , Msg
5+ , event
6+ , init
7+ , subscriptions
8+ , title
9+ , update
10+ , view
11+ )
212
313import Bridge
414import GlobalEvents
@@ -6,13 +16,16 @@ import Html exposing (..)
616import Html.Attributes as Attr
717import Html.Events as Ev
818import Page exposing (..)
19+import Time
920import Utils exposing (..)
1021
1122
12−type alias Model =
23+type alias Model emsg =
1324 { cipher : Bridge.FullCipher
1425 , passwordHidden : Bool
1526 , cvvHidden : Bool
27+ , decodedTotp : Maybe { code : String, interval : Int }
28+ , callbacks : Callbacks emsg
1629 }
1730
1831
@@ -21,73 +34,107 @@ type Msg
2134 | ToggleCVVVisiblity
2235 | Copy String
2336 | Open String
37+ | Edit Bridge.FullCipher
2438
2539
2640type alias Callbacks msg =
2741 { copy : String -> msg
2842 , open : String -> msg
2943 , edit : Bridge.FullCipher -> msg
44+ , needTotp : String -> msg
3045 }
3146
3247
33−page : Callbacks emsg -> Page Bridge.FullCipher Model Msg emsg
34−page callbacks liftMsg =
35− { init = \data -> Tuple.mapSecond (Cmd.map liftMsg) (init data)
36− , view = \model -> view model |> List.map (Html.map liftMsg)
37− , update = \msg model -> update callbacks liftMsg msg model
38− , subscriptions = \model -> subscriptions model |> Sub.map liftMsg
39− , title =
40− \{ cipher } ->
41− [ text cipher.name
42− , span [ Attr.class "u-float-right" ] [ iconButton "edit" (callbacks.edit cipher) ]
43− ]
44− , event =
45− \model ev ->
46− case ev of
47− GlobalEvents.UpdateCipher c ->
48− { model
49− | cipher =
50− if c.id == model.cipher.id then
51− c
52−
53− else
54− model.cipher
55− }
48+event : Model emsg -> GlobalEvents.Event -> Model emsg
49+event model ev =
50+ case ev of
51+ GlobalEvents.UpdateCipher c ->
52+ { model
53+ | cipher =
54+ if c.id == model.cipher.id then
55+ c
56+
57+ else
58+ model.cipher
59+ }
60+
61+ GlobalEvents.DecodedTotp { source, code, interval } ->
62+ case model.cipher.cipher of
63+ Bridge.LoginCipher { totp } ->
64+ if totp == Just source then
65+ { model | decodedTotp = Just { code = code, interval = interval } }
66+
67+ else
68+ model
5669
5770 _ ->
5871 model
59− }
72+
73+ _ ->
74+ model
75+
76+
77+title : Model emsg -> List (Html Msg)
78+title { cipher } =
79+ [ text cipher.name
80+ , span [ Attr.class "u-float-right" ] [ iconButton "edit" (Edit cipher) ]
81+ ]
6082
6183
62−init : Bridge.FullCipher -> ( Model, Cmd Msg )
63−init cipher =
84+init : Callbacks emsg -> Bridge.FullCipher -> ( Model emsg, Cmd emsg )
85+init callbacks cipher =
6486 ( { cipher = cipher
6587 , passwordHidden = True
6688 , cvvHidden = True
89+ , decodedTotp = Nothing
90+ , callbacks = callbacks
6791 }
68− , Cmd.none
92+ , case cipher.cipher of
93+ Bridge.LoginCipher { totp } ->
94+ case totp of
95+ Just x ->
96+ callbacks.needTotp x |> pureCmd
97+
98+ Nothing ->
99+ Cmd.none
100+
101+ _ ->
102+ Cmd.none
69103 )
70104
71105
72−update : Callbacks emsg -> (Msg -> emsg) -> Msg -> Model -> ( Result String Model, Cmd emsg )
73−update { copy, open } _ msg model =
106+update : Msg -> Model emsg -> ( Model emsg, Cmd emsg )
107+update msg model =
74108 case msg of
75109 TogglePasswordVisiblity ->
76− ( Ok { model | passwordHidden = not model.passwordHidden }, Cmd.none )
110+ ( { model | passwordHidden = not model.passwordHidden }, Cmd.none )
77111
78112 ToggleCVVVisiblity ->
79− ( Ok { model | cvvHidden = not model.cvvHidden }, Cmd.none )
113+ ( { model | cvvHidden = not model.cvvHidden }, Cmd.none )
80114
81115 Copy text ->
82− ( Ok model, copy text |> pureCmd )
116+ ( model, model.callbacks.copy text |> pureCmd )
83117
84118 Open uri ->
85− ( Ok model, open uri |> pureCmd )
119+ ( model, model.callbacks.open uri |> pureCmd )
120+
121+ Edit cipher ->
122+ ( model, model.callbacks.edit cipher |> pureCmd )
123+
124+
125+subscriptions : Model msg -> Sub msg
126+subscriptions { cipher, callbacks } =
127+ case cipher.cipher of
128+ Bridge.LoginCipher { totp } ->
129+ case totp of
130+ Just x ->
131+ Time.every 1000 (always (callbacks.needTotp x))
86132
133+ Nothing ->
134+ Sub.none
87135
88−subscriptions : Model -> Sub Msg
89−subscriptions _ =
90− Sub.none
136+ _ ->
137+ Sub.none
91138
92139
93140row : { name : String, value : String, nameIcon : String, icons : List ( String, msg ) } -> Html msg
@@ -100,10 +147,10 @@ row { name, value, nameIcon, icons } =
100147 ]
101148
102149
103−view : Model -> List (Html Msg)
104−view { passwordHidden, cipher, cvvHidden } =
150+view : Model emsg -> List (Html Msg)
151+view { passwordHidden, cipher, cvvHidden, decodedTotp } =
105152 (case cipher.cipher of
106− Bridge.LoginCipher { username, password, uris } ->
153+ Bridge.LoginCipher { username, password, uris, totp } ->
107154 maybeList username
108155 (\x ->
109156 row
@@ -123,11 +170,36 @@ view { passwordHidden, cipher, cvvHidden } =
123170 , icons = [ ( hiddenButtonIcon passwordHidden, TogglePasswordVisiblity ), ( "copy", Copy x ) ]
124171 }
125172 )
173+ ++ maybeList totp
174+ (\_ ->
175+ row
176+ { name = "One-time password"
177+ , value =
178+ case decodedTotp of
179+ Nothing ->
180+ "Loading..."
181+
182+ Just { code } ->
183+ code
184+ , nameIcon = "revisions"
185+ , icons =
186+ case decodedTotp of
187+ Nothing ->
188+ []
189+
190+ Just { code } ->
191+ [ ( "copy", Copy code ) ]
192+ }
193+ )
126194 ++ (uris
127195 |> List.map
128196 (\uri ->
129197 row
130− { name = "URL", value = uri, nameIcon = "get-link", icons = [ ( "external-link", Open uri ), ( "copy", Copy uri ) ] }
198+ { name = "URL"
199+ , value = uri
200+ , nameIcon = "get-link"
201+ , icons = [ ( "external-link", Open uri ), ( "copy", Copy uri ) ]
202+ }
131203 )
132204 )
133205
frontend/Pages/Ciphers.elm+151−84
@@ -1,4 +1,15 @@
1−module Pages.Ciphers exposing (Callbacks, Model, Msg, menuConfig, page)
1+module Pages.Ciphers exposing
2+ ( Callbacks
3+ , Model
4+ , Msg
5+ , event
6+ , init
7+ , menuConfig
8+ , title
9+ , update
10+ , updateCiphers
11+ , view
12+ )
213
314import Bridge
415import GlobalEvents
@@ -14,16 +25,23 @@ import Search
1425import Utils exposing (..)
1526
1627
17−type alias Model =
28+type alias Model emsg =
1829 { ciphers : Bridge.Sub_LoadCiphers_List
1930 , search : String
2031 , menuVisible : Bool
2132 , ciphersListFilter : CiphersFilter
2233 , scroll : InfiniteScroll.Model Msg
2334 , sublist : { end : Int }
35+ , cipherSelectDropdownVisivble : Bool
36+ , callbacks : Callbacks emsg
2437 }
2538
2639
40+updateCiphers : Bridge.Sub_LoadCiphers_List -> Model emsg -> Model emsg
41+updateCiphers ciphers model =
42+ { model | ciphers = ciphers }
43+
44+
2745type CiphersFilter
2846 = AllCiphers
2947 | SpecificCiphers Bridge.CipherType
@@ -39,7 +57,7 @@ applyCipherFilter filter list =
3957 list |> List.filter (.cipherType >> (==) t)
4058
4159
42−menuConfig : String -> Model -> Navigation.MenuConfig Msg
60+menuConfig : String -> Model emsg -> Navigation.MenuConfig Msg
4361menuConfig email { ciphersListFilter, menuVisible } =
4462 { title = email
4563 , items =
@@ -64,7 +82,7 @@ menuConfig email { ciphersListFilter, menuVisible } =
6482 , current = ciphersListFilter == SpecificCiphers Bridge.CardType
6583 }
6684 , { icon = "user"
67− , name = "Identities"
85+ , name = "Contacts"
6886 , trigger = UpdateFilter (SpecificCiphers Bridge.IdentityType)
6987 , current = ciphersListFilter == SpecificCiphers Bridge.IdentityType
7088 }
@@ -83,94 +101,137 @@ type Msg
83101 | LogOut
84102 | LoadMore InfiniteScroll.Direction
85103 | InfiniteScrollMsg InfiniteScroll.Msg
104+ | CreateNewCipher Bridge.CipherType
105+ | ToggleSelectCipherTypeVisible
106+ | SelectCipher String
86107
87108
88109type alias Callbacks msg =
89110 { selected : String -> msg
90111 , logOut : msg
112+ , createNewCipher : Bridge.CipherType -> msg
113+ , lift : Msg -> msg
91114 }
92115
93116
94−page : Callbacks emsg -> Page Bridge.Sub_LoadCiphers_List Model Msg emsg
95−page callbacks liftMsg =
96− { init = \ciphers -> Tuple.mapSecond (Cmd.map liftMsg) (init ciphers)
97− , view = view callbacks liftMsg
98− , update = \msg model -> update callbacks liftMsg msg model
99− , subscriptions = \model -> subscriptions model |> Sub.map liftMsg
100− , title =
101− \{ ciphersListFilter } ->
102− [ text
103− (case ciphersListFilter of
104− AllCiphers ->
105− "All items"
106−
107− SpecificCiphers cipherType ->
108− case cipherType of
109− Bridge.CardType ->
110− "Cards"
111−
112− Bridge.IdentityType ->
113− "Identities"
114−
115− Bridge.LoginType ->
116− "Passwords"
117−
118− Bridge.NoteType ->
119− "Notes"
120− )
121− ]
122− , event =
123− \model ev ->
124− case ev of
125− GlobalEvents.UpdateCipher c ->
126− { model
127− | ciphers =
128− model.ciphers
129− |> List.map
130− (\cipher ->
131− if cipher.id == c.id then
132− { cipher | name = c.name }
133−
134− else
135− cipher
136− )
137− }
138−
139− _ ->
140− model
141− }
117+event : Model emsg -> GlobalEvents.Event -> Model emsg
118+event model ev =
119+ case ev of
120+ GlobalEvents.UpdateCipher c ->
121+ { model
122+ | ciphers =
123+ model.ciphers
124+ |> List.map
125+ (\cipher ->
126+ if cipher.id == c.id then
127+ { cipher | name = c.name }
142128
129+ else
130+ cipher
131+ )
132+ }
133+
134+ _ ->
135+ model
143136
144−init : Bridge.Sub_LoadCiphers_List -> ( Model, Cmd Msg )
145−init ciphers =
146− ( { ciphers = ciphers
147− , search = ""
148− , menuVisible = False
149− , ciphersListFilter = AllCiphers
150− , scroll = InfiniteScroll.init (LoadMore >> pureCmd)
151− , sublist = { end = 20 }
152− }
153− , Cmd.none
154− )
155137
138+title : Model emsg -> List (Html Msg)
139+title { ciphersListFilter, cipherSelectDropdownVisivble } =
140+ [ text
141+ (case ciphersListFilter of
142+ AllCiphers ->
143+ "All items"
156144
157−update : Callbacks emsg -> (Msg -> emsg) -> Msg -> Model -> ( Result String Model, Cmd emsg )
158−update { logOut } liftMsg msg model =
145+ SpecificCiphers cipherType ->
146+ case cipherType of
147+ Bridge.CardType ->
148+ "Cards"
149+
150+ Bridge.IdentityType ->
151+ "Contacts"
152+
153+ Bridge.LoginType ->
154+ "Passwords"
155+
156+ Bridge.NoteType ->
157+ "Notes"
158+ )
159+ , span [ Attr.class "u-float-right" ]
160+ [ span [ Attr.class "p-contextual-menu" ]
161+ ([ span [] [ iconButton "plus" ToggleSelectCipherTypeVisible ]
162+ ]
163+ ++ optionals cipherSelectDropdownVisivble
164+ [ div
165+ [ Attr.class "overlay"
166+ , Ev.onClick ToggleSelectCipherTypeVisible
167+ ]
168+ []
169+ , span
170+ [ Attr.class "p-contextual-menu__dropdown"
171+ , Attr.attribute "aria-hidden" "false"
172+ ]
173+ (let
174+ create t =
175+ CreateNewCipher t
176+ in
177+ [ button
178+ [ Attr.class "p-contextual-menu__link"
179+ , Ev.onClick (create Bridge.LoginType)
180+ ]
181+ [ text "Login" ]
182+ , button
183+ [ Attr.class "p-contextual-menu__link"
184+ , Ev.onClick (create Bridge.CardType)
185+ ]
186+ [ text "Card" ]
187+ , button
188+ [ Attr.class "p-contextual-menu__link"
189+ , Ev.onClick (create Bridge.NoteType)
190+ ]
191+ [ text "Note" ]
192+ , button
193+ [ Attr.class "p-contextual-menu__link"
194+ , Ev.onClick (create Bridge.IdentityType)
195+ ]
196+ [ text "Contact" ]
197+ ]
198+ )
199+ ]
200+ )
201+ ]
202+ ]
203+
204+
205+init : Callbacks emsg -> Bridge.Sub_LoadCiphers_List -> Model emsg
206+init callbacks ciphers =
207+ { ciphers = ciphers
208+ , search = ""
209+ , menuVisible = False
210+ , ciphersListFilter = AllCiphers
211+ , scroll = InfiniteScroll.init (LoadMore >> pureCmd)
212+ , sublist = { end = 20 }
213+ , cipherSelectDropdownVisivble = False
214+ , callbacks = callbacks
215+ }
216+
217+
218+update : Msg -> Model emsg -> ( Model emsg, Cmd emsg )
219+update msg model =
159220 case msg of
160221 Noop ->
161− ( Ok model, Cmd.none )
222+ ( model, Cmd.none )
162223
163224 UpdateSearch s ->
164− ( Ok { model | search = s }, Cmd.none )
225+ ( { model | search = s }, Cmd.none )
165226
166227 ToggleMenu ->
167− ( Ok { model | menuVisible = not model.menuVisible }, Cmd.none )
228+ ( { model | menuVisible = not model.menuVisible }, Cmd.none )
168229
169230 UpdateFilter filter ->
170− ( Ok { model | ciphersListFilter = filter, menuVisible = False }, Cmd.none )
231+ ( { model | ciphersListFilter = filter, menuVisible = False }, Cmd.none )
171232
172233 LogOut ->
173− ( Ok model, pureCmd logOut )
234+ ( model, pureCmd model.callbacks.logOut )
174235
175236 LoadMore direction ->
176237 let
@@ -182,48 +243,54 @@ update { logOut } liftMsg msg model =
182243 InfiniteScroll.Bottom ->
183244 { end = min (model.sublist.end + 10) (List.length model.ciphers) }
184245 in
185− ( Ok { model | sublist = newSublist, scroll = InfiniteScroll.stopLoading model.scroll }, Cmd.none )
246+ ( { model | sublist = newSublist, scroll = InfiniteScroll.stopLoading model.scroll }, Cmd.none )
186247
187248 InfiniteScrollMsg msg_ ->
188249 let
189250 ( scroll, cmd ) =
190251 InfiniteScroll.update InfiniteScrollMsg msg_ model.scroll
191252 in
192− ( Ok { model | scroll = scroll }, Cmd.map liftMsg cmd )
253+ ( { model | scroll = scroll }, Cmd.map model.callbacks.lift cmd )
254+
255+ CreateNewCipher t ->
256+ ( { model | cipherSelectDropdownVisivble = False }, pureCmd (model.callbacks.createNewCipher t) )
193257
258+ ToggleSelectCipherTypeVisible ->
259+ ( { model | cipherSelectDropdownVisivble = not model.cipherSelectDropdownVisivble }
260+ , Cmd.none
261+ )
194262
195−subscriptions : Model -> Sub Msg
196−subscriptions _ =
197− Sub.none
263+ SelectCipher id ->
264+ ( model, model.callbacks.selected id |> pureCmd )
198265
199266
200−view : Callbacks emsg -> (Msg -> emsg) -> Model -> List (Html emsg)
201−view { selected } liftMsg { ciphers, search, ciphersListFilter, sublist } =
267+view : Model emsg -> List (Html Msg)
268+view { ciphers, search, ciphersListFilter, sublist } =
202269 [ input
203270 [ Attr.type_ "search"
204271 , Attr.value search
205− , Ev.onInput (liftMsg << UpdateSearch)
272+ , Ev.onInput UpdateSearch
206273 , Attr.placeholder "Search"
207274 , Attr.attribute "autocomplete" "off"
208275 ]
209276 []
210277 , ciphers
211278 |> applyCipherFilter ciphersListFilter
212− |> Search.searchList search .name (List.take sublist.end >> Lazy.lazy3 showCiphers liftMsg selected)
279+ |> Search.searchList search .name (List.take sublist.end >> Lazy.lazy showCiphers)
213280 ]
214281
215282
216−showCiphers : (Msg -> msg) -> (String -> msg) -> Bridge.Sub_LoadCiphers_List -> Html msg
217−showCiphers liftMsg selected ciphers =
218− Keyed.ul [ Attr.class "p-list--divided", InfiniteScroll.infiniteScroll (InfiniteScrollMsg >> liftMsg), Attr.class "ciphers-list no-scroll-bar" ]
283+showCiphers : Bridge.Sub_LoadCiphers_List -> Html Msg
284+showCiphers ciphers =
285+ Keyed.ul [ Attr.class "p-list--divided", InfiniteScroll.infiniteScroll InfiniteScrollMsg, Attr.class "ciphers-list no-scroll-bar" ]
219286 (ciphers
220287 |> List.map
221288 (\{ name, date, id } ->
222289 ( id
223− , li [ Attr.class "p-list__item cipher-row", Ev.onClick (selected id) ]
290+ , li [ Attr.class "p-list__item cipher-row", Ev.onClick (SelectCipher id) ]
224291 [ div [ Attr.class "cipher-row-container" ]
225292 [ p [] [ text name ]
226− , p [ Attr.class "p-text--small u-align-text--right u-text--muted" ] [ text date ]
293+ , p [ Attr.class "p-text--small u-align-text--right u-text--muted hide-in-test" ] [ text date ]
227294 ]
228295 ]
229296 )
frontend/Pages/EditCipher.elm+101−64
@@ -1,19 +1,22 @@
1−module Pages.EditCipher exposing (..)
1+module Pages.EditCipher exposing (Callbacks, Model, Msg, event, init, title, update, view)
22
33import Bridge
44import GlobalEvents
55import Html exposing (..)
66import Html.Attributes as Attr
77import Html.Events as Ev
8+import Logic.Cipher exposing (normalizeCipher)
89import Modal exposing (modal)
910import Page exposing (..)
11+import Pages.MasterPassword exposing (Msg)
1012import Pages.PasswordGenerator as PasswordGenerator
1113import Utils exposing (..)
1214
1315
14−type alias Model =
16+type alias Model msg =
1517 { fullCipher : Bridge.FullCipher
1618 , passwordGenerator : Maybe PasswordGenerator.Model
19+ , callbacks : Callbacks msg
1720 }
1821
1922
@@ -23,95 +26,109 @@ type Msg
2326 | OpenGeneratePasword
2427 | CloseGeneratePassword
2528 | GeneratePassword Bridge.PasswordGeneratorConfig
29+ | Delete
30+ | Save
2631
2732
2833type alias Callbacks msg =
2934 { save : Bridge.FullCipher -> msg
3035 , generatePassword : Bridge.PasswordGeneratorConfig -> msg
36+ , delete : Maybe (Bridge.FullCipher -> msg)
3137 }
3238
3339
34−page : Callbacks emsg -> Page Bridge.FullCipher Model Msg emsg
35−page callbacks liftMsg =
36− { init = \data -> Tuple.mapSecond (Cmd.map liftMsg) (init data)
37− , view = \model -> view model |> List.map (Html.map liftMsg)
38− , update = \msg model -> update callbacks liftMsg msg model
39− , subscriptions = \model -> subscriptions model |> Sub.map liftMsg
40− , title =
41− \{ fullCipher } ->
42− [ text fullCipher.name
43− , span [ Attr.class "u-float-right" ] [ iconButton "task-outstanding" (callbacks.save fullCipher) ]
44− ]
45− , event =
46− \model ev ->
47− case ev of
48− GlobalEvents.UpdateCipher c ->
49− { model
50− | fullCipher =
51− if c.id == model.fullCipher.id then
52− c
53−
54− else
55− model.fullCipher
40+title : Model msg -> List (Html Msg)
41+title { fullCipher } =
42+ [ text fullCipher.name
43+ , span [ Attr.class "u-float-right" ]
44+ [ iconButton "task-outstanding" Save
45+ ]
46+ ]
47+
48+
49+event : Model msg -> GlobalEvents.Event -> Model msg
50+event model ev =
51+ case ev of
52+ GlobalEvents.UpdateCipher c ->
53+ { model
54+ | fullCipher =
55+ if c.id == model.fullCipher.id then
56+ c
57+
58+ else
59+ model.fullCipher
60+ }
61+
62+ GlobalEvents.GeneratedPassword password ->
63+ let
64+ fullCipher =
65+ model.fullCipher
66+ in
67+ { model
68+ | passwordGenerator = Nothing
69+ , fullCipher =
70+ { fullCipher
71+ | cipher =
72+ case fullCipher.cipher of
73+ Bridge.LoginCipher c ->
74+ Bridge.LoginCipher
75+ { c | password = Just password }
76+
77+ c ->
78+ c
5679 }
80+ }
5781
58− GlobalEvents.GeneratedPassword password ->
59− let
60− fullCipher =
61− model.fullCipher
62− in
63− { model
64− | passwordGenerator = Nothing
65− , fullCipher =
66− { fullCipher
67− | cipher =
68− case fullCipher.cipher of
69− Bridge.LoginCipher c ->
70− Bridge.LoginCipher
71− { c | password = Just password }
72−
73− c ->
74− c
75− }
76− }
77− }
82+ _ ->
83+ model
7884
7985
80−init : Bridge.FullCipher -> ( Model, Cmd Msg )
81−init cipher =
82− ( { fullCipher = cipher
86+init : { fullCipher : Bridge.FullCipher, callbacks : Callbacks emsg } -> ( Model emsg, Cmd emsg )
87+init { fullCipher, callbacks } =
88+ ( { fullCipher = fullCipher
8389 , passwordGenerator = Nothing
90+ , callbacks = callbacks
8491 }
8592 , Cmd.none
8693 )
8794
8895
89−update : Callbacks emsg -> (Msg -> emsg) -> Msg -> Model -> ( Result String Model, Cmd emsg )
90−update { generatePassword } _ msg model =
96+update : Msg -> Model emsg -> ( Model emsg, Cmd emsg )
97+update msg model =
9198 case msg of
9299 EditCipher fullCipher ->
93− ( Ok { model | fullCipher = fullCipher }, Cmd.none )
100+ ( { model | fullCipher = fullCipher }, Cmd.none )
94101
95102 PasswordGeneratorMsg m ->
96− ( Ok { model | passwordGenerator = Maybe.map (PasswordGenerator.update m) model.passwordGenerator }
103+ ( { model | passwordGenerator = Maybe.map (PasswordGenerator.update m) model.passwordGenerator }
97104 , Cmd.none
98105 )
99106
100107 OpenGeneratePasword ->
101− ( Ok { model | passwordGenerator = Just PasswordGenerator.init }
108+ ( { model | passwordGenerator = Just PasswordGenerator.init }
102109 , Cmd.none
103110 )
104111
105112 CloseGeneratePassword ->
106− ( Ok { model | passwordGenerator = Nothing }, Cmd.none )
113+ ( { model | passwordGenerator = Nothing }, Cmd.none )
107114
108115 GeneratePassword cfg ->
109− ( Ok model, pureCmd (generatePassword cfg) )
116+ ( model, pureCmd (model.callbacks.generatePassword cfg) )
110117
118+ Delete ->
119+ ( model
120+ , Maybe.withDefault Cmd.none
121+ (Maybe.map (\f -> f model.fullCipher |> pureCmd) model.callbacks.delete)
122+ )
111123
112−subscriptions : Model -> Sub Msg
113−subscriptions _ =
114− Sub.none
124+ Save ->
125+ let
126+ { fullCipher } =
127+ model
128+ in
129+ ( model
130+ , model.callbacks.save { fullCipher | cipher = normalizeCipher fullCipher.cipher } |> pureCmd
131+ )
115132
116133
117134heading : String -> String -> Html msg
@@ -128,8 +145,8 @@ row { name, nameIcon, attrs } =
128145 ]
129146
130147
131−view : Model -> List (Html Msg)
132−view { fullCipher, passwordGenerator } =
148+view : Model emsg -> List (Html Msg)
149+view { fullCipher, passwordGenerator, callbacks } =
133150 maybeList passwordGenerator
134151 (\x ->
135152 modal
@@ -137,7 +154,7 @@ view { fullCipher, passwordGenerator } =
137154 , close = CloseGeneratePassword
138155 , footer =
139156 [ button
140− [ Attr.class "p-button--positive"
157+ [ Attr.class "p-button--positive u-no-margin"
141158 , Ev.onClick <| GeneratePassword x.passwordConfig
142159 ]
143160 [ text "Generate password" ]
@@ -160,7 +177,7 @@ view { fullCipher, passwordGenerator } =
160177 ++ (case fullCipher.cipher of
161178 Bridge.LoginCipher cipher ->
162179 let
163− { username, password, uris } =
180+ { username, password, uris, totp } =
164181 cipher
165182
166183 edit c =
@@ -189,6 +206,16 @@ view { fullCipher, passwordGenerator } =
189206 []
190207 , iconButton "restart" OpenGeneratePasword
191208 ]
209+ , row
210+ { name = "One-time password"
211+ , nameIcon = "revisions"
212+ , attrs =
213+ [ Attr.type_ "text"
214+ , Attr.value (Maybe.withDefault "" totp)
215+ , Ev.onInput (\x -> edit { cipher | totp = Just x })
216+ , Attr.attribute "autocomplete" "off"
217+ ]
218+ }
192219 ]
193220 ++ (uris
194221 |> List.indexedMap
@@ -220,7 +247,7 @@ view { fullCipher, passwordGenerator } =
220247 , br [] []
221248 , textarea
222249 [ Attr.spellcheck True
223− , Attr.style "height" "70vh"
250+ , Attr.style "height" "65vh"
224251 , Attr.value contents
225252 , Ev.onInput (\x -> EditCipher { fullCipher | cipher = Bridge.NoteCipher x })
226253 ]
@@ -254,7 +281,8 @@ view { fullCipher, passwordGenerator } =
254281 , Attr.type_ "text"
255282 , Attr.value (Maybe.withDefault "" expMonth)
256283 , Ev.onInput (\x -> edit { cipher | expMonth = Just x })
257− , Attr.style "width" "20%"
284+ , Attr.style "width" "30%"
285+ , Attr.style "min-width" "initial"
258286 , Attr.class "u-align--right"
259287 ]
260288 []
@@ -264,7 +292,8 @@ view { fullCipher, passwordGenerator } =
264292 , Attr.type_ "text"
265293 , Attr.value (Maybe.withDefault "" expYear)
266294 , Ev.onInput (\x -> edit { cipher | expYear = Just x })
267− , Attr.style "width" "20%"
295+ , Attr.style "width" "30%"
296+ , Attr.style "min-width" "initial"
268297 ]
269298 []
270299 ]
@@ -430,6 +459,14 @@ view { fullCipher, passwordGenerator } =
430459 }
431460 ]
432461 )
462+ ++ optional (callbacks.delete /= Nothing)
463+ (button
464+ [ Attr.style "width" "100%"
465+ , Attr.class "p-button--negative"
466+ , Ev.onClick Delete
467+ ]
468+ [ text "Delete" ]
469+ )
433470
434471
435472iconButton : String -> msg -> Html msg
frontend/Pages/Login.elm+38−42
@@ -1,4 +1,4 @@
1−module Pages.Login exposing (Callbacks, Model, Msg, page)
1+module Pages.Login exposing (Callbacks, Model, Msg, init, title, update, view)
22
33import Html exposing (..)
44import Html.Attributes as Attr
@@ -7,10 +7,11 @@ import Page exposing (..)
77import Utils exposing (..)
88
99
10−type alias Model =
10+type alias Model emsg =
1111 { server : String
1212 , email : String
1313 , password : String
14+ , callbacks : Callbacks emsg
1415 }
1516
1617
@@ -26,53 +27,49 @@ type alias Callbacks emsg =
2627 }
2728
2829
29−page : Callbacks emsg -> Page () Model Msg emsg
30−page callbacks liftMsg =
31− { init = \() -> Tuple.mapSecond (Cmd.map liftMsg) init
32− , view = \model -> view model |> List.map (Html.map liftMsg)
33− , update = \msg model -> update callbacks liftMsg msg model
34− , subscriptions = \model -> subscriptions model |> Sub.map liftMsg
35− , title = always [ text "Log into bitwarden" ]
36− , event = \model _ -> model
37− }
30+title : String
31+title =
32+ "Log into bitwarden"
3833
3934
40−init : ( Model, Cmd Msg )
41−init =
42− ( { server = "https://vault.bitwarden.com"
43− , email = "ilyakooo0@gmail.com"
44− , password = ""
45− }
46− , Cmd.none
47− )
35+init : Callbacks emsg -> Model emsg
36+init callbacks =
37+ { server = "https://vault.bitwarden.com"
38+ , email = ""
39+ , password = ""
40+ , callbacks = callbacks
41+ }
4842
4943
50−update : Callbacks emsg -> (Msg -> emsg) -> Msg -> Model -> ( Result String Model, Cmd emsg )
51−update { submit } liftMsg msg model =
44+update : Msg -> Model emsg -> ( Model emsg, Cmd emsg )
45+update msg model =
5246 case msg of
5347 UpdateEmail email ->
54− ( Ok { model | email = email }, Cmd.none )
48+ ( { model | email = email }, Cmd.none )
5549
5650 UpdateServer server ->
57− ( Ok { model | server = server }, Cmd.none )
51+ ( { model | server = server }, Cmd.none )
5852
5953 UpdatePassword password ->
60− ( Ok { model | password = password }, Cmd.none )
54+ ( { model | password = password }, Cmd.none )
6155
6256 Submit ->
63− ( Ok model, pureCmd (submit model) )
64−
65−
66−subscriptions : Model -> Sub Msg
67−subscriptions model =
68− Sub.none
69−
70−
71−view : Model -> List (Html Msg)
57+ ( model
58+ , pureCmd
59+ (model.callbacks.submit
60+ { email = model.email
61+ , password = model.password
62+ , server = model.server
63+ }
64+ )
65+ )
66+
67+
68+view : Model emsg -> List (Html Msg)
7269view model =
7370 [ form [ Ev.onSubmit Submit ]
74− ([ label [] [ text "Server" ]
75− , input
71+ [ label [] [ text "Server" ]
72+ , input
7673 [ Attr.type_ "url"
7774 , Attr.value model.server
7875 , Ev.onInput UpdateServer
@@ -80,8 +77,8 @@ view model =
8077 , Attr.required True
8178 ]
8279 []
83− , label [] [ text "Email" ]
84− , input
80+ , label [] [ text "Email" ]
81+ , input
8582 [ Attr.type_ "email"
8683 , Attr.value model.email
8784 , Ev.onInput UpdateEmail
@@ -89,8 +86,8 @@ view model =
8986 , Attr.required True
9087 ]
9188 []
92− , label [] [ text "Password" ]
93− , input
89+ , label [] [ text "Password" ]
90+ , input
9491 [ Attr.type_ "password"
9592 , Attr.value model.password
9693 , Ev.onInput UpdatePassword
@@ -98,7 +95,6 @@ view model =
9895 , Attr.required True
9996 ]
10097 []
101− ]
102− ++ [ alignRight [ button [ Attr.type_ "submit" ] [ text "Log in" ] ] ]
103− )
98+ , alignRight [ button [ Attr.type_ "submit" ] [ text "Log in" ] ]
99+ ]
104100 ]
frontend/Pages/MasterPassword.elm+19−30
@@ -1,4 +1,4 @@
1−module Pages.MasterPassword exposing (..)
1+module Pages.MasterPassword exposing (Callbacks, Model, Msg, init, title, update, view)
22
33import Html exposing (..)
44import Html.Attributes as Attr
@@ -7,10 +7,11 @@ import Page exposing (..)
77import Utils exposing (..)
88
99
10−type alias Model =
10+type alias Model emsg =
1111 { server : String
1212 , email : String
1313 , password : String
14+ , callbacks : Callbacks emsg
1415 }
1516
1617
@@ -26,46 +27,34 @@ type alias Callbacks emsg =
2627 }
2728
2829
29−page : Callbacks emsg -> Page { server : String, login : String } Model Msg emsg
30−page callbacks liftMsg =
31− { init = \cfg -> Tuple.mapSecond (Cmd.map liftMsg) (init cfg)
32− , view = \model -> view model |> List.map (Html.map liftMsg)
33− , update = \msg model -> update callbacks liftMsg msg model
34− , subscriptions = \model -> subscriptions model |> Sub.map liftMsg
35− , title = always [ text "Enter master password" ]
36− , event = \model _ -> model
37− }
30+title : String
31+title =
32+ "Enter master password"
3833
3934
40−init : { server : String, login : String } -> ( Model, Cmd Msg )
41−init { server, login } =
42− ( { server = server
43− , email = login
44− , password = ""
45− }
46− , Cmd.none
47− )
35+init : Callbacks emsg -> { server : String, login : String } -> Model emsg
36+init callbacks { server, login } =
37+ { server = server
38+ , email = login
39+ , password = ""
40+ , callbacks = callbacks
41+ }
4842
4943
50−update : Callbacks emsg -> (Msg -> emsg) -> Msg -> Model -> ( Result String Model, Cmd emsg )
51−update { submit, reset } _ msg model =
44+update : Msg -> Model emsg -> ( Model emsg, Cmd emsg )
45+update msg model =
5246 case msg of
5347 UpdatePassword password ->
54− ( Ok { model | password = password }, Cmd.none )
48+ ( { model | password = password }, Cmd.none )
5549
5650 Submit ->
57− ( Ok model, pureCmd (submit { password = model.password }) )
51+ ( model, pureCmd (model.callbacks.submit { password = model.password }) )
5852
5953 Reset ->
60− ( Ok model, pureCmd reset )
61−
62−
63−subscriptions : Model -> Sub Msg
64−subscriptions _ =
65− Sub.none
54+ ( model, pureCmd model.callbacks.reset )
6655
6756
68−view : Model -> List (Html Msg)
57+view : Model emsg -> List (Html Msg)
6958view model =
7059 [ alignRight [ button [ Attr.class "p-button--negative", Ev.onClick Reset ] [ text "Log out" ] ]
7160 , table [ Attr.class "p-table--mobile-card u-no-margin" ]
frontend/Pages/SecondFactor.elmnew file+91
@@ -0,0 +1,91 @@
1+module Pages.SecondFactor exposing
2+ ( Callbacks
3+ , Model
4+ , Msg
5+ , init
6+ , title
7+ , update
8+ , view
9+ )
10+
11+import Bridge
12+import Html exposing (..)
13+import Html.Attributes as Attr
14+import Html.Events as Ev
15+import Logic.SecondFactor exposing (secondFactorName)
16+import Utils exposing (..)
17+
18+
19+type alias Callbacks msg =
20+ { submit : { token : String, remember : Bool } -> msg
21+ }
22+
23+
24+type Msg
25+ = Submit
26+ | UpdateValue String
27+ | UpdateRemember Bool
28+
29+
30+type alias Model msg =
31+ { token : String
32+ , remember : Bool
33+ , factorType : Bridge.TwoFactorProviderType
34+ , callbacks : Callbacks msg
35+ }
36+
37+
38+init :
39+ { provider : Bridge.TwoFactorProviderType
40+ , callbacks : Callbacks msg
41+ }
42+ -> Model msg
43+init { provider, callbacks } =
44+ { token = ""
45+ , remember = True
46+ , factorType = provider
47+ , callbacks = callbacks
48+ }
49+
50+
51+title : Model msg -> String
52+title { factorType } =
53+ secondFactorName factorType
54+
55+
56+update : Model emsg -> Msg -> ( Model emsg, Maybe emsg )
57+update model msg =
58+ case msg of
59+ Submit ->
60+ ( model, model.callbacks.submit { token = model.token, remember = model.remember } |> Just )
61+
62+ UpdateValue token ->
63+ ( { model | token = token }, Nothing )
64+
65+ UpdateRemember remember ->
66+ ( { model | remember = remember }, Nothing )
67+
68+
69+view : Model emsg -> Html Msg
70+view { factorType, token, remember } =
71+ form [ Ev.onSubmit Submit ]
72+ [ label [] [ secondFactorName factorType ++ " code" |> text ]
73+ , input
74+ [ Attr.type_ "number"
75+ , Attr.value token
76+ , Attr.required True
77+ , Ev.onInput UpdateValue
78+ , Attr.attribute "autocomplete" "one-time-code"
79+ ]
80+ []
81+ , label []
82+ [ input
83+ [ Attr.type_ "checkbox"
84+ , Attr.checked remember
85+ , Ev.onCheck UpdateRemember
86+ ]
87+ []
88+ , text "Remember this device"
89+ ]
90+ , alignRight [ button [ Attr.type_ "submit" ] [ text "Submit" ] ]
91+ ]
frontend/Pages/SecondFactorSelect.elmnew file+164
@@ -0,0 +1,164 @@
1+module Pages.SecondFactorSelect exposing
2+ ( Callbacks
3+ , Model
4+ , Msg
5+ , init
6+ , main
7+ , title
8+ , update
9+ , view
10+ )
11+
12+import Bridge
13+import Element as El
14+import Element.Font as Font
15+import Element.Input as Input
16+import Html
17+import Html.Attributes as Attr
18+import Logic.SecondFactor exposing (secondFactorName)
19+import Page exposing (..)
20+import Utils exposing (..)
21+
22+
23+main : Program () (Model (Maybe Msg)) (Maybe Msg)
24+main =
25+ Page.page
26+ { init =
27+ init { selectFactor = always Nothing }
28+ [ Bridge.Duo
29+ , Bridge.Email
30+ ]
31+ , view = view
32+ , update = update
33+ }
34+
35+
36+type alias Model emsg =
37+ { availableFactors : List Bridge.TwoFactorProviderType
38+ , callbacks : Callbacks emsg
39+ }
40+
41+
42+type Msg
43+ = SelectFactor Bridge.TwoFactorProviderType
44+
45+
46+type alias Callbacks emsg =
47+ { selectFactor : Bridge.TwoFactorProviderType -> emsg
48+ }
49+
50+
51+title : String
52+title =
53+ "Second factor"
54+
55+
56+init : Callbacks emsg -> List Bridge.TwoFactorProviderType -> Model emsg
57+init callbacks providers =
58+ { availableFactors = providers
59+ , callbacks = callbacks
60+ }
61+
62+
63+update : Msg -> Model emsg -> ( Model emsg, Cmd emsg )
64+update msg model =
65+ case msg of
66+ SelectFactor factor ->
67+ ( model, model.callbacks.selectFactor factor |> pureCmd )
68+
69+
70+view : Model emsg -> List (Html.Html Msg)
71+view { availableFactors } =
72+ let
73+ ( supportedProviders, unsupportedProviders ) =
74+ List.partition providerIsSupported availableFactors
75+ in
76+ [ El.layout [ El.padding 8 ]
77+ (El.column
78+ [ El.width El.fill
79+ , El.spacing 8
80+ ]
81+ ([ if List.isEmpty supportedProviders then
82+ El.column []
83+ [ El.paragraph []
84+ [ El.text "Unfortunately, your account has no compatible 2FA providers."
85+ ]
86+ , El.el
87+ [ El.height (El.px 16)
88+ ]
89+ El.none
90+ ]
91+
92+ else
93+ El.column [ El.centerX ]
94+ (supportedProviders
95+ |> List.map
96+ (\factor ->
97+ Input.button [ El.centerX ]
98+ { label =
99+ Html.button [ Attr.class "p-button" ] [ secondFactorName factor |> Html.text ]
100+ |> El.html
101+ , onPress = SelectFactor factor |> Just
102+ }
103+ )
104+ )
105+ ]
106+ ++ (if List.isEmpty unsupportedProviders |> not then
107+ [ Html.div
108+ [ Attr.class "p-notification--information"
109+ ]
110+ [ Html.div [ Attr.class "p-notification__content" ]
111+ [ Html.h5 [ Attr.class "p-notification__title" ]
112+ [ Html.text "The following 2FA providers are not supported"
113+ ]
114+ , Html.p [ Attr.class "notification__message" ]
115+ [ Html.ul []
116+ (List.map
117+ (secondFactorName >> Html.text >> List.singleton >> Html.li [])
118+ unsupportedProviders
119+ )
120+ ]
121+ ]
122+ ]
123+ |> El.html
124+ |> List.singleton
125+ |> El.paragraph
126+ [ Font.size 16
127+ , El.width El.fill
128+ ]
129+ ]
130+
131+ else
132+ []
133+ )
134+ )
135+ )
136+ ]
137+
138+
139+providerIsSupported : Bridge.TwoFactorProviderType -> Bool
140+providerIsSupported provider =
141+ case provider of
142+ Bridge.Authenticator ->
143+ True
144+
145+ Bridge.Duo ->
146+ False
147+
148+ Bridge.Email ->
149+ True
150+
151+ Bridge.OrganizationDuo ->
152+ False
153+
154+ Bridge.Remember ->
155+ False
156+
157+ Bridge.U2f ->
158+ False
159+
160+ Bridge.WebAuthn ->
161+ False
162+
163+ Bridge.Yubikey ->
164+ False
frontend/Utils.elm+7
@@ -2,6 +2,7 @@ module Utils exposing (..)
22
33import Html exposing (..)
44import Html.Attributes as Attr
5+import Html.Events as Ev
56import Task
67
78
@@ -144,3 +145,9 @@ spanList f aa =
144145
145146 else
146147 ( [], aa )
148+
149+
150+iconButton : String -> msg -> Html msg
151+iconButton name msg =
152+ button [ Attr.class "p-button is-inline is-dense has-icon", Ev.onClick msg ]
153+ [ i [ Attr.class ("p-icon--" ++ name) ] [] ]
img/donate.pngnew file

Binary file not shown.

index.html+2−2
@@ -5,9 +5,9 @@
55 <meta charset="UTF-8">
66 <meta http-equiv="X-UA-Compatible" content="IE=edge">
77 <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
8− <link rel="stylesheet" href="vanilla/style.css">
8+ <link rel="stylesheet" href="vanilla/dist/style.css">
99 <link rel="stylesheet" href="style.css">
10− <title>Test.</title>
10+ <title>Warden</title>
1111 <script src="elm.js"></script>
1212</head>
1313
manifest.json+6−6
@@ -1,15 +1,15 @@
11{
22 "name": "soy.iko.warden",
3− "description": "A simple bitwarden client",
3+ "description": "A simple Bitwarden client",
44 "architecture": "@CLICK_ARCH@",
55 "title": "Warden",
66 "hooks": {
7− "appname": {
8− "apparmor": "appname.apparmor",
9− "desktop": "appname.desktop"
7+ "warden": {
8+ "apparmor": "warden.apparmor",
9+ "desktop": "warden.desktop"
1010 }
1111 },
12− "version": "1.0.0",
13− "maintainer": "Ilia Kostiuchenko <email@iko.soy>",
12+ "version": "1.2.4",
13+ "maintainer": "Ilia Kostiuchenko <mail@iko.soy>",
1414 "framework": "@CLICK_FRAMEWORK@"
1515}
package-lock.jsonnew file+959
@@ -0,0 +1,959 @@
1+{
2+ "name": "warden",
3+ "lockfileVersion": 3,
4+ "requires": true,
5+ "packages": {
6+ "": {
7+ "devDependencies": {
8+ "cssnano": "^5.1.15"
9+ }
10+ },
11+ "node_modules/@trysound/sax": {
12+ "version": "0.2.0",
13+ "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
14+ "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==",
15+ "dev": true,
16+ "engines": {
17+ "node": ">=10.13.0"
18+ }
19+ },
20+ "node_modules/boolbase": {
21+ "version": "1.0.0",
22+ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
23+ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
24+ "dev": true
25+ },
26+ "node_modules/browserslist": {
27+ "version": "4.21.5",
28+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz",
29+ "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==",
30+ "dev": true,
31+ "funding": [
32+ {
33+ "type": "opencollective",
34+ "url": "https://opencollective.com/browserslist"
35+ },
36+ {
37+ "type": "tidelift",
38+ "url": "https://tidelift.com/funding/github/npm/browserslist"
39+ }
40+ ],
41+ "dependencies": {
42+ "caniuse-lite": "^1.0.30001449",
43+ "electron-to-chromium": "^1.4.284",
44+ "node-releases": "^2.0.8",
45+ "update-browserslist-db": "^1.0.10"
46+ },
47+ "bin": {
48+ "browserslist": "cli.js"
49+ },
50+ "engines": {
51+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
52+ }
53+ },
54+ "node_modules/caniuse-api": {
55+ "version": "3.0.0",
56+ "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz",
57+ "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==",
58+ "dev": true,
59+ "dependencies": {
60+ "browserslist": "^4.0.0",
61+ "caniuse-lite": "^1.0.0",
62+ "lodash.memoize": "^4.1.2",
63+ "lodash.uniq": "^4.5.0"
64+ }
65+ },
66+ "node_modules/caniuse-lite": {
67+ "version": "1.0.30001457",
68+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001457.tgz",
69+ "integrity": "sha512-SDIV6bgE1aVbK6XyxdURbUE89zY7+k1BBBaOwYwkNCglXlel/E7mELiHC64HQ+W0xSKlqWhV9Wh7iHxUjMs4fA==",
70+ "dev": true,
71+ "funding": [
72+ {
73+ "type": "opencollective",
74+ "url": "https://opencollective.com/browserslist"
75+ },
76+ {
77+ "type": "tidelift",
78+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
79+ }
80+ ]
81+ },
82+ "node_modules/colord": {
83+ "version": "2.9.3",
84+ "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz",
85+ "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==",
86+ "dev": true
87+ },
88+ "node_modules/commander": {
89+ "version": "7.2.0",
90+ "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
91+ "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
92+ "dev": true,
93+ "engines": {
94+ "node": ">= 10"
95+ }
96+ },
97+ "node_modules/css-declaration-sorter": {
98+ "version": "6.3.1",
99+ "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.3.1.tgz",
100+ "integrity": "sha512-fBffmak0bPAnyqc/HO8C3n2sHrp9wcqQz6ES9koRF2/mLOVAx9zIQ3Y7R29sYCteTPqMCwns4WYQoCX91Xl3+w==",
101+ "dev": true,
102+ "engines": {
103+ "node": "^10 || ^12 || >=14"
104+ },
105+ "peerDependencies": {
106+ "postcss": "^8.0.9"
107+ }
108+ },
109+ "node_modules/css-select": {
110+ "version": "4.3.0",
111+ "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz",
112+ "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==",
113+ "dev": true,
114+ "dependencies": {
115+ "boolbase": "^1.0.0",
116+ "css-what": "^6.0.1",
117+ "domhandler": "^4.3.1",
118+ "domutils": "^2.8.0",
119+ "nth-check": "^2.0.1"
120+ },
121+ "funding": {
122+ "url": "https://github.com/sponsors/fb55"
123+ }
124+ },
125+ "node_modules/css-tree": {
126+ "version": "1.1.3",
127+ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz",
128+ "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==",
129+ "dev": true,
130+ "dependencies": {
131+ "mdn-data": "2.0.14",
132+ "source-map": "^0.6.1"
133+ },
134+ "engines": {
135+ "node": ">=8.0.0"
136+ }
137+ },
138+ "node_modules/css-what": {
139+ "version": "6.1.0",
140+ "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
141+ "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==",
142+ "dev": true,
143+ "engines": {
144+ "node": ">= 6"
145+ },
146+ "funding": {
147+ "url": "https://github.com/sponsors/fb55"
148+ }
149+ },
150+ "node_modules/cssesc": {
151+ "version": "3.0.0",
152+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
153+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
154+ "dev": true,
155+ "bin": {
156+ "cssesc": "bin/cssesc"
157+ },
158+ "engines": {
159+ "node": ">=4"
160+ }
161+ },
162+ "node_modules/cssnano": {
163+ "version": "5.1.15",
164+ "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.1.15.tgz",
165+ "integrity": "sha512-j+BKgDcLDQA+eDifLx0EO4XSA56b7uut3BQFH+wbSaSTuGLuiyTa/wbRYthUXX8LC9mLg+WWKe8h+qJuwTAbHw==",
166+ "dev": true,
167+ "dependencies": {
168+ "cssnano-preset-default": "^5.2.14",
169+ "lilconfig": "^2.0.3",
170+ "yaml": "^1.10.2"
171+ },
172+ "engines": {
173+ "node": "^10 || ^12 || >=14.0"
174+ },
175+ "funding": {
176+ "type": "opencollective",
177+ "url": "https://opencollective.com/cssnano"
178+ },
179+ "peerDependencies": {
180+ "postcss": "^8.2.15"
181+ }
182+ },
183+ "node_modules/cssnano-preset-default": {
184+ "version": "5.2.14",
185+ "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.2.14.tgz",
186+ "integrity": "sha512-t0SFesj/ZV2OTylqQVOrFgEh5uanxbO6ZAdeCrNsUQ6fVuXwYTxJPNAGvGTxHbD68ldIJNec7PyYZDBrfDQ+6A==",
187+ "dev": true,
188+ "dependencies": {
189+ "css-declaration-sorter": "^6.3.1",
190+ "cssnano-utils": "^3.1.0",
191+ "postcss-calc": "^8.2.3",
192+ "postcss-colormin": "^5.3.1",
193+ "postcss-convert-values": "^5.1.3",
194+ "postcss-discard-comments": "^5.1.2",
195+ "postcss-discard-duplicates": "^5.1.0",
196+ "postcss-discard-empty": "^5.1.1",
197+ "postcss-discard-overridden": "^5.1.0",
198+ "postcss-merge-longhand": "^5.1.7",
199+ "postcss-merge-rules": "^5.1.4",
200+ "postcss-minify-font-values": "^5.1.0",
201+ "postcss-minify-gradients": "^5.1.1",
202+ "postcss-minify-params": "^5.1.4",
203+ "postcss-minify-selectors": "^5.2.1",
204+ "postcss-normalize-charset": "^5.1.0",
205+ "postcss-normalize-display-values": "^5.1.0",
206+ "postcss-normalize-positions": "^5.1.1",
207+ "postcss-normalize-repeat-style": "^5.1.1",
208+ "postcss-normalize-string": "^5.1.0",
209+ "postcss-normalize-timing-functions": "^5.1.0",
210+ "postcss-normalize-unicode": "^5.1.1",
211+ "postcss-normalize-url": "^5.1.0",
212+ "postcss-normalize-whitespace": "^5.1.1",
213+ "postcss-ordered-values": "^5.1.3",
214+ "postcss-reduce-initial": "^5.1.2",
215+ "postcss-reduce-transforms": "^5.1.0",
216+ "postcss-svgo": "^5.1.0",
217+ "postcss-unique-selectors": "^5.1.1"
218+ },
219+ "engines": {
220+ "node": "^10 || ^12 || >=14.0"
221+ },
222+ "peerDependencies": {
223+ "postcss": "^8.2.15"
224+ }
225+ },
226+ "node_modules/cssnano-utils": {
227+ "version": "3.1.0",
228+ "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-3.1.0.tgz",
229+ "integrity": "sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==",
230+ "dev": true,
231+ "engines": {
232+ "node": "^10 || ^12 || >=14.0"
233+ },
234+ "peerDependencies": {
235+ "postcss": "^8.2.15"
236+ }
237+ },
238+ "node_modules/csso": {
239+ "version": "4.2.0",
240+ "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz",
241+ "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==",
242+ "dev": true,
243+ "dependencies": {
244+ "css-tree": "^1.1.2"
245+ },
246+ "engines": {
247+ "node": ">=8.0.0"
248+ }
249+ },
250+ "node_modules/dom-serializer": {
251+ "version": "1.4.1",
252+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz",
253+ "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==",
254+ "dev": true,
255+ "dependencies": {
256+ "domelementtype": "^2.0.1",
257+ "domhandler": "^4.2.0",
258+ "entities": "^2.0.0"
259+ },
260+ "funding": {
261+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
262+ }
263+ },
264+ "node_modules/domelementtype": {
265+ "version": "2.3.0",
266+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
267+ "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
268+ "dev": true,
269+ "funding": [
270+ {
271+ "type": "github",
272+ "url": "https://github.com/sponsors/fb55"
273+ }
274+ ]
275+ },
276+ "node_modules/domhandler": {
277+ "version": "4.3.1",
278+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz",
279+ "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==",
280+ "dev": true,
281+ "dependencies": {
282+ "domelementtype": "^2.2.0"
283+ },
284+ "engines": {
285+ "node": ">= 4"
286+ },
287+ "funding": {
288+ "url": "https://github.com/fb55/domhandler?sponsor=1"
289+ }
290+ },
291+ "node_modules/domutils": {
292+ "version": "2.8.0",
293+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz",
294+ "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==",
295+ "dev": true,
296+ "dependencies": {
297+ "dom-serializer": "^1.0.1",
298+ "domelementtype": "^2.2.0",
299+ "domhandler": "^4.2.0"
300+ },
301+ "funding": {
302+ "url": "https://github.com/fb55/domutils?sponsor=1"
303+ }
304+ },
305+ "node_modules/electron-to-chromium": {
306+ "version": "1.4.302",
307+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.302.tgz",
308+ "integrity": "sha512-Uk7C+7aPBryUR1Fwvk9VmipBcN9fVsqBO57jV2ZjTm+IZ6BMNqu7EDVEg2HxCNufk6QcWlFsBkhQyQroB2VWKw==",
309+ "dev": true
310+ },
311+ "node_modules/entities": {
312+ "version": "2.2.0",
313+ "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
314+ "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==",
315+ "dev": true,
316+ "funding": {
317+ "url": "https://github.com/fb55/entities?sponsor=1"
318+ }
319+ },
320+ "node_modules/escalade": {
321+ "version": "3.1.1",
322+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
323+ "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
324+ "dev": true,
325+ "engines": {
326+ "node": ">=6"
327+ }
328+ },
329+ "node_modules/lilconfig": {
330+ "version": "2.0.6",
331+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz",
332+ "integrity": "sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==",
333+ "dev": true,
334+ "engines": {
335+ "node": ">=10"
336+ }
337+ },
338+ "node_modules/lodash.memoize": {
339+ "version": "4.1.2",
340+ "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
341+ "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==",
342+ "dev": true
343+ },
344+ "node_modules/lodash.uniq": {
345+ "version": "4.5.0",
346+ "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
347+ "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==",
348+ "dev": true
349+ },
350+ "node_modules/mdn-data": {
351+ "version": "2.0.14",
352+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
353+ "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==",
354+ "dev": true
355+ },
356+ "node_modules/nanoid": {
357+ "version": "3.3.4",
358+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
359+ "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==",
360+ "dev": true,
361+ "peer": true,
362+ "bin": {
363+ "nanoid": "bin/nanoid.cjs"
364+ },
365+ "engines": {
366+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
367+ }
368+ },
369+ "node_modules/node-releases": {
370+ "version": "2.0.10",
371+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz",
372+ "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==",
373+ "dev": true
374+ },
375+ "node_modules/normalize-url": {
376+ "version": "6.1.0",
377+ "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
378+ "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==",
379+ "dev": true,
380+ "engines": {
381+ "node": ">=10"
382+ },
383+ "funding": {
384+ "url": "https://github.com/sponsors/sindresorhus"
385+ }
386+ },
387+ "node_modules/nth-check": {
388+ "version": "2.1.1",
389+ "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
390+ "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
391+ "dev": true,
392+ "dependencies": {
393+ "boolbase": "^1.0.0"
394+ },
395+ "funding": {
396+ "url": "https://github.com/fb55/nth-check?sponsor=1"
397+ }
398+ },
399+ "node_modules/picocolors": {
400+ "version": "1.0.0",
401+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
402+ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
403+ "dev": true
404+ },
405+ "node_modules/postcss": {
406+ "version": "8.4.21",
407+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz",
408+ "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==",
409+ "dev": true,
410+ "funding": [
411+ {
412+ "type": "opencollective",
413+ "url": "https://opencollective.com/postcss/"
414+ },
415+ {
416+ "type": "tidelift",
417+ "url": "https://tidelift.com/funding/github/npm/postcss"
418+ }
419+ ],
420+ "peer": true,
421+ "dependencies": {
422+ "nanoid": "^3.3.4",
423+ "picocolors": "^1.0.0",
424+ "source-map-js": "^1.0.2"
425+ },
426+ "engines": {
427+ "node": "^10 || ^12 || >=14"
428+ }
429+ },
430+ "node_modules/postcss-calc": {
431+ "version": "8.2.4",
432+ "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-8.2.4.tgz",
433+ "integrity": "sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==",
434+ "dev": true,
435+ "dependencies": {
436+ "postcss-selector-parser": "^6.0.9",
437+ "postcss-value-parser": "^4.2.0"
438+ },
439+ "peerDependencies": {
440+ "postcss": "^8.2.2"
441+ }
442+ },
443+ "node_modules/postcss-colormin": {
444+ "version": "5.3.1",
445+ "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-5.3.1.tgz",
446+ "integrity": "sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ==",
447+ "dev": true,
448+ "dependencies": {
449+ "browserslist": "^4.21.4",
450+ "caniuse-api": "^3.0.0",
451+ "colord": "^2.9.1",
452+ "postcss-value-parser": "^4.2.0"
453+ },
454+ "engines": {
455+ "node": "^10 || ^12 || >=14.0"
456+ },
457+ "peerDependencies": {
458+ "postcss": "^8.2.15"
459+ }
460+ },
461+ "node_modules/postcss-convert-values": {
462+ "version": "5.1.3",
463+ "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.1.3.tgz",
464+ "integrity": "sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA==",
465+ "dev": true,
466+ "dependencies": {
467+ "browserslist": "^4.21.4",
468+ "postcss-value-parser": "^4.2.0"
469+ },
470+ "engines": {
471+ "node": "^10 || ^12 || >=14.0"
472+ },
473+ "peerDependencies": {
474+ "postcss": "^8.2.15"
475+ }
476+ },
477+ "node_modules/postcss-discard-comments": {
478+ "version": "5.1.2",
479+ "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz",
480+ "integrity": "sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==",
481+ "dev": true,
482+ "engines": {
483+ "node": "^10 || ^12 || >=14.0"
484+ },
485+ "peerDependencies": {
486+ "postcss": "^8.2.15"
487+ }
488+ },
489+ "node_modules/postcss-discard-duplicates": {
490+ "version": "5.1.0",
491+ "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz",
492+ "integrity": "sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==",
493+ "dev": true,
494+ "engines": {
495+ "node": "^10 || ^12 || >=14.0"
496+ },
497+ "peerDependencies": {
498+ "postcss": "^8.2.15"
499+ }
500+ },
501+ "node_modules/postcss-discard-empty": {
502+ "version": "5.1.1",
503+ "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz",
504+ "integrity": "sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==",
505+ "dev": true,
506+ "engines": {
507+ "node": "^10 || ^12 || >=14.0"
508+ },
509+ "peerDependencies": {
510+ "postcss": "^8.2.15"
511+ }
512+ },
513+ "node_modules/postcss-discard-overridden": {
514+ "version": "5.1.0",
515+ "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz",
516+ "integrity": "sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==",
517+ "dev": true,
518+ "engines": {
519+ "node": "^10 || ^12 || >=14.0"
520+ },
521+ "peerDependencies": {
522+ "postcss": "^8.2.15"
523+ }
524+ },
525+ "node_modules/postcss-merge-longhand": {
526+ "version": "5.1.7",
527+ "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.1.7.tgz",
528+ "integrity": "sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==",
529+ "dev": true,
530+ "dependencies": {
531+ "postcss-value-parser": "^4.2.0",
532+ "stylehacks": "^5.1.1"
533+ },
534+ "engines": {
535+ "node": "^10 || ^12 || >=14.0"
536+ },
537+ "peerDependencies": {
538+ "postcss": "^8.2.15"
539+ }
540+ },
541+ "node_modules/postcss-merge-rules": {
542+ "version": "5.1.4",
543+ "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.1.4.tgz",
544+ "integrity": "sha512-0R2IuYpgU93y9lhVbO/OylTtKMVcHb67zjWIfCiKR9rWL3GUk1677LAqD/BcHizukdZEjT8Ru3oHRoAYoJy44g==",
545+ "dev": true,
546+ "dependencies": {
547+ "browserslist": "^4.21.4",
548+ "caniuse-api": "^3.0.0",
549+ "cssnano-utils": "^3.1.0",
550+ "postcss-selector-parser": "^6.0.5"
551+ },
552+ "engines": {
553+ "node": "^10 || ^12 || >=14.0"
554+ },
555+ "peerDependencies": {
556+ "postcss": "^8.2.15"
557+ }
558+ },
559+ "node_modules/postcss-minify-font-values": {
560+ "version": "5.1.0",
561+ "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-5.1.0.tgz",
562+ "integrity": "sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==",
563+ "dev": true,
564+ "dependencies": {
565+ "postcss-value-parser": "^4.2.0"
566+ },
567+ "engines": {
568+ "node": "^10 || ^12 || >=14.0"
569+ },
570+ "peerDependencies": {
571+ "postcss": "^8.2.15"
572+ }
573+ },
574+ "node_modules/postcss-minify-gradients": {
575+ "version": "5.1.1",
576+ "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-5.1.1.tgz",
577+ "integrity": "sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==",
578+ "dev": true,
579+ "dependencies": {
580+ "colord": "^2.9.1",
581+ "cssnano-utils": "^3.1.0",
582+ "postcss-value-parser": "^4.2.0"
583+ },
584+ "engines": {
585+ "node": "^10 || ^12 || >=14.0"
586+ },
587+ "peerDependencies": {
588+ "postcss": "^8.2.15"
589+ }
590+ },
591+ "node_modules/postcss-minify-params": {
592+ "version": "5.1.4",
593+ "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.1.4.tgz",
594+ "integrity": "sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw==",
595+ "dev": true,
596+ "dependencies": {
597+ "browserslist": "^4.21.4",
598+ "cssnano-utils": "^3.1.0",
599+ "postcss-value-parser": "^4.2.0"
600+ },
601+ "engines": {
602+ "node": "^10 || ^12 || >=14.0"
603+ },
604+ "peerDependencies": {
605+ "postcss": "^8.2.15"
606+ }
607+ },
608+ "node_modules/postcss-minify-selectors": {
609+ "version": "5.2.1",
610+ "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-5.2.1.tgz",
611+ "integrity": "sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==",
612+ "dev": true,
613+ "dependencies": {
614+ "postcss-selector-parser": "^6.0.5"
615+ },
616+ "engines": {
617+ "node": "^10 || ^12 || >=14.0"
618+ },
619+ "peerDependencies": {
620+ "postcss": "^8.2.15"
621+ }
622+ },
623+ "node_modules/postcss-normalize-charset": {
624+ "version": "5.1.0",
625+ "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz",
626+ "integrity": "sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==",
627+ "dev": true,
628+ "engines": {
629+ "node": "^10 || ^12 || >=14.0"
630+ },
631+ "peerDependencies": {
632+ "postcss": "^8.2.15"
633+ }
634+ },
635+ "node_modules/postcss-normalize-display-values": {
636+ "version": "5.1.0",
637+ "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-5.1.0.tgz",
638+ "integrity": "sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==",
639+ "dev": true,
640+ "dependencies": {
641+ "postcss-value-parser": "^4.2.0"
642+ },
643+ "engines": {
644+ "node": "^10 || ^12 || >=14.0"
645+ },
646+ "peerDependencies": {
647+ "postcss": "^8.2.15"
648+ }
649+ },
650+ "node_modules/postcss-normalize-positions": {
651+ "version": "5.1.1",
652+ "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-5.1.1.tgz",
653+ "integrity": "sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg==",
654+ "dev": true,
655+ "dependencies": {
656+ "postcss-value-parser": "^4.2.0"
657+ },
658+ "engines": {
659+ "node": "^10 || ^12 || >=14.0"
660+ },
661+ "peerDependencies": {
662+ "postcss": "^8.2.15"
663+ }
664+ },
665+ "node_modules/postcss-normalize-repeat-style": {
666+ "version": "5.1.1",
667+ "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.1.1.tgz",
668+ "integrity": "sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g==",
669+ "dev": true,
670+ "dependencies": {
671+ "postcss-value-parser": "^4.2.0"
672+ },
673+ "engines": {
674+ "node": "^10 || ^12 || >=14.0"
675+ },
676+ "peerDependencies": {
677+ "postcss": "^8.2.15"
678+ }
679+ },
680+ "node_modules/postcss-normalize-string": {
681+ "version": "5.1.0",
682+ "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-5.1.0.tgz",
683+ "integrity": "sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==",
684+ "dev": true,
685+ "dependencies": {
686+ "postcss-value-parser": "^4.2.0"
687+ },
688+ "engines": {
689+ "node": "^10 || ^12 || >=14.0"
690+ },
691+ "peerDependencies": {
692+ "postcss": "^8.2.15"
693+ }
694+ },
695+ "node_modules/postcss-normalize-timing-functions": {
696+ "version": "5.1.0",
697+ "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.1.0.tgz",
698+ "integrity": "sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==",
699+ "dev": true,
700+ "dependencies": {
701+ "postcss-value-parser": "^4.2.0"
702+ },
703+ "engines": {
704+ "node": "^10 || ^12 || >=14.0"
705+ },
706+ "peerDependencies": {
707+ "postcss": "^8.2.15"
708+ }
709+ },
710+ "node_modules/postcss-normalize-unicode": {
711+ "version": "5.1.1",
712+ "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.1.tgz",
713+ "integrity": "sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA==",
714+ "dev": true,
715+ "dependencies": {
716+ "browserslist": "^4.21.4",
717+ "postcss-value-parser": "^4.2.0"
718+ },
719+ "engines": {
720+ "node": "^10 || ^12 || >=14.0"
721+ },
722+ "peerDependencies": {
723+ "postcss": "^8.2.15"
724+ }
725+ },
726+ "node_modules/postcss-normalize-url": {
727+ "version": "5.1.0",
728+ "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-5.1.0.tgz",
729+ "integrity": "sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==",
730+ "dev": true,
731+ "dependencies": {
732+ "normalize-url": "^6.0.1",
733+ "postcss-value-parser": "^4.2.0"
734+ },
735+ "engines": {
736+ "node": "^10 || ^12 || >=14.0"
737+ },
738+ "peerDependencies": {
739+ "postcss": "^8.2.15"
740+ }
741+ },
742+ "node_modules/postcss-normalize-whitespace": {
743+ "version": "5.1.1",
744+ "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.1.1.tgz",
745+ "integrity": "sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==",
746+ "dev": true,
747+ "dependencies": {
748+ "postcss-value-parser": "^4.2.0"
749+ },
750+ "engines": {
751+ "node": "^10 || ^12 || >=14.0"
752+ },
753+ "peerDependencies": {
754+ "postcss": "^8.2.15"
755+ }
756+ },
757+ "node_modules/postcss-ordered-values": {
758+ "version": "5.1.3",
759+ "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-5.1.3.tgz",
760+ "integrity": "sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ==",
761+ "dev": true,
762+ "dependencies": {
763+ "cssnano-utils": "^3.1.0",
764+ "postcss-value-parser": "^4.2.0"
765+ },
766+ "engines": {
767+ "node": "^10 || ^12 || >=14.0"
768+ },
769+ "peerDependencies": {
770+ "postcss": "^8.2.15"
771+ }
772+ },
773+ "node_modules/postcss-reduce-initial": {
774+ "version": "5.1.2",
775+ "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.1.2.tgz",
776+ "integrity": "sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg==",
777+ "dev": true,
778+ "dependencies": {
779+ "browserslist": "^4.21.4",
780+ "caniuse-api": "^3.0.0"
781+ },
782+ "engines": {
783+ "node": "^10 || ^12 || >=14.0"
784+ },
785+ "peerDependencies": {
786+ "postcss": "^8.2.15"
787+ }
788+ },
789+ "node_modules/postcss-reduce-transforms": {
790+ "version": "5.1.0",
791+ "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-5.1.0.tgz",
792+ "integrity": "sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==",
793+ "dev": true,
794+ "dependencies": {
795+ "postcss-value-parser": "^4.2.0"
796+ },
797+ "engines": {
798+ "node": "^10 || ^12 || >=14.0"
799+ },
800+ "peerDependencies": {
801+ "postcss": "^8.2.15"
802+ }
803+ },
804+ "node_modules/postcss-selector-parser": {
805+ "version": "6.0.11",
806+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz",
807+ "integrity": "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==",
808+ "dev": true,
809+ "dependencies": {
810+ "cssesc": "^3.0.0",
811+ "util-deprecate": "^1.0.2"
812+ },
813+ "engines": {
814+ "node": ">=4"
815+ }
816+ },
817+ "node_modules/postcss-svgo": {
818+ "version": "5.1.0",
819+ "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-5.1.0.tgz",
820+ "integrity": "sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==",
821+ "dev": true,
822+ "dependencies": {
823+ "postcss-value-parser": "^4.2.0",
824+ "svgo": "^2.7.0"
825+ },
826+ "engines": {
827+ "node": "^10 || ^12 || >=14.0"
828+ },
829+ "peerDependencies": {
830+ "postcss": "^8.2.15"
831+ }
832+ },
833+ "node_modules/postcss-unique-selectors": {
834+ "version": "5.1.1",
835+ "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-5.1.1.tgz",
836+ "integrity": "sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==",
837+ "dev": true,
838+ "dependencies": {
839+ "postcss-selector-parser": "^6.0.5"
840+ },
841+ "engines": {
842+ "node": "^10 || ^12 || >=14.0"
843+ },
844+ "peerDependencies": {
845+ "postcss": "^8.2.15"
846+ }
847+ },
848+ "node_modules/postcss-value-parser": {
849+ "version": "4.2.0",
850+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
851+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
852+ "dev": true
853+ },
854+ "node_modules/source-map": {
855+ "version": "0.6.1",
856+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
857+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
858+ "dev": true,
859+ "engines": {
860+ "node": ">=0.10.0"
861+ }
862+ },
863+ "node_modules/source-map-js": {
864+ "version": "1.0.2",
865+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
866+ "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
867+ "dev": true,
868+ "peer": true,
869+ "engines": {
870+ "node": ">=0.10.0"
871+ }
872+ },
873+ "node_modules/stable": {
874+ "version": "0.1.8",
875+ "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz",
876+ "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==",
877+ "deprecated": "Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility",
878+ "dev": true
879+ },
880+ "node_modules/stylehacks": {
881+ "version": "5.1.1",
882+ "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz",
883+ "integrity": "sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw==",
884+ "dev": true,
885+ "dependencies": {
886+ "browserslist": "^4.21.4",
887+ "postcss-selector-parser": "^6.0.4"
888+ },
889+ "engines": {
890+ "node": "^10 || ^12 || >=14.0"
891+ },
892+ "peerDependencies": {
893+ "postcss": "^8.2.15"
894+ }
895+ },
896+ "node_modules/svgo": {
897+ "version": "2.8.0",
898+ "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz",
899+ "integrity": "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==",
900+ "dev": true,
901+ "dependencies": {
902+ "@trysound/sax": "0.2.0",
903+ "commander": "^7.2.0",
904+ "css-select": "^4.1.3",
905+ "css-tree": "^1.1.3",
906+ "csso": "^4.2.0",
907+ "picocolors": "^1.0.0",
908+ "stable": "^0.1.8"
909+ },
910+ "bin": {
911+ "svgo": "bin/svgo"
912+ },
913+ "engines": {
914+ "node": ">=10.13.0"
915+ }
916+ },
917+ "node_modules/update-browserslist-db": {
918+ "version": "1.0.10",
919+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz",
920+ "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==",
921+ "dev": true,
922+ "funding": [
923+ {
924+ "type": "opencollective",
925+ "url": "https://opencollective.com/browserslist"
926+ },
927+ {
928+ "type": "tidelift",
929+ "url": "https://tidelift.com/funding/github/npm/browserslist"
930+ }
931+ ],
932+ "dependencies": {
933+ "escalade": "^3.1.1",
934+ "picocolors": "^1.0.0"
935+ },
936+ "bin": {
937+ "browserslist-lint": "cli.js"
938+ },
939+ "peerDependencies": {
940+ "browserslist": ">= 4.21.0"
941+ }
942+ },
943+ "node_modules/util-deprecate": {
944+ "version": "1.0.2",
945+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
946+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
947+ "dev": true
948+ },
949+ "node_modules/yaml": {
950+ "version": "1.10.2",
951+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
952+ "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
953+ "dev": true,
954+ "engines": {
955+ "node": ">= 6"
956+ }
957+ }
958+ }
959+}
packages.dhall+6−6
@@ -99,8 +99,8 @@ in upstream
9999-------------------------------
100100-}
101101let upstream =
102− https://github.com/purescript/package-sets/releases/download/psc-0.15.2-20220706/packages.dhall
103− sha256:7a24ebdbacb2bfa27b2fc6ce3da96f048093d64e54369965a2a7b5d9892b6031
102+ https://github.com/purescript/package-sets/releases/download/psc-0.15.4-20220808/packages.dhall
103+ sha256:60eee64b04ca0013fae3e02a69fc3b176105c6baa2f31865c67cd5f881a412fd
104104
105105in upstream
106106 with argonaut-aeson-generic =
@@ -125,7 +125,7 @@ in upstream
125125 ]
126126 , repo =
127127 "https://github.com/ilyakooo0/purescript-argonaut-aeson-generic.git"
128− , version = "purescript-0.14"
128+ , version = "3e388500811ef4b6718fc776789f3be050ad7b57"
129129 }
130130 with untagged-union =
131131 { dependencies =
@@ -142,12 +142,12 @@ in upstream
142142 , "tuples"
143143 , "unsafe-coerce"
144144 ]
145− , repo = "https://github.com/ilyakooo0/purescript-untagged-union.git"
146− , version = "iko/ps-15"
145+ , repo = "https://github.com/rowtype-yoga/purescript-untagged-union.git"
146+ , version = "6c5bccef38696a16e86ba9a65cf778b4462c4a66"
147147 }
148148 with literals =
149149 { repo = "https://github.com/ilyakooo0/purescript-literals.git"
150− , version = "iko/_null"
150+ , version = "6875fb28026595cfb780318305a77e79b098bb01"
151151 , dependencies =
152152 [ "assert"
153153 , "effect"
registry.datnew file

Binary file not shown.

scratchpad/password_generator.html+16−1
@@ -5,7 +5,7 @@
55 <meta charset="UTF-8">
66 <meta http-equiv="X-UA-Compatible" content="IE=edge">
77 <meta name="viewport" content="width=device-width, initial-scale=1.0">
8− <link rel="stylesheet" href="../vanilla/style.css">
8+ <link rel="stylesheet" href="../vanilla/dist/style.css">
99 <link rel="stylesheet" href="../style.css">
1010 <title>Password generator</title>
1111
@@ -20,6 +20,14 @@
2020
2121<body>
2222
23+ <div class="select-second-factor-container">
24+ <button class="p-button">Hello</button>
25+ <button class="p-button">Bar</button>
26+ <button class="p-button">Bar</button>
27+ <button class="p-button">Bardi</button>
28+ <button class="p-button">Wolrd</button>
29+ </div>
30+
2331 <div class="password-generator-group">
2432 <p>Password length</p>
2533 <input type="number" class="p-slider__input">
@@ -35,6 +43,13 @@
3543 <input type="number" class="p-slider__input">
3644 </div>
3745
46+ <form action="">
47+ <label><input type="checkbox" name="" id="">Hahahah</label>
48+ <input type="checkbox">
49+
50+ </form>
51+
52+
3853
3954 <input type="range" min="0" max="100" value="50" step="1">
4055 <input type="range" min="0" max="100" value="50" step="1"
screenshots/1.pngnew file

Binary file not shown.

screenshots/2.pngnew file

Binary file not shown.

screenshots/3.pngnew file

Binary file not shown.

screenshots/4.pngnew file

Binary file not shown.

screenshots/5.pngnew file

Binary file not shown.

spago-packages.nixnew file+1102
@@ -0,0 +1,1102 @@
1+# This file was generated by Spago2Nix
2+
3+{ pkgs ? import <nixpkgs> {} }:
4+
5+let
6+ inputs = {
7+
8+ "aff" = pkgs.stdenv.mkDerivation {
9+ name = "aff";
10+ version = "v7.0.0";
11+ src = pkgs.fetchgit {
12+ url = "https://github.com/purescript-contrib/purescript-aff.git";
13+ rev = "2d44d9f9d0d6a416a4103fba2fb39e5160f80e36";
14+ sha256 = "1d38l16s8hvfqq5w40w21j5089svdiggkraw3rwrl6ml7lijjqx4";
15+ };
16+ phases = "installPhase";
17+ installPhase = "ln -s $src $out";
18+ };
19+
20+ "aff-promise" = pkgs.stdenv.mkDerivation {
21+ name = "aff-promise";
22+ version = "v4.0.0";
23+ src = pkgs.fetchgit {
24+ url = "https://github.com/nwolverson/purescript-aff-promise.git";
25+ rev = "3aa74e68e3e4c3e38d821375703e0b2f49d831eb";
26+ sha256 = "10gv3b05dxnlmr9zmigqs1hz4xqrx7da30aqh12kqg3zrg49k9cc";
27+ };
28+ phases = "installPhase";
29+ installPhase = "ln -s $src $out";
30+ };
31+
32+ "argonaut" = pkgs.stdenv.mkDerivation {
33+ name = "argonaut";
34+ version = "v9.0.0";
35+ src = pkgs.fetchgit {
36+ url = "https://github.com/purescript-contrib/purescript-argonaut.git";
37+ rev = "7505a47f2edb0c9cacaac4f11dcedf4723a3e9c8";
38+ sha256 = "18yxhlrwri6q858krz1klyq29fx8nvfm16c04wm2rn91mgyasn6x";
39+ };
40+ phases = "installPhase";
41+ installPhase = "ln -s $src $out";
42+ };
43+
44+ "argonaut-aeson-generic" = pkgs.stdenv.mkDerivation {
45+ name = "argonaut-aeson-generic";
46+ version = "3e388500811ef4b6718fc776789f3be050ad7b57";
47+ src = pkgs.fetchgit {
48+ url = "https://github.com/ilyakooo0/purescript-argonaut-aeson-generic.git";
49+ rev = "3e388500811ef4b6718fc776789f3be050ad7b57";
50+ sha256 = "0yhcxxz5lpgwxxsx93kmr884rk5nrvgy7lmpaixazqps2a2q0fhc";
51+ };
52+ phases = "installPhase";
53+ installPhase = "ln -s $src $out";
54+ };
55+
56+ "argonaut-codecs" = pkgs.stdenv.mkDerivation {
57+ name = "argonaut-codecs";
58+ version = "v9.0.0";
59+ src = pkgs.fetchgit {
60+ url = "https://github.com/purescript-contrib/purescript-argonaut-codecs.git";
61+ rev = "176a5dddae28fda9a2ff31ed4bf1efcc148513a4";
62+ sha256 = "08ys445222syysxh24la8bc8l20lwwqi4vdfa3ph2a60l1lx3kp8";
63+ };
64+ phases = "installPhase";
65+ installPhase = "ln -s $src $out";
66+ };
67+
68+ "argonaut-core" = pkgs.stdenv.mkDerivation {
69+ name = "argonaut-core";
70+ version = "v7.0.0";
71+ src = pkgs.fetchgit {
72+ url = "https://github.com/purescript-contrib/purescript-argonaut-core.git";
73+ rev = "68da81dd80ec36d3b013eff46dc067a972c22e5d";
74+ sha256 = "0hxl17ddbflkk0hchjgk5xj6j9fwp3b182w4g073p0dwscdl4f08";
75+ };
76+ phases = "installPhase";
77+ installPhase = "ln -s $src $out";
78+ };
79+
80+ "argonaut-generic" = pkgs.stdenv.mkDerivation {
81+ name = "argonaut-generic";
82+ version = "v8.0.0";
83+ src = pkgs.fetchgit {
84+ url = "https://github.com/purescript-contrib/purescript-argonaut-generic.git";
85+ rev = "2df4080f036762df91a24b22842e389395ef0bdd";
86+ sha256 = "0f3bkmv37nk89vscvivdr971sjydqiw7lwk9hfdfms05id8jdjkw";
87+ };
88+ phases = "installPhase";
89+ installPhase = "ln -s $src $out";
90+ };
91+
92+ "argonaut-traversals" = pkgs.stdenv.mkDerivation {
93+ name = "argonaut-traversals";
94+ version = "v10.0.0";
95+ src = pkgs.fetchgit {
96+ url = "https://github.com/purescript-contrib/purescript-argonaut-traversals.git";
97+ rev = "8d2403d8d57afb568933dbb36063d5670ce770a0";
98+ sha256 = "0v965bcl1hdp1hacbzxcm35kxxclixj0kpsdm816gs7m548z6l43";
99+ };
100+ phases = "installPhase";
101+ installPhase = "ln -s $src $out";
102+ };
103+
104+ "arraybuffer" = pkgs.stdenv.mkDerivation {
105+ name = "arraybuffer";
106+ version = "v13.0.0";
107+ src = pkgs.fetchgit {
108+ url = "https://github.com/purescript-contrib/purescript-arraybuffer.git";
109+ rev = "3b5ba3040f060057e864805546a50f319853517d";
110+ sha256 = "0cfi15r95zd7ikvs8v3pf2i3rzxjqxk7209j96d8qs6jzqlypbwd";
111+ };
112+ phases = "installPhase";
113+ installPhase = "ln -s $src $out";
114+ };
115+
116+ "arraybuffer-types" = pkgs.stdenv.mkDerivation {
117+ name = "arraybuffer-types";
118+ version = "v3.0.2";
119+ src = pkgs.fetchgit {
120+ url = "https://github.com/purescript-contrib/purescript-arraybuffer-types.git";
121+ rev = "9b0b7a0f9ee034e039f3d3a2a9c3f74eb7c9264a";
122+ sha256 = "1q3111jk6jj64bxy86lc7ik6q5mgdz87bc086wj5rm57dmlivlxx";
123+ };
124+ phases = "installPhase";
125+ installPhase = "ln -s $src $out";
126+ };
127+
128+ "arrays" = pkgs.stdenv.mkDerivation {
129+ name = "arrays";
130+ version = "v7.0.0";
131+ src = pkgs.fetchgit {
132+ url = "https://github.com/purescript/purescript-arrays.git";
133+ rev = "d20bae2f76db6cf29b7b75e26e82b8a54c32295e";
134+ sha256 = "1ciz796yrdfnqiskjjhh6r3bn9mks1dm5zin7kwzh1nfy1i4w9pz";
135+ };
136+ phases = "installPhase";
137+ installPhase = "ln -s $src $out";
138+ };
139+
140+ "assert" = pkgs.stdenv.mkDerivation {
141+ name = "assert";
142+ version = "v6.0.0";
143+ src = pkgs.fetchgit {
144+ url = "https://github.com/purescript/purescript-assert.git";
145+ rev = "27c0edb57d2ee497eb5fab664f5601c35b613eda";
146+ sha256 = "02qpcsijil64qq4kcikv2ynxl85zkryvv0vmzjm6752956ymzi0j";
147+ };
148+ phases = "installPhase";
149+ installPhase = "ln -s $src $out";
150+ };
151+
152+ "avar" = pkgs.stdenv.mkDerivation {
153+ name = "avar";
154+ version = "v5.0.0";
155+ src = pkgs.fetchgit {
156+ url = "https://github.com/purescript-contrib/purescript-avar.git";
157+ rev = "d00f5784d9cc8f079babd62740f5c52b87e5caa5";
158+ sha256 = "132g0ccmjr8328xpaycww8b5wmx1vi3li4irxxqcjk5wi47i8c9h";
159+ };
160+ phases = "installPhase";
161+ installPhase = "ln -s $src $out";
162+ };
163+
164+ "bifunctors" = pkgs.stdenv.mkDerivation {
165+ name = "bifunctors";
166+ version = "v6.0.0";
167+ src = pkgs.fetchgit {
168+ url = "https://github.com/purescript/purescript-bifunctors.git";
169+ rev = "16ba2fb6dd7f05528ebd9e2f9ca3a068b325e5b3";
170+ sha256 = "1qz5aaiq5w4g1rhjb63z47h6nqhw8hw23x1zw5alam047pq4vfkg";
171+ };
172+ phases = "installPhase";
173+ installPhase = "ln -s $src $out";
174+ };
175+
176+ "catenable-lists" = pkgs.stdenv.mkDerivation {
177+ name = "catenable-lists";
178+ version = "v7.0.0";
179+ src = pkgs.fetchgit {
180+ url = "https://github.com/purescript/purescript-catenable-lists.git";
181+ rev = "09abe1f4888bc00841ad2b59e56a9e7ce7ebd4ab";
182+ sha256 = "0fn6caspdnb0nrlj31sy7d5hxrhndfz7a2b9wfkxf5bv5vpjyawb";
183+ };
184+ phases = "installPhase";
185+ installPhase = "ln -s $src $out";
186+ };
187+
188+ "console" = pkgs.stdenv.mkDerivation {
189+ name = "console";
190+ version = "v6.0.0";
191+ src = pkgs.fetchgit {
192+ url = "https://github.com/purescript/purescript-console.git";
193+ rev = "3b83d7b792d03872afeea5e62b4f686ab0f09842";
194+ sha256 = "0fr5l1myhscp910mybp04cg6g8f2hy3ikjfc8fkqlb2dm2cqzdfs";
195+ };
196+ phases = "installPhase";
197+ installPhase = "ln -s $src $out";
198+ };
199+
200+ "const" = pkgs.stdenv.mkDerivation {
201+ name = "const";
202+ version = "v6.0.0";
203+ src = pkgs.fetchgit {
204+ url = "https://github.com/purescript/purescript-const.git";
205+ rev = "ab9570cf2b6e67f7e441178211db1231cfd75c37";
206+ sha256 = "0mcpwqqf5bcwxlzk53smvsjjz1ymlnq0ypphg6sc83ibw70g64f6";
207+ };
208+ phases = "installPhase";
209+ installPhase = "ln -s $src $out";
210+ };
211+
212+ "contravariant" = pkgs.stdenv.mkDerivation {
213+ name = "contravariant";
214+ version = "v6.0.0";
215+ src = pkgs.fetchgit {
216+ url = "https://github.com/purescript/purescript-contravariant.git";
217+ rev = "9ad3e105b8855bcc25f4e0893c784789d05a58de";
218+ sha256 = "1xkd3rfs8v20w7mj6sj6dmhfb2vb1zpmjv245xj1rahk3g16qm7m";
219+ };
220+ phases = "installPhase";
221+ installPhase = "ln -s $src $out";
222+ };
223+
224+ "control" = pkgs.stdenv.mkDerivation {
225+ name = "control";
226+ version = "v6.0.0";
227+ src = pkgs.fetchgit {
228+ url = "https://github.com/purescript/purescript-control.git";
229+ rev = "a6033808790879a17b2729e73747a9ed3fb2264e";
230+ sha256 = "05sdgywprwpav62d2bvlb80yd39brxhnmhhl1f116mm9hw5clqia";
231+ };
232+ phases = "installPhase";
233+ installPhase = "ln -s $src $out";
234+ };
235+
236+ "datetime" = pkgs.stdenv.mkDerivation {
237+ name = "datetime";
238+ version = "v6.0.0";
239+ src = pkgs.fetchgit {
240+ url = "https://github.com/purescript/purescript-datetime.git";
241+ rev = "a6a0cf1b0324964ad1854bc3377ed8766ba90e6f";
242+ sha256 = "1idjljxy8rjlw3wiypk1c9cc1fc4g9kf7drzdy6a0yjdg7504gad";
243+ };
244+ phases = "installPhase";
245+ installPhase = "ln -s $src $out";
246+ };
247+
248+ "datetime-parsing" = pkgs.stdenv.mkDerivation {
249+ name = "datetime-parsing";
250+ version = "v0.2.0";
251+ src = pkgs.fetchgit {
252+ url = "https://github.com/flounders/purescript-datetime-parsing.git";
253+ rev = "aa7d336c2681501dbe343adcbbf8f2ee51b23932";
254+ sha256 = "1w0lfs64qz1hs7zijyzpvqlp2z06wmbz4jp8cjsnw9yj46jsy8nh";
255+ };
256+ phases = "installPhase";
257+ installPhase = "ln -s $src $out";
258+ };
259+
260+ "distributive" = pkgs.stdenv.mkDerivation {
261+ name = "distributive";
262+ version = "v6.0.0";
263+ src = pkgs.fetchgit {
264+ url = "https://github.com/purescript/purescript-distributive.git";
265+ rev = "6005e513642e855ebf6f884d24a35c2803ca252a";
266+ sha256 = "0x6cfsx9ff66kvw9l10pxd5vv16r91mmfxrfc68w1xfdfi81lhdx";
267+ };
268+ phases = "installPhase";
269+ installPhase = "ln -s $src $out";
270+ };
271+
272+ "effect" = pkgs.stdenv.mkDerivation {
273+ name = "effect";
274+ version = "v4.0.0";
275+ src = pkgs.fetchgit {
276+ url = "https://github.com/purescript/purescript-effect.git";
277+ rev = "a192ddb923027d426d6ea3d8deb030c9aa7c7dda";
278+ sha256 = "0aa10lc6h9mlf4xf3g3ziig7v6kxdqvbh20kma8ay59w0b1bhmj1";
279+ };
280+ phases = "installPhase";
281+ installPhase = "ln -s $src $out";
282+ };
283+
284+ "either" = pkgs.stdenv.mkDerivation {
285+ name = "either";
286+ version = "v6.1.0";
287+ src = pkgs.fetchgit {
288+ url = "https://github.com/purescript/purescript-either.git";
289+ rev = "af655a04ed2fd694b6688af39ee20d7907ad0763";
290+ sha256 = "05zps4klvgmvlm06f4hrycssm8q0pysbqnjsrk26lfvmid6mmg63";
291+ };
292+ phases = "installPhase";
293+ installPhase = "ln -s $src $out";
294+ };
295+
296+ "enums" = pkgs.stdenv.mkDerivation {
297+ name = "enums";
298+ version = "v6.0.0";
299+ src = pkgs.fetchgit {
300+ url = "https://github.com/purescript/purescript-enums.git";
301+ rev = "bae47961d401f9acf88da38f32e87403e5f0cf2f";
302+ sha256 = "08sl5q07hv6v2008dcpg4mxiqwk9z0fhq67k0bgkxvlbsmar7khg";
303+ };
304+ phases = "installPhase";
305+ installPhase = "ln -s $src $out";
306+ };
307+
308+ "exceptions" = pkgs.stdenv.mkDerivation {
309+ name = "exceptions";
310+ version = "v6.0.0";
311+ src = pkgs.fetchgit {
312+ url = "https://github.com/purescript/purescript-exceptions.git";
313+ rev = "afab3c07c820bb49b6c5be50049db46a964a6161";
314+ sha256 = "04xrbrcjwsv8gj465ygrlqhv1majxmr6m242iazg6apxlr89hgvg";
315+ };
316+ phases = "installPhase";
317+ installPhase = "ln -s $src $out";
318+ };
319+
320+ "exists" = pkgs.stdenv.mkDerivation {
321+ name = "exists";
322+ version = "v6.0.0";
323+ src = pkgs.fetchgit {
324+ url = "https://github.com/purescript/purescript-exists.git";
325+ rev = "f765b4ace7869c27b9c05949e18c843881f9173b";
326+ sha256 = "0xnxi8fsmi15wrpq5d99gg94glk8a45hpx7pf4pmxg5r6rn1vjvx";
327+ };
328+ phases = "installPhase";
329+ installPhase = "ln -s $src $out";
330+ };
331+
332+ "float32" = pkgs.stdenv.mkDerivation {
333+ name = "float32";
334+ version = "v2.0.0";
335+ src = pkgs.fetchgit {
336+ url = "https://github.com/purescript-contrib/purescript-float32.git";
337+ rev = "3b70984d823c12c1b84a45a9b3d45199a4fcc67d";
338+ sha256 = "0a1lvga6bbhbb8g3crp3fr0kmpkzjmffhfq71ig6flin13xijgys";
339+ };
340+ phases = "installPhase";
341+ installPhase = "ln -s $src $out";
342+ };
343+
344+ "foldable-traversable" = pkgs.stdenv.mkDerivation {
345+ name = "foldable-traversable";
346+ version = "v6.0.0";
347+ src = pkgs.fetchgit {
348+ url = "https://github.com/purescript/purescript-foldable-traversable.git";
349+ rev = "b3926f870532d287ea59e2d5cd3873b81ef2a93a";
350+ sha256 = "0xg8qvyc6r9wqgy7wnw1rjqljl4wpgdrkxsm5x9rsagj2k5brxip";
351+ };
352+ phases = "installPhase";
353+ installPhase = "ln -s $src $out";
354+ };
355+
356+ "foreign" = pkgs.stdenv.mkDerivation {
357+ name = "foreign";
358+ version = "v7.0.0";
359+ src = pkgs.fetchgit {
360+ url = "https://github.com/purescript/purescript-foreign.git";
361+ rev = "2dd222d1ec7363fa0a0a7adb0d8eaf81bb7006dd";
362+ sha256 = "0ycjaal8b2rsg3zl3b7acv166vjfdvziiza3fs8nhfw0rx0xmm1m";
363+ };
364+ phases = "installPhase";
365+ installPhase = "ln -s $src $out";
366+ };
367+
368+ "foreign-object" = pkgs.stdenv.mkDerivation {
369+ name = "foreign-object";
370+ version = "v4.0.0";
371+ src = pkgs.fetchgit {
372+ url = "https://github.com/purescript/purescript-foreign-object.git";
373+ rev = "28a635827a9a6c251df73f68874070d51fe9f756";
374+ sha256 = "0ql0vgndcb5dqaqz81mvrqkrl5c2rlgxpydgdiiaxmmfxvxwi439";
375+ };
376+ phases = "installPhase";
377+ installPhase = "ln -s $src $out";
378+ };
379+
380+ "free" = pkgs.stdenv.mkDerivation {
381+ name = "free";
382+ version = "v7.0.0";
383+ src = pkgs.fetchgit {
384+ url = "https://github.com/purescript/purescript-free.git";
385+ rev = "e2d8fa8023a864363857834e11393483bced5e38";
386+ sha256 = "0gyy0k297m26gw76w7kx8k1zjczlqbqb3nmh2h4gb3r84gl05g6r";
387+ };
388+ phases = "installPhase";
389+ installPhase = "ln -s $src $out";
390+ };
391+
392+ "functions" = pkgs.stdenv.mkDerivation {
393+ name = "functions";
394+ version = "v6.0.0";
395+ src = pkgs.fetchgit {
396+ url = "https://github.com/purescript/purescript-functions.git";
397+ rev = "f626f20580483977c5b27a01aac6471e28aff367";
398+ sha256 = "1bsf2y8hx103a92yrc6m05q2dv51ckag1srd7q5n9vaf2k9byrj9";
399+ };
400+ phases = "installPhase";
401+ installPhase = "ln -s $src $out";
402+ };
403+
404+ "functors" = pkgs.stdenv.mkDerivation {
405+ name = "functors";
406+ version = "v5.0.0";
407+ src = pkgs.fetchgit {
408+ url = "https://github.com/purescript/purescript-functors.git";
409+ rev = "022ffd7a2a7ec12080314f3d217b400674a247b4";
410+ sha256 = "1pnw3r1nk0rx8mrw3ajjml40zvx9v26qrqwj77pz7hpwxigxfbff";
411+ };
412+ phases = "installPhase";
413+ installPhase = "ln -s $src $out";
414+ };
415+
416+ "gen" = pkgs.stdenv.mkDerivation {
417+ name = "gen";
418+ version = "v4.0.0";
419+ src = pkgs.fetchgit {
420+ url = "https://github.com/purescript/purescript-gen.git";
421+ rev = "9fbcc2a1261c32e30d79c5418edef4d96fe76931";
422+ sha256 = "0kajzcp9a8dmqvb3ga27hlf18m2smbp98a0bmzzb9vv156ckw0b4";
423+ };
424+ phases = "installPhase";
425+ installPhase = "ln -s $src $out";
426+ };
427+
428+ "identity" = pkgs.stdenv.mkDerivation {
429+ name = "identity";
430+ version = "v6.0.0";
431+ src = pkgs.fetchgit {
432+ url = "https://github.com/purescript/purescript-identity.git";
433+ rev = "ef6768f8a52ab0bc943a85f5761ba07c257f639f";
434+ sha256 = "1lh0pkwwdyd8q9bgf4l1m96gdnardki4r2s359cnhrwp94b43fyr";
435+ };
436+ phases = "installPhase";
437+ installPhase = "ln -s $src $out";
438+ };
439+
440+ "integers" = pkgs.stdenv.mkDerivation {
441+ name = "integers";
442+ version = "v6.0.0";
443+ src = pkgs.fetchgit {
444+ url = "https://github.com/purescript/purescript-integers.git";
445+ rev = "54d712b25c594833083d15dc9ff2418eb9c52822";
446+ sha256 = "04j9sqvkgyxxvbm8jfc2118wzmgswn8jva9fysgbsnmsg9vcxfm6";
447+ };
448+ phases = "installPhase";
449+ installPhase = "ln -s $src $out";
450+ };
451+
452+ "invariant" = pkgs.stdenv.mkDerivation {
453+ name = "invariant";
454+ version = "v6.0.0";
455+ src = pkgs.fetchgit {
456+ url = "https://github.com/purescript/purescript-invariant.git";
457+ rev = "1d2a196d51e90623adb88496c2cfd759c6736894";
458+ sha256 = "07rqag47ykdiqc5yfbc1a64ijaqliljhcw1vgbv81m7ljzzd2xna";
459+ };
460+ phases = "installPhase";
461+ installPhase = "ln -s $src $out";
462+ };
463+
464+ "js-date" = pkgs.stdenv.mkDerivation {
465+ name = "js-date";
466+ version = "v8.0.0";
467+ src = pkgs.fetchgit {
468+ url = "https://github.com/purescript-contrib/purescript-js-date.git";
469+ rev = "1ea020316946cc4b87195bca9c54d0c16abaa490";
470+ sha256 = "03535g8hc4bzyzig25hf75dls9ypsgrdk2nc45h8zw4xyy4ha0gz";
471+ };
472+ phases = "installPhase";
473+ installPhase = "ln -s $src $out";
474+ };
475+
476+ "js-timers" = pkgs.stdenv.mkDerivation {
477+ name = "js-timers";
478+ version = "v6.1.0";
479+ src = pkgs.fetchgit {
480+ url = "https://github.com/purescript-contrib/purescript-js-timers.git";
481+ rev = "7cb728b3e6dc29f355143617e6e9ac770ecd9273";
482+ sha256 = "0dm933kqhwgmpkylgrlpjplidn8biic8jlzs4l4nny4a1z20mr61";
483+ };
484+ phases = "installPhase";
485+ installPhase = "ln -s $src $out";
486+ };
487+
488+ "lazy" = pkgs.stdenv.mkDerivation {
489+ name = "lazy";
490+ version = "v6.0.0";
491+ src = pkgs.fetchgit {
492+ url = "https://github.com/purescript/purescript-lazy.git";
493+ rev = "48347841226b27af5205a1a8ec71e27a93ce86fd";
494+ sha256 = "0dxlc3b2bdqsri45i7a9bvwvv0gg43cc1gq61zi41f290a29ny2q";
495+ };
496+ phases = "installPhase";
497+ installPhase = "ln -s $src $out";
498+ };
499+
500+ "lcg" = pkgs.stdenv.mkDerivation {
501+ name = "lcg";
502+ version = "v4.0.0";
503+ src = pkgs.fetchgit {
504+ url = "https://github.com/purescript/purescript-lcg.git";
505+ rev = "67c6c6483a563a59ae036d9dca0f1be2835326a5";
506+ sha256 = "1shzn6zvc1cxd7v0bvfsk7x3xf59vxby8c5lfjvd746r6396zn65";
507+ };
508+ phases = "installPhase";
509+ installPhase = "ln -s $src $out";
510+ };
511+
512+ "lists" = pkgs.stdenv.mkDerivation {
513+ name = "lists";
514+ version = "v7.0.0";
515+ src = pkgs.fetchgit {
516+ url = "https://github.com/purescript/purescript-lists.git";
517+ rev = "b113451e5b41cad87d669a3165f955c71cd863e2";
518+ sha256 = "1mg7vy44k8jlqkri7x4ikciixx92b17wi35887x09dwdsicjf0sx";
519+ };
520+ phases = "installPhase";
521+ installPhase = "ln -s $src $out";
522+ };
523+
524+ "literals" = pkgs.stdenv.mkDerivation {
525+ name = "literals";
526+ version = "6875fb28026595cfb780318305a77e79b098bb01";
527+ src = pkgs.fetchgit {
528+ url = "https://github.com/ilyakooo0/purescript-literals.git";
529+ rev = "6875fb28026595cfb780318305a77e79b098bb01";
530+ sha256 = "0s45cdgpdhni3rj7irr8pch46yxwmdcmj2nrg1vpkvygydkikdmb";
531+ };
532+ phases = "installPhase";
533+ installPhase = "ln -s $src $out";
534+ };
535+
536+ "maybe" = pkgs.stdenv.mkDerivation {
537+ name = "maybe";
538+ version = "v6.0.0";
539+ src = pkgs.fetchgit {
540+ url = "https://github.com/purescript/purescript-maybe.git";
541+ rev = "c6f98ac1088766287106c5d9c8e30e7648d36786";
542+ sha256 = "0n33g8c579vcs06ii0r7f14jjdwzjw7p583w4nmrv9h442q2cvlz";
543+ };
544+ phases = "installPhase";
545+ installPhase = "ln -s $src $out";
546+ };
547+
548+ "media-types" = pkgs.stdenv.mkDerivation {
549+ name = "media-types";
550+ version = "v6.0.0";
551+ src = pkgs.fetchgit {
552+ url = "https://github.com/purescript-contrib/purescript-media-types.git";
553+ rev = "af853de226592f319a953637069a943dd261cba3";
554+ sha256 = "0bxpsd2yw3zg58ay649v3xld7n17n7j7n9y8l841cjih9bc7ccxx";
555+ };
556+ phases = "installPhase";
557+ installPhase = "ln -s $src $out";
558+ };
559+
560+ "newtype" = pkgs.stdenv.mkDerivation {
561+ name = "newtype";
562+ version = "v5.0.0";
563+ src = pkgs.fetchgit {
564+ url = "https://github.com/purescript/purescript-newtype.git";
565+ rev = "29d8e6dd77aec2c975c948364ec3faf26e14ee7b";
566+ sha256 = "0kfc644zn2f9rpx8fy4gjvmz04k9mv1c2xrqis8m3s0ydnhbffd7";
567+ };
568+ phases = "installPhase";
569+ installPhase = "ln -s $src $out";
570+ };
571+
572+ "nonempty" = pkgs.stdenv.mkDerivation {
573+ name = "nonempty";
574+ version = "v7.0.0";
575+ src = pkgs.fetchgit {
576+ url = "https://github.com/purescript/purescript-nonempty.git";
577+ rev = "28150ecc7419238b187abd609a92a645273348bb";
578+ sha256 = "1mwdc2sny2ygp67kyk7h2493vq7syf7j18s3d9a5gwf1y7jlf7gh";
579+ };
580+ phases = "installPhase";
581+ installPhase = "ln -s $src $out";
582+ };
583+
584+ "now" = pkgs.stdenv.mkDerivation {
585+ name = "now";
586+ version = "v6.0.0";
587+ src = pkgs.fetchgit {
588+ url = "https://github.com/purescript-contrib/purescript-now.git";
589+ rev = "b5ffed2381e5fefc063f484e607e8499e79eaf32";
590+ sha256 = "01khmqs4iz7dhlvd43zxj19q0c0vvqf1m128vgvgxz5mv430zfgr";
591+ };
592+ phases = "installPhase";
593+ installPhase = "ln -s $src $out";
594+ };
595+
596+ "nullable" = pkgs.stdenv.mkDerivation {
597+ name = "nullable";
598+ version = "v6.0.0";
599+ src = pkgs.fetchgit {
600+ url = "https://github.com/purescript-contrib/purescript-nullable.git";
601+ rev = "3202744c6c65e8d1fbba7f4256a1c482078e7fb5";
602+ sha256 = "10s3b54pjmr2rxlyzvc8yc99k1dpn0qrijnc4y65yj5z6bxf2791";
603+ };
604+ phases = "installPhase";
605+ installPhase = "ln -s $src $out";
606+ };
607+
608+ "numbers" = pkgs.stdenv.mkDerivation {
609+ name = "numbers";
610+ version = "v9.0.0";
611+ src = pkgs.fetchgit {
612+ url = "https://github.com/purescript/purescript-numbers.git";
613+ rev = "2a53528f18f9415334bae28e7bb3cf3be86342c2";
614+ sha256 = "1h4v6ir1hq6gvkk0kla38vlcd6bv73xkc8pv71jygwqxrdwfxxgw";
615+ };
616+ phases = "installPhase";
617+ installPhase = "ln -s $src $out";
618+ };
619+
620+ "ordered-collections" = pkgs.stdenv.mkDerivation {
621+ name = "ordered-collections";
622+ version = "v3.0.0";
623+ src = pkgs.fetchgit {
624+ url = "https://github.com/purescript/purescript-ordered-collections.git";
625+ rev = "9826b7632d0d0a691173bde308a634195f42a419";
626+ sha256 = "1wk8mcn1zsxi0yk9ybas2v7m0drb492806gld1xxxic8x1i19ws0";
627+ };
628+ phases = "installPhase";
629+ installPhase = "ln -s $src $out";
630+ };
631+
632+ "orders" = pkgs.stdenv.mkDerivation {
633+ name = "orders";
634+ version = "v6.0.0";
635+ src = pkgs.fetchgit {
636+ url = "https://github.com/purescript/purescript-orders.git";
637+ rev = "f86db621ec5eef1274145f8b1fd8ebbfe0ed4a2c";
638+ sha256 = "14fjls3v14ia6fr2w4p0i5dqv10zk343wg28m4c77a17jmvbs9r4";
639+ };
640+ phases = "installPhase";
641+ installPhase = "ln -s $src $out";
642+ };
643+
644+ "parallel" = pkgs.stdenv.mkDerivation {
645+ name = "parallel";
646+ version = "v6.0.0";
647+ src = pkgs.fetchgit {
648+ url = "https://github.com/purescript/purescript-parallel.git";
649+ rev = "85290dca837771ac4870071008c933d315ef678f";
650+ sha256 = "0bpwvzq4dn7vx5mfj12w1zvvnamc6w2c45c2v54b9yw36snnwpcf";
651+ };
652+ phases = "installPhase";
653+ installPhase = "ln -s $src $out";
654+ };
655+
656+ "parsing" = pkgs.stdenv.mkDerivation {
657+ name = "parsing";
658+ version = "v9.1.0";
659+ src = pkgs.fetchgit {
660+ url = "https://github.com/purescript-contrib/purescript-parsing.git";
661+ rev = "44ef16b7cfb7cedc3d518c00ef10c6cb73ff4fc8";
662+ sha256 = "1wkk3p1w2k1cvby4airyxgavk1djfzvzc2ch6b8951b592rj8lkr";
663+ };
664+ phases = "installPhase";
665+ installPhase = "ln -s $src $out";
666+ };
667+
668+ "partial" = pkgs.stdenv.mkDerivation {
669+ name = "partial";
670+ version = "v4.0.0";
671+ src = pkgs.fetchgit {
672+ url = "https://github.com/purescript/purescript-partial.git";
673+ rev = "0fa0646f5ea1ec5f0c46dcbd770c705a6c9ad3ec";
674+ sha256 = "04s1h0r3slyd8kcamhqqrr6piksl9y76nmf2418j0ifhp16qwxmm";
675+ };
676+ phases = "installPhase";
677+ installPhase = "ln -s $src $out";
678+ };
679+
680+ "prelude" = pkgs.stdenv.mkDerivation {
681+ name = "prelude";
682+ version = "v6.0.0";
683+ src = pkgs.fetchgit {
684+ url = "https://github.com/purescript/purescript-prelude.git";
685+ rev = "32787f4399c92459c41602131a5858559eb868c5";
686+ sha256 = "0jkbckxw2640gmfi2f8bpf2zwnay2dxy8n24rvxl3w3dihcgd9qc";
687+ };
688+ phases = "installPhase";
689+ installPhase = "ln -s $src $out";
690+ };
691+
692+ "profunctor" = pkgs.stdenv.mkDerivation {
693+ name = "profunctor";
694+ version = "v6.0.0";
695+ src = pkgs.fetchgit {
696+ url = "https://github.com/purescript/purescript-profunctor.git";
697+ rev = "0a966a14e7b0c827d44657dc1710cdc712d2e034";
698+ sha256 = "07rhk8micbc74my6s53xbj2smvvwah2w1drqrcmcnncxdks3lqg3";
699+ };
700+ phases = "installPhase";
701+ installPhase = "ln -s $src $out";
702+ };
703+
704+ "profunctor-lenses" = pkgs.stdenv.mkDerivation {
705+ name = "profunctor-lenses";
706+ version = "v8.0.0";
707+ src = pkgs.fetchgit {
708+ url = "https://github.com/purescript-contrib/purescript-profunctor-lenses.git";
709+ rev = "973d567afe458fd802cf4f0d9725b6dc35ad9297";
710+ sha256 = "0axz7nznn12iqh6sn9qjg6i8qglpdsqgkp30ndwdj98czcnbnybv";
711+ };
712+ phases = "installPhase";
713+ installPhase = "ln -s $src $out";
714+ };
715+
716+ "psci-support" = pkgs.stdenv.mkDerivation {
717+ name = "psci-support";
718+ version = "v6.0.0";
719+ src = pkgs.fetchgit {
720+ url = "https://github.com/purescript/purescript-psci-support.git";
721+ rev = "897cdb543548cb6078d69b6413b54841404eda72";
722+ sha256 = "1ix53r8avkn3fw72mngwzw7v6c6mv7j4miw5mrgjrh9hb8p2ydl1";
723+ };
724+ phases = "installPhase";
725+ installPhase = "ln -s $src $out";
726+ };
727+
728+ "quickcheck" = pkgs.stdenv.mkDerivation {
729+ name = "quickcheck";
730+ version = "v8.0.1";
731+ src = pkgs.fetchgit {
732+ url = "https://github.com/purescript/purescript-quickcheck.git";
733+ rev = "bf5029f97e6c0d7552d3a08d2ab793a19e2c5e3d";
734+ sha256 = "142dvh57fl8b6i7mm37a38v7vkc1znbqz6l6wqa704m53hkikvyd";
735+ };
736+ phases = "installPhase";
737+ installPhase = "ln -s $src $out";
738+ };
739+
740+ "random" = pkgs.stdenv.mkDerivation {
741+ name = "random";
742+ version = "v6.0.0";
743+ src = pkgs.fetchgit {
744+ url = "https://github.com/purescript/purescript-random.git";
745+ rev = "9540bc965a9596da02fefd9949418bb19c92533a";
746+ sha256 = "0qz14qviz5053j9h1jwpk0gnn692hcdx6fp90wckzrnw31d53174";
747+ };
748+ phases = "installPhase";
749+ installPhase = "ln -s $src $out";
750+ };
751+
752+ "record" = pkgs.stdenv.mkDerivation {
753+ name = "record";
754+ version = "v4.0.0";
755+ src = pkgs.fetchgit {
756+ url = "https://github.com/purescript/purescript-record.git";
757+ rev = "c89cd1ada6b636692571fc374196b1c39c4c9f70";
758+ sha256 = "1g7s2h1as5cz824wpm0jhjprrh66shha5i4gq37q73yw0s5p2ahm";
759+ };
760+ phases = "installPhase";
761+ installPhase = "ln -s $src $out";
762+ };
763+
764+ "refs" = pkgs.stdenv.mkDerivation {
765+ name = "refs";
766+ version = "v6.0.0";
767+ src = pkgs.fetchgit {
768+ url = "https://github.com/purescript/purescript-refs.git";
769+ rev = "f8e6216da4cb9309fde1f20cd6f69ac3a3b7f9e8";
770+ sha256 = "09bvfxhjfwfwv55py45s71maazwrr68k0rk4v8ynfqv91h34319h";
771+ };
772+ phases = "installPhase";
773+ installPhase = "ln -s $src $out";
774+ };
775+
776+ "run" = pkgs.stdenv.mkDerivation {
777+ name = "run";
778+ version = "v5.0.0";
779+ src = pkgs.fetchgit {
780+ url = "https://github.com/natefaubion/purescript-run.git";
781+ rev = "abec7c343e92154d44b9dafd52b91ee82d32a870";
782+ sha256 = "059xhjxjfs1w2hmgld53kciyfws3pmjyz75w836ph99rqr4aczny";
783+ };
784+ phases = "installPhase";
785+ installPhase = "ln -s $src $out";
786+ };
787+
788+ "safe-coerce" = pkgs.stdenv.mkDerivation {
789+ name = "safe-coerce";
790+ version = "v2.0.0";
791+ src = pkgs.fetchgit {
792+ url = "https://github.com/purescript/purescript-safe-coerce.git";
793+ rev = "7fa799ae80a38b8d948efcb52608e58e198b3da7";
794+ sha256 = "00m4l733gpl0153cbl6n5kly7jr8ids399apza2rbczif40brp9g";
795+ };
796+ phases = "installPhase";
797+ installPhase = "ln -s $src $out";
798+ };
799+
800+ "st" = pkgs.stdenv.mkDerivation {
801+ name = "st";
802+ version = "v6.0.0";
803+ src = pkgs.fetchgit {
804+ url = "https://github.com/purescript/purescript-st.git";
805+ rev = "2cc7ae1c3318a303378c4a9f3fa0f10ee7cc3589";
806+ sha256 = "1ykk27zn03svfsv2s1mf88d6x3q6qhb9bwjzgals79krw8w0aiyl";
807+ };
808+ phases = "installPhase";
809+ installPhase = "ln -s $src $out";
810+ };
811+
812+ "strings" = pkgs.stdenv.mkDerivation {
813+ name = "strings";
814+ version = "v6.0.0";
815+ src = pkgs.fetchgit {
816+ url = "https://github.com/purescript/purescript-strings.git";
817+ rev = "4bc6954448d056f8aa7a659695a6ad60ad4fdf19";
818+ sha256 = "101va3rawcn5inp20krfg192bbjbvm27xfzwwc1inb2ya0m98amg";
819+ };
820+ phases = "installPhase";
821+ installPhase = "ln -s $src $out";
822+ };
823+
824+ "tailrec" = pkgs.stdenv.mkDerivation {
825+ name = "tailrec";
826+ version = "v6.0.0";
827+ src = pkgs.fetchgit {
828+ url = "https://github.com/purescript/purescript-tailrec.git";
829+ rev = "5e2104aa734b31a17074cc805bf087d72b65afd1";
830+ sha256 = "0vc3dpsd2xxz694zlm75nx1cn6c6f29nzkb8xlxkk6ydw3wpsk3w";
831+ };
832+ phases = "installPhase";
833+ installPhase = "ln -s $src $out";
834+ };
835+
836+ "test-unit" = pkgs.stdenv.mkDerivation {
837+ name = "test-unit";
838+ version = "v17.0.0";
839+ src = pkgs.fetchgit {
840+ url = "https://github.com/bodil/purescript-test-unit.git";
841+ rev = "3112d7ebe06d467238058a6384dc75ffd960d335";
842+ sha256 = "02gfb4dqbj5v7dwifm8jhr0lh7lfp5hari5795w1z48wy3apnvvx";
843+ };
844+ phases = "installPhase";
845+ installPhase = "ln -s $src $out";
846+ };
847+
848+ "transformers" = pkgs.stdenv.mkDerivation {
849+ name = "transformers";
850+ version = "v6.0.0";
851+ src = pkgs.fetchgit {
852+ url = "https://github.com/purescript/purescript-transformers.git";
853+ rev = "be72ab52357d9a665cbf93d73ba1c07c4b0957ee";
854+ sha256 = "0ijrdsppl8vx0rlbwc9p0vjbmvwlcy4ia7xymvk6y34zbxzjlzm6";
855+ };
856+ phases = "installPhase";
857+ installPhase = "ln -s $src $out";
858+ };
859+
860+ "tuples" = pkgs.stdenv.mkDerivation {
861+ name = "tuples";
862+ version = "v7.0.0";
863+ src = pkgs.fetchgit {
864+ url = "https://github.com/purescript/purescript-tuples.git";
865+ rev = "4f52da2729b448c8564369378f1232d8d2dc1d8b";
866+ sha256 = "1m1ng0xxicb73945jymcl1hn2y2hmynlnmhb2k0kkn1jrjwgcc3d";
867+ };
868+ phases = "installPhase";
869+ installPhase = "ln -s $src $out";
870+ };
871+
872+ "type-equality" = pkgs.stdenv.mkDerivation {
873+ name = "type-equality";
874+ version = "v4.0.1";
875+ src = pkgs.fetchgit {
876+ url = "https://github.com/purescript/purescript-type-equality.git";
877+ rev = "0525b7d39e0fbd81b4209518139fb8ab02695774";
878+ sha256 = "1ass38jdycsjisdimdc4drg2w8vkkwp6lkvz3kvy7q0h98vdmlbr";
879+ };
880+ phases = "installPhase";
881+ installPhase = "ln -s $src $out";
882+ };
883+
884+ "typelevel-prelude" = pkgs.stdenv.mkDerivation {
885+ name = "typelevel-prelude";
886+ version = "v7.0.0";
887+ src = pkgs.fetchgit {
888+ url = "https://github.com/purescript/purescript-typelevel-prelude.git";
889+ rev = "dca2fe3c8cfd5527d4fe70c4bedfda30148405bf";
890+ sha256 = "0x86mrg33kpnrnsfp4p3c92j5lpyqzy87bxdynwf7smk3inqr2jc";
891+ };
892+ phases = "installPhase";
893+ installPhase = "ln -s $src $out";
894+ };
895+
896+ "uint" = pkgs.stdenv.mkDerivation {
897+ name = "uint";
898+ version = "v7.0.0";
899+ src = pkgs.fetchgit {
900+ url = "https://github.com/purescript-contrib/purescript-uint.git";
901+ rev = "9e4f76ffd5192472f75583844172fe8ab3c0cb9f";
902+ sha256 = "173bhrd006q53s7agwyasxhfbr89x9jpz5b47vm2fr74l3jcw3lq";
903+ };
904+ phases = "installPhase";
905+ installPhase = "ln -s $src $out";
906+ };
907+
908+ "undefined-is-not-a-problem" = pkgs.stdenv.mkDerivation {
909+ name = "undefined-is-not-a-problem";
910+ version = "v1.1.0";
911+ src = pkgs.fetchgit {
912+ url = "https://github.com/paluh/purescript-undefined-is-not-a-problem.git";
913+ rev = "8ac77ac8525343f7319cd48bf2ceb736286f3b27";
914+ sha256 = "1qv7gdyslfj86ga0a7gxkbpvykm3s1x4kzcm8zi75lhlvkpyjh3w";
915+ };
916+ phases = "installPhase";
917+ installPhase = "ln -s $src $out";
918+ };
919+
920+ "unfoldable" = pkgs.stdenv.mkDerivation {
921+ name = "unfoldable";
922+ version = "v6.0.0";
923+ src = pkgs.fetchgit {
924+ url = "https://github.com/purescript/purescript-unfoldable.git";
925+ rev = "493dfe04ed590e20d8f69079df2f58486882748d";
926+ sha256 = "15z2k639ph8wdkrc2y838m5am1z7szw2vqymmv021skzisyn7zwf";
927+ };
928+ phases = "installPhase";
929+ installPhase = "ln -s $src $out";
930+ };
931+
932+ "unicode" = pkgs.stdenv.mkDerivation {
933+ name = "unicode";
934+ version = "v6.0.0";
935+ src = pkgs.fetchgit {
936+ url = "https://github.com/purescript-contrib/purescript-unicode.git";
937+ rev = "6454d59d9e1fd1bc5a72e80e985d8282022a567a";
938+ sha256 = "0w2gsmqcy7pj5vd22qzpig20bdgzmbagyqxrg9mm1njjfsmcm8cv";
939+ };
940+ phases = "installPhase";
941+ installPhase = "ln -s $src $out";
942+ };
943+
944+ "unsafe-coerce" = pkgs.stdenv.mkDerivation {
945+ name = "unsafe-coerce";
946+ version = "v6.0.0";
947+ src = pkgs.fetchgit {
948+ url = "https://github.com/purescript/purescript-unsafe-coerce.git";
949+ rev = "ab956f82e66e633f647fb3098e8ddd3ec58d689f";
950+ sha256 = "0r6d3dx8jalfzvrvkagz9v05yxwkkhgbzlpswg4w1cyl03zjcla4";
951+ };
952+ phases = "installPhase";
953+ installPhase = "ln -s $src $out";
954+ };
955+
956+ "untagged-union" = pkgs.stdenv.mkDerivation {
957+ name = "untagged-union";
958+ version = "6c5bccef38696a16e86ba9a65cf778b4462c4a66";
959+ src = pkgs.fetchgit {
960+ url = "https://github.com/rowtype-yoga/purescript-untagged-union.git";
961+ rev = "6c5bccef38696a16e86ba9a65cf778b4462c4a66";
962+ sha256 = "0s4pik6xcr7zqhlrjxjqgzp8hfg09kgd58zq3xjj0inw9ly35ywn";
963+ };
964+ phases = "installPhase";
965+ installPhase = "ln -s $src $out";
966+ };
967+
968+ "variant" = pkgs.stdenv.mkDerivation {
969+ name = "variant";
970+ version = "v8.0.0";
971+ src = pkgs.fetchgit {
972+ url = "https://github.com/natefaubion/purescript-variant.git";
973+ rev = "8b2bd73a4202257d80a955c4dd36f6055f149823";
974+ sha256 = "18nyxnaqqg1zzydwd2hh9hbzr31j509g5yb1lnd0m60bma4l9h5g";
975+ };
976+ phases = "installPhase";
977+ installPhase = "ln -s $src $out";
978+ };
979+
980+ "web-dom" = pkgs.stdenv.mkDerivation {
981+ name = "web-dom";
982+ version = "v6.0.0";
983+ src = pkgs.fetchgit {
984+ url = "https://github.com/purescript-web/purescript-web-dom.git";
985+ rev = "568a1ee158b29e6e739e7a9aaed3e35ca4c4305a";
986+ sha256 = "0angs86ld0h3ld889ag55lh62101dfn3fknyqrfv0imgn40vwm44";
987+ };
988+ phases = "installPhase";
989+ installPhase = "ln -s $src $out";
990+ };
991+
992+ "web-events" = pkgs.stdenv.mkDerivation {
993+ name = "web-events";
994+ version = "v4.0.0";
995+ src = pkgs.fetchgit {
996+ url = "https://github.com/purescript-web/purescript-web-events.git";
997+ rev = "2124356117be7b764a2f3948032255ac4dab7051";
998+ sha256 = "0l47gsgj71x0gjbhjqwsar2y44ljhq3xa0nkgr1qvqxmcm00llqf";
999+ };
1000+ phases = "installPhase";
1001+ installPhase = "ln -s $src $out";
1002+ };
1003+
1004+ "web-file" = pkgs.stdenv.mkDerivation {
1005+ name = "web-file";
1006+ version = "v4.0.0";
1007+ src = pkgs.fetchgit {
1008+ url = "https://github.com/purescript-web/purescript-web-file.git";
1009+ rev = "023786ae62bbb8bf58156dd7f02011fa38221ef1";
1010+ sha256 = "1wjf1m5y2qsba83is297icv9zyl7jilxdga0192mm1rp0j16jjln";
1011+ };
1012+ phases = "installPhase";
1013+ installPhase = "ln -s $src $out";
1014+ };
1015+
1016+ "web-html" = pkgs.stdenv.mkDerivation {
1017+ name = "web-html";
1018+ version = "v4.0.0";
1019+ src = pkgs.fetchgit {
1020+ url = "https://github.com/purescript-web/purescript-web-html.git";
1021+ rev = "be189cf91b9a19cf493637423522e2fe4a0088cc";
1022+ sha256 = "0yspv6sci3mkflvqwf5jvz3a8nclmhbvazx9n34acyj3aai7k6bs";
1023+ };
1024+ phases = "installPhase";
1025+ installPhase = "ln -s $src $out";
1026+ };
1027+
1028+ "web-storage" = pkgs.stdenv.mkDerivation {
1029+ name = "web-storage";
1030+ version = "v5.0.0";
1031+ src = pkgs.fetchgit {
1032+ url = "https://github.com/purescript-web/purescript-web-storage.git";
1033+ rev = "6b74461e136755db70c271dc898d51776363d7e2";
1034+ sha256 = "0q1grh8cp6h1szsqg3xmwlzrc1bbz8yzlwxlcl1iym5mjiyzgkcs";
1035+ };
1036+ phases = "installPhase";
1037+ installPhase = "ln -s $src $out";
1038+ };
1039+
1040+ };
1041+
1042+ cpPackage = pkg:
1043+ let
1044+ target = ".spago/${pkg.name}/${pkg.version}";
1045+ in ''
1046+ if [ ! -e ${target} ]; then
1047+ echo "Installing ${target}."
1048+ mkdir -p ${target}
1049+ cp --no-preserve=mode,ownership,timestamp -r ${toString pkg.outPath}/* ${target}
1050+ else
1051+ echo "${target} already exists. Skipping."
1052+ fi
1053+ '';
1054+
1055+ getGlob = pkg: ''".spago/${pkg.name}/${pkg.version}/src/**/*.purs"'';
1056+
1057+ getStoreGlob = pkg: ''"${pkg.outPath}/src/**/*.purs"'';
1058+
1059+in {
1060+ inherit inputs;
1061+
1062+ installSpagoStyle = pkgs.writeShellScriptBin "install-spago-style" ''
1063+ set -e
1064+ echo installing dependencies...
1065+ ${builtins.toString (builtins.map cpPackage (builtins.attrValues inputs))}
1066+ echo "echo done."
1067+ '';
1068+
1069+ buildSpagoStyle = pkgs.writeShellScriptBin "build-spago-style" ''
1070+ set -e
1071+ echo building project...
1072+ purs compile ${builtins.toString (builtins.map getGlob (builtins.attrValues inputs))} "$@"
1073+ echo done.
1074+ '';
1075+
1076+ buildFromNixStore = pkgs.writeShellScriptBin "build-from-store" ''
1077+ set -e
1078+ echo building project using sources from nix store...
1079+ purs compile ${builtins.toString (
1080+ builtins.map getStoreGlob (builtins.attrValues inputs))} "$@"
1081+ echo done.
1082+ '';
1083+
1084+ mkBuildProjectOutput =
1085+ { src, purs }:
1086+
1087+ pkgs.stdenv.mkDerivation {
1088+ name = "build-project-output";
1089+ src = src;
1090+
1091+ buildInputs = [ purs ];
1092+
1093+ installPhase = ''
1094+ mkdir -p $out
1095+ purs compile "$src/**/*.purs" ${builtins.toString
1096+ (builtins.map
1097+ (x: ''"${x.outPath}/src/**/*.purs"'')
1098+ (builtins.attrValues inputs))}
1099+ mv output $out
1100+ '';
1101+ };
1102+}
spago.dhall+5
@@ -28,12 +28,17 @@ to generate this file without the comments in this block.
2828 , "either"
2929 , "exceptions"
3030 , "foldable-traversable"
31+ , "foreign"
3132 , "functions"
33+ , "integers"
34+ , "literals"
3235 , "maybe"
3336 , "newtype"
3437 , "nullable"
38+ , "numbers"
3539 , "orders"
3640 , "prelude"
41+ , "random"
3742 , "refs"
3843 , "run"
3944 , "strings"
style.css+42
@@ -107,3 +107,45 @@ textarea {
107107 min-width: 5rem;
108108 text-align: center;
109109}
110+
111+.p-side-navigation__drawer {
112+ max-width: initial;
113+}
114+
115+.select-cipher-type-container {
116+ display: flex;
117+ flex-direction: column;
118+ align-items: center;
119+ justify-content: center;
120+}
121+
122+.select-cipher-type-container > * {
123+ margin: 0px;
124+ margin-bottom: 1.5rem;
125+}
126+
127+.overlay {
128+ position: fixed;
129+ top: 0;
130+ left: 0;
131+ right: 0;
132+ bottom: 0;
133+}
134+
135+.select-second-factor-container {
136+ display: flex;
137+ flex-direction: column;
138+ align-items: center;
139+ justify-content: center;
140+}
141+
142+.p-tabs__list {
143+ overflow: initial;
144+}
145+
146+.p-modal {
147+ top: 0;
148+ left: 0;
149+ right: 0;
150+ bottom: 0;
151+}
tests/package-lock.jsonnew file+73
@@ -0,0 +1,73 @@
1+{
2+ "name": "warden-tests",
3+ "version": "1.0.0",
4+ "lockfileVersion": 2,
5+ "requires": true,
6+ "packages": {
7+ "": {
8+ "name": "warden-tests",
9+ "version": "1.0.0",
10+ "devDependencies": {
11+ "@playwright/test": "^1.27.1"
12+ }
13+ },
14+ "node_modules/@playwright/test": {
15+ "version": "1.27.1",
16+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.27.1.tgz",
17+ "integrity": "sha512-mrL2q0an/7tVqniQQF6RBL2saskjljXzqNcCOVMUjRIgE6Y38nCNaP+Dc2FBW06bcpD3tqIws/HT9qiMHbNU0A==",
18+ "dev": true,
19+ "dependencies": {
20+ "@types/node": "*",
21+ "playwright-core": "1.27.1"
22+ },
23+ "bin": {
24+ "playwright": "cli.js"
25+ },
26+ "engines": {
27+ "node": ">=14"
28+ }
29+ },
30+ "node_modules/@types/node": {
31+ "version": "18.11.9",
32+ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz",
33+ "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==",
34+ "dev": true
35+ },
36+ "node_modules/playwright-core": {
37+ "version": "1.27.1",
38+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.27.1.tgz",
39+ "integrity": "sha512-9EmeXDncC2Pmp/z+teoVYlvmPWUC6ejSSYZUln7YaP89Z6lpAaiaAnqroUt/BoLo8tn7WYShcfaCh+xofZa44Q==",
40+ "dev": true,
41+ "bin": {
42+ "playwright": "cli.js"
43+ },
44+ "engines": {
45+ "node": ">=14"
46+ }
47+ }
48+ },
49+ "dependencies": {
50+ "@playwright/test": {
51+ "version": "1.27.1",
52+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.27.1.tgz",
53+ "integrity": "sha512-mrL2q0an/7tVqniQQF6RBL2saskjljXzqNcCOVMUjRIgE6Y38nCNaP+Dc2FBW06bcpD3tqIws/HT9qiMHbNU0A==",
54+ "dev": true,
55+ "requires": {
56+ "@types/node": "*",
57+ "playwright-core": "1.27.1"
58+ }
59+ },
60+ "@types/node": {
61+ "version": "18.11.9",
62+ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz",
63+ "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==",
64+ "dev": true
65+ },
66+ "playwright-core": {
67+ "version": "1.27.1",
68+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.27.1.tgz",
69+ "integrity": "sha512-9EmeXDncC2Pmp/z+teoVYlvmPWUC6ejSSYZUln7YaP89Z6lpAaiaAnqroUt/BoLo8tn7WYShcfaCh+xofZa44Q==",
70+ "dev": true
71+ }
72+ }
73+}
tests/package.jsonnew file+9
@@ -0,0 +1,9 @@
1+{
2+ "name": "warden-tests",
3+ "version": "1.0.0",
4+ "scripts": {},
5+ "keywords": [],
6+ "devDependencies": {
7+ "@playwright/test": "^1.27.1"
8+ }
9+}
tests/playwright.config.tsnew file+65
@@ -0,0 +1,65 @@
1+import type { PlaywrightTestConfig } from '@playwright/test';
2+import { devices } from '@playwright/test';
3+
4+/**
5+ * See https://playwright.dev/docs/test-configuration.
6+ */
7+const config: PlaywrightTestConfig = {
8+ testDir: './src',
9+ /* Maximum time one test can run for. */
10+ timeout: 30 * 1000,
11+ expect: {
12+ /**
13+ * Maximum time expect() should wait for the condition to be met.
14+ * For example in `await expect(locator).toHaveText();`
15+ */
16+ timeout: 5000
17+ },
18+ /* Run tests in files in parallel */
19+ fullyParallel: false,
20+ /* Fail the build on CI if you accidentally left test.only in the source code. */
21+ forbidOnly: !!process.env.CI,
22+ /* Retry on CI only */
23+ retries: 0,
24+ /* Opt out of parallel tests on CI. */
25+ workers: 1,
26+ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
27+ reporter: 'html',
28+ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
29+ use: {
30+ /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
31+ actionTimeout: 0,
32+ /* Base URL to use in actions like `await page.goto('/')`. */
33+ // baseURL: 'http://localhost:3000',
34+
35+ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
36+ trace: 'on-first-retry',
37+ },
38+
39+ /* Configure projects for major browsers */
40+ projects: [
41+ {
42+ name: 'Pixel 3',
43+ use: {
44+ ...devices['Pixel 3'],
45+ launchOptions: {
46+ args: ["--disable-web-security"],
47+ },
48+ headless: process.env.RECORD_TEST !== "true",
49+ }
50+ },
51+
52+ ],
53+
54+ /* Folder for test artifacts such as screenshots, videos, traces, etc. */
55+ outputDir: 'test-results/',
56+
57+ /* Run your local dev server before starting the tests */
58+ webServer: {
59+ command: './vaultwarden/run.sh',
60+ port: 8000,
61+ },
62+
63+};
64+
65+export default config;
tests/src/test.spec.tsnew file+257
@@ -0,0 +1,257 @@
1+import { test, expect, Page } from '@playwright/test';
2+
3+async function logIn(page: Page) {
4+ await page.locator('input[type="url"]').tap();
5+ await page.locator('input[type="url"]').press('Control+a');
6+ await page.locator('input[type="url"]').fill('http://localhost:8000');
7+ await page.locator('input[type="email"]').tap();
8+ await page.locator('input[type="email"]').fill('test@example.com');
9+ await page.locator('input[type="password"]').tap();
10+ await page.locator('input[type="password"]').fill('Password123');
11+ await page.getByRole('button', { name: 'Log in' }).tap();
12+ await page.locator('.p-icon--spinner.u-animation--spin.spinner').waitFor({ state: "visible" })
13+ await page.locator('.p-icon--spinner.u-animation--spin.spinner').waitFor({ state: "hidden" })
14+}
15+
16+async function fillInLoginCipher(page: Page) {
17+ await page.locator('p:has-text("Entry name") input[type="text"]').tap();
18+ await page.locator('p:has-text("Entry name") input[type="text"]').fill('Test login entry');
19+ await page.locator('p:has-text("Username") input[type="text"]').tap();
20+ await page.locator('p:has-text("Username") input[type="text"]').fill('login_entry_username');
21+ await page.locator('input[type="text"]').nth(2).tap();
22+ await page.locator('input[type="text"]').nth(2).fill('Password123');
23+ await page.locator('input[type="url"]').tap();
24+ await page.locator('input[type="url"]').fill('https://example.com');
25+}
26+
27+async function fillInCardCipher(page: Page) {
28+ await page.locator('p:has-text("Entry name") input[type="text"]').tap();
29+ await page.locator('p:has-text("Entry name") input[type="text"]').fill('Test card');
30+ await page.locator('p:has-text("Card number") input[type="text"]').tap();
31+ await page.locator('p:has-text("Card number") input[type="text"]').fill('1234567890123456');
32+ await page.locator('p:has-text("Expiration date /") input[type="text"]').first().tap();
33+ await page.locator('p:has-text("Expiration date /") input[type="text"]').first().fill('01');
34+ await page.locator('p:has-text("Expiration date /") input[type="text"]').nth(1).tap();
35+ await page.locator('p:has-text("Expiration date /") input[type="text"]').nth(1).fill('01');
36+ await page.locator('p:has-text("CVV") input[type="text"]').tap();
37+ await page.locator('p:has-text("CVV") input[type="text"]').fill('123');
38+ await page.locator('p:has-text("Cardholder name") input[type="text"]').tap();
39+ await page.locator('p:has-text("Cardholder name") input[type="text"]').fill('Sampel Palnet');
40+}
41+
42+async function fillInNoteCipher(page: Page) {
43+ await page.locator('input[type="text"]').tap();
44+ await page.locator('input[type="text"]').fill('Secret note');
45+ await page.locator('textarea').tap();
46+ await page.locator('textarea').fill('This is a very secret note');
47+}
48+
49+async function fillInContractCipher(page: Page) {
50+ await page.locator('p:has-text("Entry name") input[type="text"]').tap();
51+ await page.locator('p:has-text("Entry name") input[type="text"]').fill('John Appleseed');
52+ await page.locator('p:has-text("First name") input[type="text"]').tap();
53+ await page.locator('p:has-text("First name") input[type="text"]').fill('John');
54+ await page.locator('p:has-text("Middle name") input[type="text"]').tap();
55+ await page.locator('p:has-text("Middle name") input[type="text"]').fill('Mathews');
56+ await page.locator('p:has-text("Last name") input[type="text"]').tap();
57+ await page.locator('p:has-text("Last name") input[type="text"]').fill('Appleseed');
58+ await page.locator('p:has-text("Username") input[type="text"]').tap();
59+ await page.locator('p:has-text("Username") input[type="text"]').fill('jappleseed');
60+ await page.locator('p:has-text("Company") input[type="text"]').tap();
61+ await page.locator('p:has-text("Company") input[type="text"]').fill('Very good building company');
62+ await page.locator('p:has-text("Social security number") input[type="number"]').tap();
63+ await page.locator('p:has-text("Social security number") input[type="number"]').fill('123456789');
64+ await page.locator('p:has-text("Passport number") input[type="number"]').tap();
65+ await page.locator('p:has-text("Passport number") input[type="number"]').fill('123456798');
66+ await page.locator('p:has-text("License Number") input[type="number"]').tap();
67+ await page.locator('p:has-text("License Number") input[type="number"]').fill('12345');
68+ await page.locator('input[type="email"]').tap();
69+ await page.locator('input[type="email"]').fill('john@example.com');
70+ await page.locator('input[type="tel"]').tap();
71+ await page.locator('input[type="tel"]').fill('123456789');
72+ await page.locator('p:has-text("Address line 1") input[type="text"]').tap();
73+ await page.locator('p:has-text("Address line 1") input[type="text"]').fill('Abc def');
74+ await page.locator('p:has-text("Address line 2") input[type="text"]').tap();
75+ await page.locator('p:has-text("Address line 2") input[type="text"]').fill('Ghijk Lmn');
76+}
77+
78+async function screenshot(page: Page) {
79+ await page.waitForLoadState("networkidle")
80+
81+ if (await page.locator('div.floating-notifications').isVisible()) {
82+ await page.locator("button.p-notification__close").tap()
83+ await screenshot(page)
84+ } else if (await page.locator('.p-icon--spinner.u-animation--spin.spinner').isVisible()) {
85+ await page.locator('.p-icon--spinner.u-animation--spin.spinner').waitFor({ state: "hidden" })
86+ await screenshot(page)
87+ } else {
88+ await expect.soft(page).toHaveScreenshot({ animations: "disabled", fullPage: true, maxDiffPixelRatio: 0.01 })
89+ }
90+}
91+
92+async function waitForNotification(page: Page) {
93+ await page.locator('div.floating-notifications').waitFor({ state: "visible" })
94+}
95+
96+
97+/**
98+ * Screenshots the very first (last edited) entry.
99+ */
100+function cipherPage(name: string) {
101+ test(name + " cipher page", async ({ page }) => {
102+ await logIn(page)
103+
104+ await page.locator('.cipher-row').first().tap()
105+
106+ await screenshot(page)
107+ })
108+}
109+
110+test.beforeEach(async ({ page }) => {
111+ await page.goto(`file://${__dirname}/../../dist/index.html`);
112+ await page.addStyleTag({ content: '.hide-in-test {opacity: 0;}' })
113+})
114+
115+if (process.env.RECORD_TEST === "true") {
116+
117+ test('record', async ({ page }) => {
118+ await logIn(page)
119+ await page.pause()
120+ })
121+
122+} else {
123+ test('Login page', async ({ page }) => {
124+ await screenshot(page)
125+ });
126+
127+ test('Empty ciphers page', async ({ page }) => {
128+ await logIn(page)
129+
130+ await screenshot(page)
131+ })
132+
133+ test('Login entry page', async ({ page }) => {
134+ await logIn(page)
135+
136+ await page.getByRole('button').nth(2).tap();
137+ await page.getByRole('button', { name: 'Login' }).tap();
138+
139+ await screenshot(page)
140+
141+ await fillInLoginCipher(page)
142+
143+ await screenshot(page)
144+
145+ await page.getByRole('heading').locator('span').getByRole('button').tap();
146+
147+ await waitForNotification(page)
148+ });
149+
150+ cipherPage("Login")
151+
152+ test('Create Card entry page', async ({ page }) => {
153+ await logIn(page)
154+
155+ await page.getByRole('button').nth(2).tap();
156+ await page.getByRole('button', { name: 'Card' }).tap();
157+
158+ await screenshot(page)
159+
160+ await fillInCardCipher(page)
161+
162+ await screenshot(page)
163+
164+ await page.getByRole('heading').locator('span').getByRole('button').tap();
165+
166+ await waitForNotification(page)
167+ })
168+
169+ cipherPage("Card")
170+
171+ test('Create Note entry page', async ({ page }) => {
172+ await logIn(page)
173+
174+ await page.getByRole('button').nth(2).tap();
175+ await page.getByRole('button', { name: 'Note' }).tap();
176+
177+ await screenshot(page)
178+
179+ await fillInNoteCipher(page)
180+
181+ await screenshot(page)
182+
183+ await page.getByRole('heading').locator('span').getByRole('button').tap();
184+
185+ await waitForNotification(page)
186+ })
187+
188+ cipherPage("Note")
189+
190+ test('Create Contact entry page', async ({ page }) => {
191+ await logIn(page)
192+
193+ await page.getByRole('button').nth(2).tap();
194+ await page.getByRole('button', { name: 'Contact' }).tap();
195+
196+ await screenshot(page)
197+
198+ await fillInContractCipher(page)
199+
200+ await screenshot(page)
201+
202+ await page.getByRole('heading').locator('span').getByRole('button').tap();
203+
204+ await waitForNotification(page)
205+ })
206+
207+ cipherPage("Contact")
208+
209+ test('Filled ciphers page', async ({ page }) => {
210+ await logIn(page)
211+ await screenshot(page)
212+ })
213+
214+ test('Side bar', async ({ page }) => {
215+ await logIn(page)
216+
217+ await page.getByRole('button').nth(1).tap();
218+
219+ await screenshot(page)
220+ })
221+
222+ test('Passwords view', async ({ page }) => {
223+ await logIn(page)
224+
225+ await page.getByRole('button').nth(1).click();
226+ await page.getByText('Passwords').click();
227+
228+ await screenshot(page)
229+ })
230+
231+ test('Notes view', async ({ page }) => {
232+ await logIn(page)
233+
234+ await page.getByRole('button').nth(1).click();
235+ await page.getByText('Notes').click();
236+
237+ await screenshot(page)
238+ })
239+
240+ test('Cards view', async ({ page }) => {
241+ await logIn(page)
242+
243+ await page.getByRole('button').nth(1).click();
244+ await page.getByText('Cards').click();
245+
246+ await screenshot(page)
247+ })
248+
249+ test('Contacts view', async ({ page }) => {
250+ await logIn(page)
251+
252+ await page.getByRole('button').nth(1).click();
253+ await page.getByText('Contacts').click();
254+
255+ await screenshot(page)
256+ })
257+}
tests/src/test.spec.ts-snapshots/Card-cipher-page-1-Pixel-3-linux.pngnew file

Binary file not shown.

tests/src/test.spec.ts-snapshots/Cards-view-1-Pixel-3-linux.pngnew file

Binary file not shown.

tests/src/test.spec.ts-snapshots/Contact-cipher-page-1-Pixel-3-linux.pngnew file

Binary file not shown.

tests/src/test.spec.ts-snapshots/Contacts-view-1-Pixel-3-linux.pngnew file

Binary file not shown.

tests/src/test.spec.ts-snapshots/Create-Card-entry-page-1-Pixel-3-linux.pngnew file

Binary file not shown.

tests/src/test.spec.ts-snapshots/Create-Card-entry-page-2-Pixel-3-linux.pngnew file

Binary file not shown.

tests/src/test.spec.ts-snapshots/Create-Contact-entry-page-1-Pixel-3-linux.pngnew file

Binary file not shown.

tests/src/test.spec.ts-snapshots/Create-Contact-entry-page-2-Pixel-3-linux.pngnew file

Binary file not shown.

tests/src/test.spec.ts-snapshots/Create-Note-entry-page-1-Pixel-3-linux.pngnew file

Binary file not shown.

tests/src/test.spec.ts-snapshots/Create-Note-entry-page-2-Pixel-3-linux.pngnew file

Binary file not shown.

tests/src/test.spec.ts-snapshots/Empty-ciphers-page-1-Pixel-3-linux.pngnew file

Binary file not shown.

tests/src/test.spec.ts-snapshots/Filled-ciphers-page-1-Pixel-3-linux.pngnew file

Binary file not shown.

tests/src/test.spec.ts-snapshots/Login-cipher-page-1-Pixel-3-linux.pngnew file

Binary file not shown.

tests/src/test.spec.ts-snapshots/Login-entry-page-1-Pixel-3-linux.pngnew file

Binary file not shown.

tests/src/test.spec.ts-snapshots/Login-entry-page-2-Pixel-3-linux.pngnew file

Binary file not shown.

tests/src/test.spec.ts-snapshots/Login-page-1-Pixel-3-linux.pngnew file

Binary file not shown.

tests/src/test.spec.ts-snapshots/Note-cipher-page-1-Pixel-3-linux.pngnew file

Binary file not shown.

tests/src/test.spec.ts-snapshots/Notes-view-1-Pixel-3-linux.pngnew file

Binary file not shown.

tests/src/test.spec.ts-snapshots/Passwords-view-1-Pixel-3-linux.pngnew file

Binary file not shown.

tests/src/test.spec.ts-snapshots/Side-bar-1-Pixel-3-linux.pngnew file

Binary file not shown.

tests/vaultwarden/.envnew file+3
@@ -0,0 +1,3 @@
1+WEB_VAULT_ENABLED=false
2+LOGIN_RATELIMIT_SECONDS=1
3+LOGIN_RATELIMIT_MAX_BURST=100000000
tests/vaultwarden/README.mdnew file+4
@@ -0,0 +1,4 @@
1+# Test vaultwarden setup
2+
3+login: `test@example.com`
4+password: `Password123`
tests/vaultwarden/data/db.sqlite3new file

Binary file not shown.

tests/vaultwarden/data/db.sqlite3-shmnew file

Binary file not shown.

tests/vaultwarden/data/db.sqlite3-walnew file

No changes to the content.

tests/vaultwarden/data/rsa_key.pemnew file+27
@@ -0,0 +1,27 @@
1+-----BEGIN RSA PRIVATE KEY-----
2+MIIEowIBAAKCAQEArVVTdkrmMbDxyGxGY73Fse9Du7DplgSBPQ5uyOnAsqf6pa1U
3+jjWwTV7Xqzp0z7k32WC64P0eGdacTQGZiFUQxTVm6bJ4TZ8bh4fz51tB4npCAzpr
4+8EcgGRzZT+cA1GQOG7jDqv8w9edWWGdPr6mx1ENvnpXtv60tvnwfsioAUSnx3SC6
5+0oTVCxj1InyneUDXCk/UqBE1wmwl8aF5U3aOc42qcELAgr6i0aCsK1LQjH+SOREh
6+tRQgpDF17IdYAxdYUTyinqrYJnoo5kQXeC/4QoZIsDi1empas7pXwrDlVYtjNK2T
7+Oht6dWUnffc2oTUCNY+ATZ6Nsu4zItEM7yQo/QIDAQABAoIBABUX/q21JLlkXrA0
8+2QYXWKE0TGphd7O9fZrYEExcAgrb3WlfJBygVQG9aoKYfRGwaScHmgZ6VzjuwJuw
9+d6Y7L+zH+Gu1P+W2WKvQNDLyDJ0KotDwwaT+it3zgFC0fIncWUkvXYSIRMWQLxZY
10+ED8A6Da8NC+48DN02E3HnjXY3awCusn9fQkPLeparsdyrvz5s8lnYYQZ6u7FQeD9
11+gM5SUP8WVLtPl4KBidJvHl4qPDAxnu98cZEoffbWIAsg3wvKNZL9yE21ZGWdExxA
12+/czWYO4ckuGY7Tb0Y+MiRI5kBM4VbCzwVfZTLRPXnKddiL7QpLMNmYpHzEcMZut4
13+z3vcdnMCgYEA5ZIzaltIEfL8zmX76pIPlyBs5n98RaagCIxQyURNRSU4H6Sr6yW2
14+CNAszcn6iAxiteXkPUkWmPXE4BFt0ifKgecPQIDk0spaVu2khRPMwUMyR0PfwXfY
15+2fqT0zMZX0MB3VPxnDTY9sAvTi2FsskL9j+Pil7U8bYaixzdc2H+LcsCgYEAwUm2
16+5A1eyX8ojZM5xUaQ/ZAiW+9u0fI5UxJ9Q0SFUWn11LJyIoIvkw6puE10508e7UT1
17+rnYZc5v/Mr6E1oO2NGBHkOZdlfJMD346r2YAtm2Nt/HZuLZU38wyIZzg0FxS6lmZ
18+m1vTEoM6Ko5GVtjvLEst27ZZDgWRW7rJNyYEq1cCgYBm6ZJY9FCrkzUXRYqXziBl
19+DCklbzMDej21d0lj4FWPUK8HKGfVQxOGgeQD4K6JvJunlUYq3w/dRBecptOHaUUT
20+1MP2k5GAmSvEWkXtTkH0xS3N2rR84z/3/6ey/u450S0Nzeq6rsowW1kZf7i2u/+7
21+xSSQ0BA/xvXBNRBlACHU3wKBgQDAuYF0zqgOfY2v7HaT75KuYUMBLDHxgHYMEMl5
22+gx7zWJXXqL4JDonlNZ+gtwqzgL6zkijt9a/XCFpi/HhzdAz/GGkDX888rcWNRS26
23+qhhXzdCa8rlGZfcm1vG9djDrUMMUDJcOw3YoZVRgDlCwU8nW/ncRNjaBtqeDMTFk
24+nbHWLQKBgH0Xatwy6DVHDbU+GYcZBjyQP5zCz0JEjDE8hEhv/ioCfUK1rfjY58zU
25+oXDdn3GpFm6eFxcVxv9Ki4jViQDYAvAErBl0kMkVU6hvITPlMEyqBh89h65xUEnw
26+ttaX+Q2lKGwHsZT4RzA45XjO8kvBymY9siL2JQUMLB8wpu9RyTRj
27+-----END RSA PRIVATE KEY-----
tests/vaultwarden/data/rsa_key.pub.pemnew file+9
@@ -0,0 +1,9 @@
1+-----BEGIN PUBLIC KEY-----
2+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArVVTdkrmMbDxyGxGY73F
3+se9Du7DplgSBPQ5uyOnAsqf6pa1UjjWwTV7Xqzp0z7k32WC64P0eGdacTQGZiFUQ
4+xTVm6bJ4TZ8bh4fz51tB4npCAzpr8EcgGRzZT+cA1GQOG7jDqv8w9edWWGdPr6mx
5+1ENvnpXtv60tvnwfsioAUSnx3SC60oTVCxj1InyneUDXCk/UqBE1wmwl8aF5U3aO
6+c42qcELAgr6i0aCsK1LQjH+SOREhtRQgpDF17IdYAxdYUTyinqrYJnoo5kQXeC/4
7+QoZIsDi1empas7pXwrDlVYtjNK2TOht6dWUnffc2oTUCNY+ATZ6Nsu4zItEM7yQo
8+/QIDAQAB
9+-----END PUBLIC KEY-----
tests/vaultwarden/run.shnew file+13
@@ -0,0 +1,13 @@
1+#!/bin/sh
2+
3+set -e
4+
5+cd "$(dirname "$0")"
6+
7+tmp="$(mktemp -d)"
8+
9+cp -r data .env "$tmp"
10+
11+cd "$tmp"
12+
13+nix run github:NixOS/nixpkgs?rev=d9a1414346059619d9e13ab93e749bbb82e5252a#vaultwarden
vanilladeleted−1

Submodule changed.

appname.apparmor → warden.apparmorrenamed+1−1
@@ -5,5 +5,5 @@
55 "audio",
66 "networking"
77 ],
8− "policy_version": 16.04
8+ "policy_version": 20.04
99}
appname.desktop → warden.desktoprenamed

The content is the same.