on master help sign in
.github/FUNDING.ymlnew file+1
since splitsnapshots
unifiedsplit
@@ -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
7.spago
7.spago
8output
8output
9.psc-ide-port
9.psc-ide-port
10−backend.js
11elm.js
10elm.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>
2
4
3−A short description of your app
5+<h1 align="center">Warden</h1>
4
6
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>
6
10
7−Copyright (C) 2022 Your FullName
8
11
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
12+<p align="center">
10−by the Free Software Foundation.
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>
11
19
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).
13
21
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";
1+import { StateFactory } from "../../bw/factories/stateFactory";
2−import { GlobalState } from "../../deps/bw/libs/shared/dist/src/models/domain/globalState";
2+import { GlobalState } from "../../bw/models/domain/global-state";
3−import { ApiService } from "../../deps/bw/libs/shared/dist/src/services/api.service";
3+import { ApiService } from "../../bw/services/api.service";
4−import { AppIdService } from "../../deps/bw/libs/shared/dist/src/services/appId.service";
4+import { AppIdService } from "../../bw/services/appId.service";
5−import { ConsoleLogService } from "../../deps/bw/libs/shared/dist/src/services/consoleLog.service";
5+import { ConsoleLogService } from "../../bw/services/consoleLog.service";
6−import { EnvironmentService } from "../../deps/bw/libs/shared/dist/src/services/environment.service";
6+import { EnvironmentService } from "../../bw/services/environment.service";
7−import { StateMigrationService } from "../../deps/bw/libs/shared/dist/src/services/stateMigration.service";
7+import { StateMigrationService } from "../../bw/services/stateMigration.service";
8−import { TokenService } from "../../deps/bw/libs/shared/dist/src/services/token.service";
8+import { TokenService } from "../../bw/services/token.service";
9−import { WebCryptoFunctionService } from "../../deps/bw/libs/shared/dist/src/services/webCryptoFunction.service";
9+import { WebCryptoFunctionService } from "../../bw/services/webCryptoFunction.service";
10−import { Account, AccountProfile, AccountTokens } from "../../deps/bw/libs/shared/dist/src/models/domain/account";
10+import { Account, AccountProfile, AccountTokens } from "../../bw/models/domain/account";
11−
11+import {MemoryStorageService} from "../../bw/services/memoryStorage.service"
12+
12−// import BrowserStorageService from "../../../../deps/bw/libs/shared/dist/src/services/browserStorage.service";
13+// import BrowserStorageService from "../../../../bw/services/browserStorage.service";
13−import { CryptoService } from "../../deps/bw/libs/shared/dist/src/services/crypto.service";
14+import { CryptoService } from "../../bw/services/crypto.service";
14−
15+
15−import { I18nService } from "../../deps/bw/libs/shared/dist/src/services/i18n.service";
16+import { I18nService } from "../../bw/services/i18n.service";
16−import { StateService as BaseStateService } from "../../deps/bw/libs/shared/dist/src/services/state.service";
17+import { StateService as BaseStateService } from "../../bw/services/state.service";
17−import { PasswordTokenRequest } from "../../deps/bw/libs/shared/dist/src/models/request/identityToken/passwordTokenRequest";
18+import { PasswordTokenRequest } from "../../bw/models/request/identity-token/password-token.request";
18−import { TokenRequestTwoFactor } from "../../deps/bw/libs/common/src/models/request/identityToken/tokenRequestTwoFactor";
19+import { TokenTwoFactorRequest} from "../../bw/models/request/identity-token/token-two-factor.request";
19−import { CipherRequest } from "../../deps/bw/libs/common/src/models/request/cipherRequest";
20+import { CipherRequest } from "../../bw/vault/models/request/cipher.request";
21+import { CipherCreateRequest } from "../../bw/vault/models/request/cipher-create.request";
20−import { Cipher } from "../../deps/bw/libs/common/src/models/domain/cipher";
22+import { Cipher } from "../../bw/vault/models/domain/cipher";
21−import { CipherData } from "../../deps/bw/libs/common/src/models/data/cipherData";
23+import { CipherData } from "../../bw/vault/models/data/cipher.data";
22−import { PasswordGenerationService } from "../../deps/bw/libs/common/src/services/passwordGeneration.service";
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"
23
27
24function sanitize(obj) {
28function sanitize(obj) {
25 return JSON.parse(JSON.stringify(obj));
29 return JSON.parse(JSON.stringify(obj));
@@ -34,7 +38,7 @@ export function getServices() {
34
38
35 await bg.bootstrap()
39 await bg.bootstrap()
36
40
37− bg.environmentService.setUrls(urls)
41+ await bg.environmentService.setUrls(urls)
38
42
39 if (tokenResponse != null) {
43 if (tokenResponse != null) {
40 const accountInformation = await bg.tokenService.decodeToken(tokenResponse.accessToken);
44 const accountInformation = await bg.tokenService.decodeToken(tokenResponse.accessToken);
@@ -71,18 +75,25 @@ export function getServices() {
71 req.email,
75 req.email,
72 req.masterPasswordHash,
76 req.masterPasswordHash,
73 req.captchaResponse,
77 req.captchaResponse,
78+ new TokenTwoFactorRequest(
79+ req.twoFactor.provider,
80+ req.twoFactor.token,
74− new TokenRequestTwoFactor(req.twoFactor),
81+ req.twoFactor.remember
82+ ),
75 req.device
83 req.device
76 )
84 )
77 ),
85 ),
78 getSync: () => api.getSync().then(sanitize),
86 getSync: () => api.getSync().then(sanitize),
79− postCipher: (cipher) => api.postCipher(makeCipherRequest(cipher)),
87+ postCipherCreate: (cipher) => api.postCipherCreate(makeCipherCreateRequest(cipher)),
80 putCipher: (cipher) => api.putCipher(cipher.id, makeCipherRequest(cipher)),
88 putCipher: (cipher) => api.putCipher(cipher.id, makeCipherRequest(cipher)),
89+ deleteCipher: (id) => api.deleteCipher(id),
90+ postTwoFactorEmail: (req) => api.postTwoFactorEmail(req),
81 }
91 }
82 },
92 },
83 crypto: bg.cryptoService,
93 crypto: bg.cryptoService,
84 cryptoFunctions: bg.cryptoFunctionService,
94 cryptoFunctions: bg.cryptoFunctionService,
85− passwordGeneration: bg.passwordGenerationService
95+ passwordGeneration: bg.passwordGenerationService,
96+ totpService: bg.totpService,
86 }
97 }
87}
98}
88
99
@@ -90,6 +101,10 @@ function makeCipherRequest(cipher) {
90 return new CipherRequest(new Cipher(new CipherData(cipher)))
101 return new CipherRequest(new Cipher(new CipherData(cipher)))
91}
102}
92
103
104+function makeCipherCreateRequest(cipher) {
105+ return new CipherCreateRequest(new Cipher(new CipherData(cipher)))
106+}
107+
93class MainBackground {
108class MainBackground {
94 // messagingService; //: MessagingServiceAbstraction;
109 // messagingService; //: MessagingServiceAbstraction;
95 // storageService; //: StorageServiceAbstraction;
110 // storageService; //: StorageServiceAbstraction;
@@ -118,6 +133,7 @@ class MainBackground {
118 new StateFactory(GlobalState, Account)
133 new StateFactory(GlobalState, Account)
119 );
134 );
120 this.stateService = new StateService(
135 this.stateService = new StateService(
136+ this.storageService,
121 this.storageService,
137 this.storageService,
122 this.storageService,
138 this.storageService,
123 this.logService,
139 this.logService,
@@ -148,8 +164,13 @@ class MainBackground {
148 );
164 );
149 this.i18nService = new I18nService("en");
165 this.i18nService = new I18nService("en");
150 this.cryptoFunctionService = new WebCryptoFunctionService(window);
166 this.cryptoFunctionService = new WebCryptoFunctionService(window);
167+ this.encryptService = new EncryptServiceImplementation(
168+ this.cryptoFunctionService,
169+ this.logService
170+ );
151 this.cryptoService = new BrowserCryptoService(
171 this.cryptoService = new BrowserCryptoService(
152 this.cryptoFunctionService,
172 this.cryptoFunctionService,
173+ this.encryptService,
153 this.platformUtilsService,
174 this.platformUtilsService,
154 this.logService,
175 this.logService,
155 this.stateService
176 this.stateService
@@ -169,6 +190,11 @@ class MainBackground {
169 this.policyService,
190 this.policyService,
170 this.stateService
191 this.stateService
171 );
192 );
193+ this.totpService = new TotpService(
194+ this.cryptoFunctionService,
195+ this.logService,
196+ this.stateService
197+ );
172 }
198 }
173
199
174 async bootstrap() {
200 async bootstrap() {
@@ -188,34 +214,6 @@ class MainBackground {
188 }
214 }
189}
215}
190
216
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−
219class BrowserCryptoService extends CryptoService {
217class BrowserCryptoService extends CryptoService {
220 async retrieveKeyFromStorage(keySuffix) {
218 async retrieveKeyFromStorage(keySuffix) {
221 if (keySuffix === "biometric") {
219 if (keySuffix === "biometric") {
@@ -296,7 +294,7 @@ class BrowserPlatformUtilsService {
296 saveFile(win, blobData, blobOptions, fileName) {
294 saveFile(win, blobData, blobOptions, fileName) {
297 // BrowserApi.downloadFile(win, blobData, blobOptions, fileName);
295 // BrowserApi.downloadFile(win, blobData, blobOptions, fileName);
298 }
296 }
299− getApplicationVersion() {
297+ getApplicationVersionNumber() {
300 // return Promise.resolve(BrowserApi.getApplicationVersion());
298 // return Promise.resolve(BrowserApi.getApplicationVersion());
301 }
299 }
302 supportsWebAuthn(win) {
300 supportsWebAuthn(win) {
backend/BW.purs+37−31
@@ -17,24 +17,31 @@ import Effect (Effect)
17import Effect.Unsafe (unsafePerformEffect)
17import Effect.Unsafe (unsafePerformEffect)
18import Untagged.Union (type (|+|))
18import Untagged.Union (type (|+|))
19
19
20+type TotpService =
21+ { getCode :: String -> Promise String
22+ , getTimeInterval :: String -> Int
23+ }
24+
20−type CryptoService
25+type CryptoService =
21− = { makeKey :: Fn4 Password String KDF Int (Promise SymmetricCryptoKey)
26+ { makeKey :: Fn4 Password String KDF KdfConfig (Promise SymmetricCryptoKey)
22− , hashPassword :: Fn3 Password SymmetricCryptoKey (JNullable HashPurpose) (Promise StringHash)
27+ , hashPassword :: Fn3 Password SymmetricCryptoKey (JNullable HashPurpose) (Promise StringHash)
23− , decryptToUtf8 :: Fn2 EncString SymmetricCryptoKey (Promise String)
28+ , decryptToUtf8 :: Fn2 EncString SymmetricCryptoKey (Promise String)
24− , decryptToBytes :: Fn2 EncString SymmetricCryptoKey (Promise ArrayBuffer)
29+ , decryptToBytes :: Fn2 EncString SymmetricCryptoKey (Promise ArrayBuffer)
25− , encrypt :: Fn2 String SymmetricCryptoKey (Promise EncString)
30+ , encrypt :: Fn2 String SymmetricCryptoKey (Promise EncString)
26− , stretchKey :: SymmetricCryptoKey -> Promise SymmetricCryptoKey
31+ , stretchKey :: SymmetricCryptoKey -> Promise SymmetricCryptoKey
27− }
32+ }
28−
33+
29−type ApiService
34+type ApiService =
30− = { postPrelogin :: PreloginRequest -> Promise PreloginResponse
35+ { postPrelogin :: PreloginRequest -> Promise PreloginResponse
31− , getProfile :: Unit -> Promise ProfileResponse
36+ , getProfile :: Unit -> Promise ProfileResponse
32− -- TODO: There can really be more request and response types. Maybe handle this later.
37+ -- TODO: There can really be more request and response types. Maybe handle this later.
33− , postIdentityToken :: PasswordTokenRequest -> Promise (IdentityCaptchaResponse |+| IdentityTokenResponse)
38+ , postIdentityToken :: PasswordTokenRequest -> Promise (IdentityCaptchaResponse |+| IdentityTwoFactorResponse |+| IdentityTokenResponse)
34− , getSync :: Unit -> Promise SyncResponse
39+ , getSync :: Unit -> Promise SyncResponse
35− , postCipher :: CipherResponse -> Promise CipherResponse
40+ , postCipherCreate :: CipherResponse -> Promise CipherResponse
36− , putCipher :: CipherResponse -> Promise CipherResponse
41+ , putCipher :: CipherResponse -> Promise CipherResponse
42+ , deleteCipher :: String -> Promise Unit
43+ , postTwoFactorEmail :: TwoFactorEmailRequest -> Promise Unit
37− }
44+ }
38
45
39type PasswordGeneration =
46type PasswordGeneration =
40 { generatePassword :: Bridge.PasswordGeneratorConfig -> Promise String
47 { generatePassword :: Bridge.PasswordGeneratorConfig -> Promise String
@@ -48,15 +55,13 @@ cryptoFunctionsTypeSha512 = "sha512" :: CryptoFunctionsType
48
55
49cryptoFunctionsTypeMd5 = "md5" :: CryptoFunctionsType
56cryptoFunctionsTypeMd5 = "md5" :: CryptoFunctionsType
50
57
51−type CryptoFunctionsType
58+type CryptoFunctionsType = String
52− = String
53
59
54−type CryptoFunctions
60+type CryptoFunctions =
55− = { hash :: Fn2 String CryptoFunctionsType (Promise Hash)
61+ { hash :: Fn2 String CryptoFunctionsType (Promise Hash)
56− }
62+ }
57
63
58−newtype Hash
59− = Hash (ArrayBuffer)
64+newtype Hash = Hash (ArrayBuffer)
60
65
61hashToArray :: Hash -> Array Int
66hashToArray :: Hash -> Array Int
62hashToArray (Hash buf) =
67hashToArray (Hash buf) =
@@ -79,11 +84,12 @@ instance EncodeJson Hash where
79instance DecodeJson Hash where
84instance DecodeJson Hash where
80 decodeJson = decodeJson >>> map arrayToHash
85 decodeJson = decodeJson >>> map arrayToHash
81
86
82−type Services
87+type Services =
83− = { crypto :: CryptoService
88+ { crypto :: CryptoService
84− , getApi :: Urls -> JNullable IdentityTokenResponse -> Promise ApiService
89+ , getApi :: Urls -> JNullable IdentityTokenResponse -> Promise ApiService
85− , cryptoFunctions :: CryptoFunctions
90+ , cryptoFunctions :: CryptoFunctions
86− , passwordGeneration :: PasswordGeneration
91+ , passwordGeneration :: PasswordGeneration
92+ , totpService :: TotpService
87− }
93+ }
88
94
89foreign import getServices :: Effect Services
95foreign import getServices :: Effect Services
backend/BW/Logic.purs+317−237
@@ -3,6 +3,7 @@ module BW.Logic where
3import BW
3import BW
4import BW.Types
4import BW.Types
5import Prelude
5import Prelude
6+
6import Bridge as Bridge
7import Bridge as Bridge
7import Control.Monad.Error.Class (throwError)
8import Control.Monad.Error.Class (throwError)
8import Control.Promise (Promise)
9import Control.Promise (Promise)
@@ -12,8 +13,9 @@ import Data.Function.Uncurried (runFn2, runFn3, runFn4)
12import Data.JNullable (JNullable, fromJNullable, jnull, nullify)
13import Data.JNullable (JNullable, fromJNullable, jnull, nullify)
13import Data.JNullable as JNullable
14import Data.JNullable as JNullable
14import Data.JOpt (JOpt(..), fromJOpt)
15import Data.JOpt (JOpt(..), fromJOpt)
15−import Data.Maybe (Maybe(..), fromMaybe)
16+import Data.Maybe (Maybe(..), fromMaybe, maybe)
16import Data.Newtype (class Newtype, unwrap, wrap)
17import Data.Newtype (class Newtype, unwrap, wrap)
18+import Data.Number.Format as Number
17import Data.String as String
19import Data.String as String
18import Data.SymmetricCryptoKey (SymmetricCryptoKey)
20import Data.SymmetricCryptoKey (SymmetricCryptoKey)
19import Data.SymmetricCryptoKey as SymmetricCryptoKey
21import Data.SymmetricCryptoKey as SymmetricCryptoKey
@@ -25,43 +27,51 @@ import Effect.Aff.Class (class MonadAff, liftAff)
25import Effect.Class (liftEffect)
27import Effect.Class (liftEffect)
26import Effect.Exception (error)
28import Effect.Exception (error)
27import Effect.Exception as Exc
29import Effect.Exception as Exc
30+import Effect.Random (random)
31+import Localstorage as Storage
28import Run (Run)
32import Run (Run)
29import Run.Reader (Reader, askAt)
33import Run.Reader (Reader, askAt)
34+import Storage (DeviceIDKey(..))
30import Type.Prelude (Proxy(..))
35import Type.Prelude (Proxy(..))
31import Untagged.Union (type (|+|))
36import Untagged.Union (type (|+|))
37+import Web.Storage.Storage (Storage)
32
38
33-- | Creates the master key
39-- | Creates the master key
34−makePreloginKey ::
40+makePreloginKey
35− forall r.
41+ :: forall r
36− PreloginResponse ->
42+ . PreloginResponse
37− Email ->
43+ -> Email
38− Password ->
44+ -> Password
39− Run
45+ -> Run
40− ( crypto ::
41− Reader CryptoService
46+ ( crypto :: Reader CryptoService
42− , effect :: Effect
47+ , effect :: Effect
43− , aff :: Aff
48+ , aff :: Aff
44− | r
49+ | r
45− )
50+ )
46− SymmetricCryptoKey
51+ SymmetricCryptoKey
47−makePreloginKey { kdf, kdfIterations } (Email email') password = do
52+makePreloginKey { kdf, kdfIterations, kdfMemory, kdfParallelism } (Email email') password = do
48 crypto <- askAt (Proxy :: _ "crypto")
53 crypto <- askAt (Proxy :: _ "crypto")
49 let
54 let
50 email = (String.trim >>> String.toLower) email'
55 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+ }
52
61
53−makeDecryptionKey ::
62+makeDecryptionKey
54− forall r.
63+ :: forall r
64+ .
55 -- | Matser key (prelogin key)
65 -- | Matser key (prelogin key)
56− SymmetricCryptoKey ->
66+ SymmetricCryptoKey
57− EncryptedString ->
67+ -> EncryptedString
58− Run
68+ -> Run
59− ( crypto :: Reader CryptoService
69+ ( crypto :: Reader CryptoService
60− , effect :: Effect
70+ , effect :: Effect
61− , aff :: Aff
71+ , aff :: Aff
62− | r
72+ | r
63− )
73+ )
64− SymmetricCryptoKey
74+ SymmetricCryptoKey
65makeDecryptionKey masterKey str = do
75makeDecryptionKey masterKey str = do
66 crypto <- askAt (Proxy :: _ "crypto")
76 crypto <- askAt (Proxy :: _ "crypto")
67 let
77 let
@@ -78,107 +88,145 @@ makeDecryptionKey masterKey str = do
78 liftEffect $ Exc.throw $ "Unsupported key encryption type: " <> show encType
88 liftEffect $ Exc.throw $ "Unsupported key encryption type: " <> show encType
79 pure $ SymmetricCryptoKey.fromArrayBuffer key
89 pure $ SymmetricCryptoKey.fromArrayBuffer key
80
90
81−getLogInRequestToken ::
91+getLogInRequestToken
82− forall r.
92+ :: forall r
83− PreloginResponse ->
93+ . { prelogin :: PreloginResponse
84− Email ->
94+ , email :: Email
85− Password ->
95+ , password :: Password
86− Maybe String ->
96+ , captchaResponse :: Maybe String
97+ , secondFactor :: Bridge.Cmd_Login_secondFactor_Maybe
98+ }
87− Run
99+ -> Run
88− ( api :: Reader ApiService
100+ ( api :: Reader ApiService
89− , crypto :: Reader CryptoService
101+ , crypto :: Reader CryptoService
90− , effect :: Effect
102+ , effect :: Effect
91− , aff :: Aff
103+ , aff :: Aff
104+ , storage :: Reader Storage
92− | r
105+ | r
93− )
106+ )
94− (IdentityCaptchaResponse |+| IdentityTokenResponse)
107+ (IdentityCaptchaResponse |+| IdentityTwoFactorResponse |+| IdentityTokenResponse)
95−getLogInRequestToken prelogin email password captchaResponse = do
108+getLogInRequestToken
109+ { prelogin
110+ , email
111+ , password
112+ , captchaResponse
113+ , secondFactor: Bridge.Cmd_Login_secondFactor_Maybe secondFactor
114+ } = do
96 key <- makePreloginKey prelogin email password
115 key <- makePreloginKey prelogin email password
97 crypto <- askAt (Proxy :: _ "crypto")
116 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
99 _localHashedPassword <-
128 _localHashedPassword <-
100 liftPromise
129 liftPromise
101 $ runFn3 crypto.hashPassword password key (nullify hashPurposeLocalAuthorization)
130 $ runFn3 crypto.hashPassword password key (nullify hashPurposeLocalAuthorization)
102 StringHash hashedPassword <- liftPromise $ runFn3 crypto.hashPassword password key jnull
131 StringHash hashedPassword <- liftPromise $ runFn3 crypto.hashPassword password key jnull
132+ deviceId <- getDeviceID
133+ api :: ApiService <- askAt (Proxy :: _ "api")
103 liftPromise
134 liftPromise
104 $ api.postIdentityToken
135 $ api.postIdentityToken
105 { email: email
136 { email: email
106 , masterPasswordHash: hashedPassword
137 , masterPasswordHash: hashedPassword
107 , captchaResponse: fromMaybe "" captchaResponse
138 , captchaResponse: fromMaybe "" captchaResponse
108− , twoFactor:
139+ , twoFactor
109− { provider: twoFactorProviderTypeEmail
110− , token: ""
111− , remember: false
112− }
113 , device:
140 , device:
114 { type: deviceTypeUnknownBrowser
141 { type: deviceTypeUnknownBrowser
115− , name: "Temporary device name"
142+ , name: "Ubuntu Touch"
116− , identifier: "42"
143+ , identifier: deviceId
117 , pushToken: jnull
144 , pushToken: jnull
118 }
145 }
119 }
146 }
120
147
121−hashPassword ::
148+bwPasswordStringHash
122− forall r.
149+ :: forall r
123− Password ->
150+ . PreloginResponse
151+ -> Email
152+ -> Password
124− Run
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
125− ( cryptoFunctions :: Reader CryptoFunctions
172+ ( cryptoFunctions :: Reader CryptoFunctions
126− , aff :: Aff
173+ , aff :: Aff
127− , effect :: Effect
174+ , effect :: Effect
128− | r
175+ | r
129− )
176+ )
130− Hash
177+ Hash
131hashPassword (Password password) = do
178hashPassword (Password password) = do
132 cryptoFunctions <- askAt (Proxy :: _ "cryptoFunctions")
179 cryptoFunctions <- askAt (Proxy :: _ "cryptoFunctions")
133 liftPromise $ runFn2 cryptoFunctions.hash password cryptoFunctionsTypeSha512
180 liftPromise $ runFn2 cryptoFunctions.hash password cryptoFunctionsTypeSha512
134
181
135−decrypt ::
182+decrypt
136− forall r.
183+ :: forall r
137− EncryptedString ->
184+ . EncryptedString
138− Run
185+ -> Run
139− ( key :: Reader SymmetricCryptoKey
186+ ( key :: Reader SymmetricCryptoKey
140− , crypto :: Reader CryptoService
187+ , crypto :: Reader CryptoService
141− , aff :: Aff
188+ , aff :: Aff
142− , effect :: Effect
189+ , effect :: Effect
143− | r
190+ | r
144− )
191+ )
145− String
192+ String
146decrypt input = do
193decrypt input = do
147 crypto <- askAt (Proxy :: _ "crypto")
194 crypto <- askAt (Proxy :: _ "crypto")
148 key <- askAt (Proxy :: _ "key")
195 key <- askAt (Proxy :: _ "key")
149 liftPromise $ runFn2 crypto.decryptToUtf8 (EncString.fromString input) key
196 liftPromise $ runFn2 crypto.decryptToUtf8 (EncString.fromString input) key
150
197
151−encrypt ::
198+encrypt
152− forall r.
199+ :: forall r
153− String ->
200+ . String
154− Run
201+ -> Run
155− ( key :: Reader SymmetricCryptoKey
202+ ( key :: Reader SymmetricCryptoKey
156− , crypto :: Reader CryptoService
203+ , crypto :: Reader CryptoService
157− , aff :: Aff
204+ , aff :: Aff
158− , effect :: Effect
205+ , effect :: Effect
159− | r
206+ | r
160− )
207+ )
161− EncryptedString
208+ EncryptedString
162encrypt input = do
209encrypt input = do
163 crypto <- askAt (Proxy :: _ "crypto")
210 crypto <- askAt (Proxy :: _ "crypto")
164 key <- askAt (Proxy :: _ "key")
211 key <- askAt (Proxy :: _ "key")
165 map EncString.toString $ liftPromise $ runFn2 crypto.encrypt input key
212 map EncString.toString $ liftPromise $ runFn2 crypto.encrypt input key
166
213
167−liftPromise ∷ forall m a. MonadAff m ⇒ Promise a → m a
214+liftPromise :: forall m a. MonadAff m => Promise a -> m a
168liftPromise = liftAff <<< Promise.toAff
215liftPromise = liftAff <<< Promise.toAff
169
216
170−encodeCipher ::
217+encodeCipher
171− forall r.
218+ :: forall r
172− Bridge.FullCipher ->
219+ . Bridge.FullCipher
173− Run
220+ -> Run
174− ( key :: Reader SymmetricCryptoKey
221+ ( key :: Reader SymmetricCryptoKey
175− , crypto :: Reader CryptoService
222+ , crypto :: Reader CryptoService
176− , aff :: Aff
223+ , aff :: Aff
177− , effect :: Effect
224+ , effect :: Effect
178− | r
225+ | r
179− )
226+ )
180− CipherResponse
227+ CipherResponse
228+encodeCipher
181−encodeCipher (Bridge.FullCipher { name, cipher, id, favorite, reprompt }) = do
229+ (Bridge.FullCipher { name, cipher, id, favorite, reprompt, collectionIds }) = do
182 let
230 let
183 f :: CipherResponse -> Run _ CipherResponse
231 f :: CipherResponse -> Run _ CipherResponse
184 f x = case cipher of
232 f x = case cipher of
@@ -192,14 +240,14 @@ encodeCipher (Bridge.FullCipher { name, cipher, id, favorite, reprompt }) = do
192 pure
240 pure
193 x
241 x
194 { card =
242 { card =
195− nullify
243+ nullify
196− { cardholderName
244+ { cardholderName
197− , brand
245+ , brand
198− , number
246+ , number
199− , expMonth
247+ , expMonth
200− , expYear
248+ , expYear
201− , code
249+ , code
202− }
250+ }
203 , type = cipherTypeCard
251 , type = cipherTypeCard
204 }
252 }
205 Bridge.IdentityCipher (Bridge.Cipher_IdentityCipher identity) -> do
253 Bridge.IdentityCipher (Bridge.Cipher_IdentityCipher identity) -> do
@@ -224,26 +272,26 @@ encodeCipher (Bridge.FullCipher { name, cipher, id, favorite, reprompt }) = do
224 pure
272 pure
225 x
273 x
226 { identity =
274 { identity =
227− nullify
275+ nullify
228− { address1
276+ { address1
229− , address2
277+ , address2
230− , address3
278+ , address3
231− , city
279+ , city
232− , company
280+ , company
233− , country
281+ , country
234− , email
282+ , email
235− , firstName
283+ , firstName
236− , lastName
284+ , lastName
237− , licenseNumber
285+ , licenseNumber
238− , middleName
286+ , middleName
239− , passportNumber
287+ , passportNumber
240− , phone
288+ , phone
241− , postalCode
289+ , postalCode
242− , ssn
290+ , ssn
243− , state
291+ , state
244− , title
292+ , title
245− , username
293+ , username
246− }
294+ }
247 , type = cipherTypeIdentity
295 , type = cipherTypeIdentity
248 }
296 }
249 Bridge.LoginCipher (Bridge.Cipher_LoginCipher login) -> do
297 Bridge.LoginCipher (Bridge.Cipher_LoginCipher login) -> do
@@ -251,22 +299,23 @@ encodeCipher (Bridge.FullCipher { name, cipher, id, favorite, reprompt }) = do
251 uris <-
299 uris <-
252 traverse
300 traverse
253 ( \uri -> do
301 ( \uri -> do
254− uriEnc <- encrypt uri
302+ uriEnc <- nullify <$> encrypt uri
255 pure { uri: uriEnc, match: jnull }
303 pure { uri: uriEnc, match: jnull }
256 )
304 )
257 (unwrap login.uris)
305 (unwrap login.uris)
258 username <- encryptNullable login.username
306 username <- encryptNullable login.username
307+ totp <- encryptNullable login.totp
259 pure
308 pure
260 x
309 x
261 { login =
310 { login =
262− nullify
311+ nullify
263− { password
312+ { password
264− , uris: JOpt $ opt uris
313+ , uris: JOpt $ opt uris
265− , username
314+ , username
266− , passwordRevisionDate: jnull
315+ , passwordRevisionDate: jnull
267− , totp: jnull
316+ , totp: totp
268− , autofillOnPageLoad: JOpt undefined
317+ , autofillOnPageLoad: JOpt undefined
269− }
318+ }
270 , type = cipherTypeLogin
319 , type = cipherTypeLogin
271 }
320 }
272 Bridge.NoteCipher note -> do
321 Bridge.NoteCipher note -> do
@@ -296,137 +345,168 @@ encodeCipher (Bridge.FullCipher { name, cipher, id, favorite, reprompt }) = do
296 , revisionDate: jnull
345 , revisionDate: jnull
297 , attachments: jnull
346 , attachments: jnull
298 , passwordHistory: jnull
347 , passwordHistory: jnull
299− , collectionIds: jnull
348+ , collectionIds: nullify $ unwrap collectionIds
300 , deletedDate: jnull
349 , deletedDate: jnull
301 , reprompt
350 , reprompt
302 }
351 }
303
352
304−decodeCipher ::
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
305− forall r.
361+ :: forall r
306− CipherResponse ->
362+ . CipherResponse
307− Run
363+ -> Run
308− ( key :: Reader SymmetricCryptoKey
364+ ( key :: Reader SymmetricCryptoKey
309− , crypto :: Reader CryptoService
365+ , crypto :: Reader CryptoService
310− , aff :: Aff
366+ , aff :: Aff
311− , effect :: Effect
367+ , effect :: Effect
312− | r
368+ | r
313− )
369+ )
314− Bridge.FullCipher
370+ (Maybe Bridge.FullCipher)
371+decodeCipher cipher | not (supportedCipher cipher) =
372+ pure $ Nothing
315decodeCipher cipher = do
373decodeCipher cipher = do
316 name <- decrypt cipher.name
374 name <- decrypt cipher.name
317 cipherType <- case cipher.type of
375 cipherType <- case cipher.type of
318 n
376 n
319 | cipherTypeSecureNote == n -> do
377 | cipherTypeSecureNote == n -> do
320− note <- fromJNullable "" <$> (traverse decrypt cipher.notes)
378+ note <- fromJNullable "" <$> (traverse decrypt cipher.notes)
321− pure $ Bridge.NoteCipher note
379+ pure $ Bridge.NoteCipher note
322 n
380 n
323 | cipherTypeLogin == n -> do
381 | cipherTypeLogin == n -> do
324− case JNullable.toMaybe cipher.login of
382+ case JNullable.toMaybe cipher.login of
325− Nothing -> liftEffect $ throwError $ error "Login data is missing"
383+ Nothing -> liftEffect $ throwError $ error "Login data is missing"
326− Just login -> do
384+ Just login -> do
327− username <- decryptNullable login.username
385+ username <- decryptNullable login.username
328− password <- decryptNullable login.password
386+ password <- decryptNullable login.password
387+ totp <- decryptNullable login.totp
388+ uris <- fromJOpt [] <$>
329− uris <- fromJOpt [] <$> ((traverse >>> traverse) (_.uri >>> decrypt) login.uris)
389+ ((traverse >>> traverse) (_.uri >>> JNullable.toMaybe >>> maybe (pure "") decrypt) login.uris)
330− pure $ Bridge.LoginCipher
390+ pure $ Bridge.LoginCipher
331− $ Bridge.Cipher_LoginCipher
391+ $ Bridge.Cipher_LoginCipher
332− { username
392+ { username
333− , password
393+ , password
334− , uris: wrap uris
394+ , uris: wrap uris
395+ , totp
335− }
396+ }
336 n
397 n
337 | cipherTypeCard == n -> do
398 | cipherTypeCard == n -> do
338− case JNullable.toMaybe cipher.card of
399+ case JNullable.toMaybe cipher.card of
339− Nothing -> liftEffect $ throwError $ error "Card data is missing"
400+ Nothing -> liftEffect $ throwError $ error "Card data is missing"
340− Just card -> do
401+ Just card -> do
341− cardholderName <- decryptNullable card.cardholderName
402+ cardholderName <- decryptNullable card.cardholderName
342− number <- decryptNullable card.number
403+ number <- decryptNullable card.number
343− code <- decryptNullable card.code
404+ code <- decryptNullable card.code
344− brand <- decryptNullable card.brand
405+ brand <- decryptNullable card.brand
345− expMonth <- decryptNullable card.expMonth
406+ expMonth <- decryptNullable card.expMonth
346− expYear <- decryptNullable card.expYear
407+ expYear <- decryptNullable card.expYear
347− pure $ Bridge.CardCipher
408+ pure $ Bridge.CardCipher
348− $ Bridge.Cipher_CardCipher
409+ $ Bridge.Cipher_CardCipher
349− { brand, cardholderName, code, expMonth, expYear, number }
410+ { brand, cardholderName, code, expMonth, expYear, number }
350 n
411 n
351 | cipherTypeIdentity == n -> do
412 | cipherTypeIdentity == n -> do
352− case JNullable.toMaybe cipher.identity of
413+ case JNullable.toMaybe cipher.identity of
353− Nothing -> liftEffect $ throwError $ error "Identity data is missing"
414+ Nothing -> liftEffect $ throwError $ error "Identity data is missing"
354− Just identity -> do
415+ Just identity -> do
355− firstName <- decryptNullable identity.firstName
416+ firstName <- decryptNullable identity.firstName
356− middleName <- decryptNullable identity.middleName
417+ middleName <- decryptNullable identity.middleName
357− lastName <- decryptNullable identity.lastName
418+ lastName <- decryptNullable identity.lastName
358− address1 <- decryptNullable identity.address1
419+ address1 <- decryptNullable identity.address1
359− address2 <- decryptNullable identity.address2
420+ address2 <- decryptNullable identity.address2
360− address3 <- decryptNullable identity.address3
421+ address3 <- decryptNullable identity.address3
361− city <- decryptNullable identity.city
422+ city <- decryptNullable identity.city
362− company <- decryptNullable identity.company
423+ company <- decryptNullable identity.company
363− country <- decryptNullable identity.country
424+ country <- decryptNullable identity.country
364− email <- decryptNullable identity.email
425+ email <- decryptNullable identity.email
365− licenseNumber <- decryptNullable identity.licenseNumber
426+ licenseNumber <- decryptNullable identity.licenseNumber
366− passportNumber <- decryptNullable identity.passportNumber
427+ passportNumber <- decryptNullable identity.passportNumber
367− phone <- decryptNullable identity.phone
428+ phone <- decryptNullable identity.phone
368− postalCode <- decryptNullable identity.postalCode
429+ postalCode <- decryptNullable identity.postalCode
369− ssn <- decryptNullable identity.ssn
430+ ssn <- decryptNullable identity.ssn
370− state <- decryptNullable identity.state
431+ state <- decryptNullable identity.state
371− title <- decryptNullable identity.title
432+ title <- decryptNullable identity.title
372− username <- decryptNullable identity.username
433+ username <- decryptNullable identity.username
373− pure $ Bridge.IdentityCipher
434+ pure $ Bridge.IdentityCipher
374− $ wrap
435+ $ wrap
375− { firstName
436+ { firstName
376− , middleName
437+ , middleName
377− , lastName
438+ , lastName
378− , address1
439+ , address1
379− , address2
440+ , address2
380− , address3
441+ , address3
381− , city
442+ , city
382− , company
443+ , company
383− , country
444+ , country
384− , email
445+ , email
385− , licenseNumber
446+ , licenseNumber
386− , passportNumber
447+ , passportNumber
387− , phone
448+ , phone
388− , postalCode
449+ , postalCode
389− , ssn
450+ , ssn
390− , state
451+ , state
391− , title
452+ , title
392− , username
453+ , username
393− }
454+ }
394 n -> liftEffect $ throwError $ error $ "Unsupported cipher type: " <> show n
455 n -> liftEffect $ throwError $ error $ "Unsupported cipher type: " <> show n
395− pure
456+ pure $ Just
396 $ wrap
457 $ wrap
397 { cipher: cipherType
458 { cipher: cipherType
398 , name
459 , name
399 , id: cipher.id
460 , id: cipher.id
400 , favorite: cipher.favorite
461 , favorite: cipher.favorite
401 , reprompt: cipher.reprompt
462 , reprompt: cipher.reprompt
463+ , collectionIds: wrap <<< fromJNullable [] $ cipher.collectionIds
402 }
464 }
403
465
404−decryptNullable ::
466+decryptNullable
405− forall x r.
467+ :: forall x r
406− Newtype x (Maybe String) =>
468+ . Newtype x (Maybe String)
407− JNullable EncryptedString ->
469+ => JNullable EncryptedString
408− Run
470+ -> Run
409− ( key :: Reader SymmetricCryptoKey
471+ ( key :: Reader SymmetricCryptoKey
410− , crypto :: Reader CryptoService
472+ , crypto :: Reader CryptoService
411− , aff :: Aff
473+ , aff :: Aff
412− , effect :: Effect
474+ , effect :: Effect
413− | r
475+ | r
414− )
476+ )
415− x
477+ x
416decryptNullable x = wrap <<< fromJNullable Nothing <<< map Just <$> (traverse decrypt x)
478decryptNullable x = wrap <<< fromJNullable Nothing <<< map Just <$> (traverse decrypt x)
417
479
418−encryptNullable ::
480+encryptNullable
419− forall x r.
481+ :: forall x r
420− Newtype x (Maybe String) =>
482+ . Newtype x (Maybe String)
421− x ->
483+ => x
422− Run
484+ -> Run
423− ( key :: Reader SymmetricCryptoKey
485+ ( key :: Reader SymmetricCryptoKey
424− , crypto :: Reader CryptoService
486+ , crypto :: Reader CryptoService
425− , aff :: Aff
487+ , aff :: Aff
426− , effect :: Effect
488+ , effect :: Effect
427− | r
489+ | r
428− )
490+ )
429− (JNullable EncryptedString)
491+ (JNullable EncryptedString)
430encryptNullable x = case unwrap x of
492encryptNullable x = case unwrap x of
431 Nothing -> pure jnull
493 Nothing -> pure jnull
432 Just y -> nullify <$> encrypt y
494 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
2
2
3import Prelude
3import Prelude
4
4
5+import Bridge as Bridge
5import Data.Argonaut (class DecodeJson, class EncodeJson)
6import Data.Argonaut (class DecodeJson, class EncodeJson)
7+import Data.Either (Either)
8+import Data.Either as Either
9+import Data.Int as Int
6import Data.JNullable (JNullable)
10import Data.JNullable (JNullable)
7import Data.JOpt (JOpt)
11import Data.JOpt (JOpt)
12+import Data.Maybe (Maybe(..))
13+import Data.Nullable (Nullable)
8import Data.ShowableJson (ShowableJson)
14import Data.ShowableJson (ShowableJson)
9import Data.Timestamp (Timestamp)
15import 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
10
24
11−newtype EncryptedString
25+newtype EncryptedString = EncryptedString String
12− = EncryptedString String
13
26
14derive newtype instance Show EncryptedString
27derive newtype instance Show EncryptedString
15derive newtype instance Ord EncryptedString
28derive newtype instance Ord EncryptedString
@@ -18,78 +31,86 @@ derive newtype instance EncodeJson EncryptedString
18derive newtype instance DecodeJson EncryptedString
31derive newtype instance DecodeJson EncryptedString
19
32
20kdfPBKDF2_SHA256 = 0 :: KDF
33kdfPBKDF2_SHA256 = 0 :: KDF
34+kdfArgon2id = 1 :: KDF
21
35
22−type KDF
36+type KDF = Int
23− = Int
37+
24−
25−type PreloginResponse
38+type PreloginResponse =
26− = { kdf :: KDF
39+ { kdf :: KDF
27− , kdfIterations :: Int
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
28− }
49+ }
29−
50+
30−type PreloginRequest
51+type PreloginRequest =
31− = { email :: String
52+ { email :: Email
32− }
53+ }
33−
54+
34−type ProfileResponse
55+type ProfileResponse =
35− = { id :: String
56+ { id :: String
36− , name :: String
57+ , name :: JNullable String
37− , email :: String
58+ , email :: String
38− , emailVerified :: Boolean
59+ , emailVerified :: Boolean
39− , masterPasswordHint :: JNullable String
60+ , masterPasswordHint :: JNullable String
40− , premiumPersonally :: Boolean
61+ -- , premiumPersonally :: Boolean
41− -- , premiumFromOrganization :: Boolean
62+ -- , premiumFromOrganization :: Boolean
42− , culture :: String
63+ , culture :: JNullable String
43− , twoFactorEnabled :: Boolean
64+ , twoFactorEnabled :: Boolean
44− , key :: EncryptedString
65+ , key :: EncryptedString
45− , privateKey :: EncryptedString
66+ , privateKey :: EncryptedString
46− , securityStamp :: String
67+ , securityStamp :: JNullable String
47− , forcePasswordReset :: Boolean
68+ , forcePasswordReset :: Boolean
48− , usesKeyConnector :: Boolean
69+ , usesKeyConnector :: Boolean
49− , organizations :: Array ProfileOrganizationResponse
70+ -- , organizations :: Array ProfileOrganizationResponse
50− , providers :: Array ProfileProviderResponse
71+ , providers :: Array ProfileProviderResponse
51− , providerOrganizations :: Array ProfileProviderOrganizationResponse
72+ , providerOrganizations :: Array ProfileProviderOrganizationResponse
52− }
73+ }
53−
74+
54−type ProfileOrganizationResponse
75+type ProfileOrganizationResponse =
55− = { id :: String
76+ { id :: String
56− , name :: String
77+ , name :: JNullable String
57− , usePolicies :: Boolean
78+ , usePolicies :: JNullable Boolean
58− , useGroups :: Boolean
79+ , useGroups :: JNullable Boolean
59− , useDirectory :: Boolean
80+ , useDirectory :: JNullable Boolean
60− , useEvents :: Boolean
81+ , useEvents :: JNullable Boolean
61− , useTotp :: Boolean
82+ , useTotp :: JNullable Boolean
62− , use2fa :: Boolean
83+ , use2fa :: JNullable Boolean
63− , useApi :: Boolean
84+ , useApi :: JNullable Boolean
64− , useSso :: Boolean
85+ , useSso :: JNullable Boolean
65− , useKeyConnector :: Boolean
86+ , useKeyConnector :: JNullable Boolean
66− , useResetPassword :: Boolean
87+ , useResetPassword :: JNullable Boolean
67− , selfHost :: Boolean
88+ , selfHost :: JNullable Boolean
68− , usersGetPremium :: Boolean
89+ , usersGetPremium :: JNullable Boolean
69− , seats :: Int
90+ , seats :: JNullable Int
70− , maxCollections :: Int
91+ , maxCollections :: JNullable Int
71− , maxStorageGb :: JNullable Int
92+ , maxStorageGb :: JNullable Int
72− , key :: String
93+ , key :: JNullable String
73− , hasPublicAndPrivateKeys :: Boolean
94+ , hasPublicAndPrivateKeys :: JNullable Boolean
74− , status :: OrganizationUserStatusType
95+ , status :: JNullable OrganizationUserStatusType
75− , type :: OrganizationUserType
96+ , type :: JNullable OrganizationUserType
76− , enabled :: Boolean
97+ , enabled :: JNullable Boolean
77− , ssoBound :: Boolean
98+ , ssoBound :: JNullable Boolean
78− , identifier :: String
99+ , identifier :: JNullable String
79− , permissions :: PermissionsApi
100+ , permissions :: JNullable PermissionsApi
80− , resetPasswordEnrolled :: Boolean
101+ , resetPasswordEnrolled :: JNullable Boolean
81− , userId :: String
102+ , userId :: JNullable String
82− , providerId :: String
103+ , providerId :: JNullable String
83− , providerName :: String
104+ , providerName :: JNullable String
84− , familySponsorshipFriendlyName :: String
105+ , familySponsorshipFriendlyName :: JNullable String
85− , familySponsorshipAvailable :: Boolean
106+ , familySponsorshipAvailable :: JNullable Boolean
86− , planProductType :: ProductType
107+ , planProductType :: JNullable ProductType
87− , keyConnectorEnabled :: Boolean
108+ , keyConnectorEnabled :: JNullable Boolean
88− , keyConnectorUrl :: String
109+ , keyConnectorUrl :: JNullable String
89− , familySponsorshipLastSyncDate :: JNullable Timestamp
110+ , familySponsorshipLastSyncDate :: JNullable Timestamp
90− , familySponsorshipValidUntil :: JNullable Timestamp
111+ , familySponsorshipValidUntil :: JNullable Timestamp
91− , familySponsorshipToDelete :: JNullable Boolean
112+ , familySponsorshipToDelete :: JNullable Boolean
92− }
113+ }
93
114
94organizationUserStatusTypeInvited = 0 :: OrganizationUserStatusType
115organizationUserStatusTypeInvited = 0 :: OrganizationUserStatusType
95
116
@@ -97,8 +118,7 @@ organizationUserStatusTypeAccepted = 0 :: OrganizationUserStatusType
97
118
98organizationUserStatusTypeConfirmed = 0 :: OrganizationUserStatusType
119organizationUserStatusTypeConfirmed = 0 :: OrganizationUserStatusType
99
120
100−type OrganizationUserStatusType
121+type OrganizationUserStatusType = Int
101− = Int
102
122
103organizationUserTypeOwner = 0 :: OrganizationUserType
123organizationUserTypeOwner = 0 :: OrganizationUserType
104
124
@@ -110,27 +130,26 @@ organizationUserTypeManager = 3 :: OrganizationUserType
110
130
111organizationUserTypeCustom = 4 :: OrganizationUserType
131organizationUserTypeCustom = 4 :: OrganizationUserType
112
132
113−type OrganizationUserType
133+type OrganizationUserType = Int
114− = Int
134+
115−
116−type PermissionsApi
135+type PermissionsApi =
117− = { accessEventLogs :: Boolean
136+ { accessEventLogs :: Boolean
118− , accessImportExport :: Boolean
137+ , accessImportExport :: Boolean
119− , accessReports :: Boolean
138+ , accessReports :: Boolean
120− , manageAllCollections :: Boolean
139+ , manageAllCollections :: Boolean
121− , createNewCollections :: Boolean
140+ , createNewCollections :: Boolean
122− , editAnyCollection :: Boolean
141+ , editAnyCollection :: Boolean
123− , deleteAnyCollection :: Boolean
142+ , deleteAnyCollection :: Boolean
124− , manageAssignedCollections :: Boolean
143+ , manageAssignedCollections :: Boolean
125− , editAssignedCollections :: Boolean
144+ , editAssignedCollections :: Boolean
126− , deleteAssignedCollections :: Boolean
145+ , deleteAssignedCollections :: Boolean
127− , manageCiphers :: Boolean
146+ , manageCiphers :: Boolean
128− , manageGroups :: Boolean
147+ , manageGroups :: Boolean
129− , manageSso :: Boolean
148+ , manageSso :: Boolean
130− , managePolicies :: Boolean
149+ , managePolicies :: Boolean
131− , manageUsers :: Boolean
150+ , manageUsers :: Boolean
132− , manageResetPassword :: Boolean
151+ , manageResetPassword :: Boolean
133− }
152+ }
134
153
135productTypeFree = 0 :: ProductType
154productTypeFree = 0 :: ProductType
136
155
@@ -140,20 +159,19 @@ productTypeTeams = 2 :: ProductType
140
159
141productTypeEnterprise = 3 :: ProductType
160productTypeEnterprise = 3 :: ProductType
142
161
143−type ProductType
162+type ProductType = Int
144− = Int
163+
145−
146−type ProfileProviderResponse
164+type ProfileProviderResponse =
147− = { id :: String
165+ { id :: String
148− , name :: String
166+ , name :: JNullable String
149− , key :: String
167+ , key :: JNullable String
150− , status :: ProviderUserStatusType
168+ , status :: JNullable ProviderUserStatusType
151− , type :: ProviderUserType
169+ , type :: JNullable ProviderUserType
152− , enabled :: Boolean
170+ , enabled :: Boolean
153− , permissions :: PermissionsApi
171+ , permissions :: PermissionsApi
154− , userId :: String
172+ , userId :: JNullable String
155− , useEvents :: Boolean
173+ , useEvents :: Boolean
156− }
174+ }
157
175
158providerUserStatusTypeInvited = 0 :: ProviderUserStatusType
176providerUserStatusTypeInvited = 0 :: ProviderUserStatusType
159
177
@@ -161,81 +179,79 @@ providerUserStatusTypeAccepted = 1 :: ProviderUserStatusType
161
179
162providerUserStatusTypeConfirmed = 2 :: ProviderUserStatusType
180providerUserStatusTypeConfirmed = 2 :: ProviderUserStatusType
163
181
164−type ProviderUserStatusType
182+type ProviderUserStatusType = Int
165− = Int
166
183
167providerUserTypeProviderAdmin = 0 :: ProviderUserType
184providerUserTypeProviderAdmin = 0 :: ProviderUserType
168
185
169providerUserTypeServiceUser = 1 :: ProviderUserType
186providerUserTypeServiceUser = 1 :: ProviderUserType
170
187
171−type ProviderUserType
188+type ProviderUserType = Int
172− = Int
189+
173−
174−type ProfileProviderOrganizationResponse
190+type ProfileProviderOrganizationResponse =
175− = { id :: String
191+ { id :: String
176− , name :: String
192+ , name :: JNullable String
177− , usePolicies :: Boolean
193+ , usePolicies :: Boolean
178− , useGroups :: Boolean
194+ , useGroups :: Boolean
179− , useDirectory :: Boolean
195+ , useDirectory :: Boolean
180− , useEvents :: Boolean
196+ , useEvents :: Boolean
181− , useTotp :: Boolean
197+ , useTotp :: Boolean
182− , use2fa :: Boolean
198+ , use2fa :: Boolean
183− , useApi :: Boolean
199+ , useApi :: Boolean
184− , useSso :: Boolean
200+ , useSso :: Boolean
185− , useKeyConnector :: Boolean
201+ , useKeyConnector :: Boolean
186− , useResetPassword :: Boolean
202+ , useResetPassword :: Boolean
187− , selfHost :: Boolean
203+ , selfHost :: Boolean
188− , usersGetPremium :: Boolean
204+ , usersGetPremium :: Boolean
189− , seats :: Int
205+ , seats :: Int
190− , maxCollections :: Int
206+ , maxCollections :: Int
191− , maxStorageGb :: JNullable Int
207+ , maxStorageGb :: JNullable Int
192− , key :: String
208+ , key :: JNullable String
193− , hasPublicAndPrivateKeys :: Boolean
209+ , hasPublicAndPrivateKeys :: Boolean
194− , status :: OrganizationUserStatusType
210+ , status :: OrganizationUserStatusType
195− , type :: OrganizationUserType
211+ , type :: OrganizationUserType
196− , enabled :: Boolean
212+ , enabled :: Boolean
197− , ssoBound :: Boolean
213+ , ssoBound :: Boolean
198− , identifier :: String
214+ , identifier :: JNullable String
199− , permissions :: PermissionsApi
215+ , permissions :: PermissionsApi
200− , resetPasswordEnrolled :: Boolean
216+ , resetPasswordEnrolled :: Boolean
201− , userId :: String
217+ , userId :: JNullable String
202− , providerId :: String
218+ , providerId :: JNullable String
203− , providerName :: String
219+ , providerName :: JNullable String
204− , familySponsorshipFriendlyName :: String
220+ , familySponsorshipFriendlyName :: JNullable String
205− , familySponsorshipAvailable :: Boolean
221+ , familySponsorshipAvailable :: Boolean
206− , planProductType :: ProductType
222+ , planProductType :: ProductType
207− , keyConnectorEnabled :: Boolean
223+ , keyConnectorEnabled :: Boolean
208− , keyConnectorUrl :: String
224+ , keyConnectorUrl :: JNullable String
209− , familySponsorshipLastSyncDate :: JNullable Timestamp
225+ , familySponsorshipLastSyncDate :: JNullable Timestamp
210− , familySponsorshipValidUntil :: JNullable Timestamp
226+ , familySponsorshipValidUntil :: JNullable Timestamp
211− , familySponsorshipToDelete :: JNullable Boolean
227+ , familySponsorshipToDelete :: JNullable Boolean
212− }
228+ }
213−
229+
214−type Urls
230+type Urls =
215− = { base :: JNullable String
231+ { base :: JNullable String
216− , webVault :: JNullable String
232+ , webVault :: JNullable String
217− , api :: JNullable String
233+ , api :: JNullable String
218− , identity :: JNullable String
234+ , identity :: JNullable String
219− , icons :: JNullable String
235+ , icons :: JNullable String
220− , notifications :: JNullable String
236+ , notifications :: JNullable String
221− , events :: JNullable String
237+ , events :: JNullable String
222− , keyConnector :: JNullable String
238+ , keyConnector :: JNullable String
223− }
239+ }
224−
240+
225−type PasswordTokenRequest
241+type PasswordTokenRequest =
226− = { email :: Email
242+ { email :: Email
227− , masterPasswordHash :: String
243+ , masterPasswordHash :: String
228− , captchaResponse :: String
244+ , captchaResponse :: String
229− , twoFactor :: TokenRequestTwoFactor
245+ , twoFactor :: TokenRequestTwoFactor
230− , device :: DeviceRequest
246+ , device :: DeviceRequest
231− }
247+ }
232−
248+
233−type DeviceRequest
249+type DeviceRequest =
234− = { type :: DeviceType
250+ { type :: DeviceType
235− , name :: String
251+ , name :: String
236− , identifier :: String
252+ , identifier :: String
237− , pushToken :: JNullable String
253+ , pushToken :: JNullable String
238− }
254+ }
239
255
240deviceTypeAndroid = 0 :: DeviceType
256deviceTypeAndroid = 0 :: DeviceType
241
257
@@ -279,14 +295,13 @@ deviceTypeVivaldiExtension = 19 :: DeviceType
279
295
280deviceTypeSafariExtension = 20 :: DeviceType
296deviceTypeSafariExtension = 20 :: DeviceType
281
297
282−type DeviceType
298+type DeviceType = Int
283− = Int
284
299
285−type TokenRequestTwoFactor
300+type TokenRequestTwoFactor =
286− = { provider :: TwoFactorProviderType
301+ { provider :: TwoFactorProviderType
287− , token :: String
302+ , token :: String
288− , remember :: Boolean
303+ , remember :: Boolean
289− }
304+ }
290
305
291twoFactorProviderTypeAuthenticator = 0 :: TwoFactorProviderType
306twoFactorProviderTypeAuthenticator = 0 :: TwoFactorProviderType
292
307
@@ -304,24 +319,51 @@ twoFactorProviderTypeOrganizationDuo = 6 :: TwoFactorProviderType
304
319
305twoFactorProviderTypeWebAuthn = 7 :: TwoFactorProviderType
320twoFactorProviderTypeWebAuthn = 7 :: TwoFactorProviderType
306
321
307−type TwoFactorProviderType
322+type TwoFactorProviderType = Int
308− = Int
323+
309−
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+
310−newtype Password
356+newtype Password = Password String
311− = Password String
312
357
313hashPurposeServerAuthorization = 1 :: HashPurpose
358hashPurposeServerAuthorization = 1 :: HashPurpose
314
359
315hashPurposeLocalAuthorization = 2 :: HashPurpose
360hashPurposeLocalAuthorization = 2 :: HashPurpose
316
361
317−type HashPurpose
362+type HashPurpose = Int
318− = Int
319
363
320−newtype StringHash
364+newtype StringHash = StringHash String
321− = StringHash String
322
365
323−newtype AccessToken
366+newtype AccessToken = AccessToken String
324− = AccessToken String
325
367
326derive newtype instance Show AccessToken
368derive newtype instance Show AccessToken
327derive newtype instance Eq AccessToken
369derive newtype instance Eq AccessToken
@@ -329,8 +371,7 @@ derive newtype instance Ord AccessToken
329derive newtype instance EncodeJson AccessToken
371derive newtype instance EncodeJson AccessToken
330derive newtype instance DecodeJson AccessToken
372derive newtype instance DecodeJson AccessToken
331
373
332−newtype RefreshToken
374+newtype RefreshToken = RefreshToken String
333− = RefreshToken String
334
375
335derive newtype instance Show RefreshToken
376derive newtype instance Show RefreshToken
336derive newtype instance Eq RefreshToken
377derive newtype instance Eq RefreshToken
@@ -342,75 +383,91 @@ type IdentityCaptchaResponse =
342 { siteKey :: String
383 { siteKey :: String
343 }
384 }
344
385
345−type IdentityTokenResponse
386+type IdentityTokenResponse =
346− = { accessToken :: AccessToken
387+ { accessToken :: AccessToken
347− , expiresIn :: Int
388+ , expiresIn :: Int
348− , refreshToken :: RefreshToken
389+ , refreshToken :: RefreshToken
349− , tokenType :: String
390+ , tokenType :: String
350− , resetMasterPassword :: Boolean
391+ , resetMasterPassword :: Boolean
351− , privateKey :: EncryptedString
392+ , privateKey :: EncryptedString
352− , key :: EncryptedString
393+ , key :: EncryptedString
353− , twoFactorToken :: JNullable String
394+ , twoFactorToken :: JNullable String
354− , kdf :: KDF
395+ , kdf :: KDF
355− , kdfIterations :: Int
396+ , kdfIterations :: Int
356− , forcePasswordReset :: JNullable Boolean
397+ , forcePasswordReset :: JNullable Boolean
357− , apiUseKeyConnector :: JNullable Boolean
398+ , apiUseKeyConnector :: JNullable Boolean
358− , keyConnectorUrl :: JNullable String
399+ , keyConnectorUrl :: JNullable String
359− }
400+ }
360−
401+
361−newtype Email
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+
362− = Email String
420+newtype Email = Email String
363−
421+
364−type SyncResponse
422+type SyncResponse =
365− = { profile :: ProfileResponse
423+ { profile :: ProfileResponse
366− , folders :: Array FolderResponse
424+ , folders :: Array FolderResponse
367− , collections :: Array CollectionDetailsResponse
425+ , collections :: Array CollectionDetailsResponse
368− , ciphers :: Array CipherResponse
426+ , ciphers :: Array CipherResponse
369− , domains :: JNullable DomainsResponse
427+ , domains :: JNullable DomainsResponse
370− , policies :: JNullable (Array PolicyResponse)
428+ , policies :: JNullable (Array PolicyResponse)
371− , sends :: Array SendResponse
429+ , 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− }
430+ }
387
431
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+ }
388
445
389type CipherId = String
446type CipherId = String
390
447
391−type CipherResponse
448+type CipherResponse =
392− = { id :: CipherId
449+ { id :: CipherId
393− , organizationId :: JNullable String
450+ , organizationId :: JNullable String
394− , folderId :: JNullable String
451+ , folderId :: JNullable String
395− , type :: CipherType
452+ , type :: CipherType
396− , name :: EncryptedString
453+ , name :: EncryptedString
397− , notes :: JNullable EncryptedString
454+ , notes :: JNullable EncryptedString
398− , fields :: JNullable (Array FieldApi)
455+ , fields :: JNullable (Array FieldApi)
399− , login :: JNullable LoginApi
456+ , login :: JNullable LoginApi
400− , card :: JNullable CardApi
457+ , card :: JNullable CardApi
401− , identity :: JNullable IdentityApi
458+ , identity :: JNullable IdentityApi
402− , secureNote :: JNullable SecureNoteApi
459+ , secureNote :: JNullable SecureNoteApi
403− , favorite :: Boolean
460+ , favorite :: Boolean
404− , edit :: JNullable Boolean
461+ , edit :: JNullable Boolean
405− , viewPassword :: JNullable Boolean
462+ , viewPassword :: JNullable Boolean
406− , organizationUseTotp :: JNullable Boolean
463+ , organizationUseTotp :: JNullable Boolean
407− , revisionDate :: JNullable Timestamp
464+ , revisionDate :: JNullable Timestamp
408− , attachments :: JNullable (Array AttachmentResponse)
465+ , attachments :: JNullable (Array AttachmentResponse)
409− , passwordHistory :: JNullable (Array PasswordHistoryResponse)
466+ , passwordHistory :: JNullable (Array PasswordHistoryResponse)
410− , collectionIds :: JNullable (Array String)
467+ , collectionIds :: JNullable (Array String)
411− , deletedDate :: JNullable String
468+ , deletedDate :: JNullable String
412− , reprompt :: CipherRepromptType
469+ , reprompt :: CipherRepromptType
413− }
470+ }
414
471
415cipherTypeLogin = 1 :: CipherType
472cipherTypeLogin = 1 :: CipherType
416
473
@@ -420,15 +477,14 @@ cipherTypeCard = 3 :: CipherType
420
477
421cipherTypeIdentity = 4 :: CipherType
478cipherTypeIdentity = 4 :: CipherType
422
479
423−type CipherType
480+type CipherType = Int
424− = Int
425
481
426−type FieldApi
482+type FieldApi =
427− = { name :: String
483+ { name :: JNullable String
428− , value :: String
484+ , value :: JNullable String
429− , type :: FieldType
485+ , type :: FieldType
430− , linkedId :: JNullable LinkedIdType
486+ , linkedId :: JNullable LinkedIdType
431− }
487+ }
432
488
433fieldTypeText = 0 :: FieldType
489fieldTypeText = 0 :: FieldType
434
490
@@ -438,8 +494,7 @@ fieldTypeBoolean = 2 :: FieldType
438
494
439fieldTypeLinked = 3 :: FieldType
495fieldTypeLinked = 3 :: FieldType
440
496
441−type FieldType
497+type FieldType = Int
442− = Int
443
498
444linkedIdTypeLoginLinkedIdUsername = 100 :: LinkedIdType
499linkedIdTypeLoginLinkedIdUsername = 100 :: LinkedIdType
445
500
@@ -495,23 +550,22 @@ linkedIdTypeIdentityLinkedIdLastName = 417 :: LinkedIdType
495
550
496linkedIdTypeIdentityLinkedIdFullName = 418 :: LinkedIdType
551linkedIdTypeIdentityLinkedIdFullName = 418 :: LinkedIdType
497
552
498−type LinkedIdType
553+type LinkedIdType = Int
499− = Int
500
554
501−type LoginApi
555+type LoginApi =
502− = { uris :: JOpt (Array LoginUriApi)
556+ { uris :: JOpt (Array LoginUriApi)
503− , username :: JNullable EncryptedString
557+ , username :: JNullable EncryptedString
504− , password :: JNullable EncryptedString
558+ , password :: JNullable EncryptedString
505− , passwordRevisionDate :: JNullable String
559+ , passwordRevisionDate :: JNullable String
506− , totp :: JNullable EncryptedString
560+ , totp :: JNullable EncryptedString
507− -- This thing is undefined in some cases for some reson. Just ignoring it
561+ -- This thing is undefined in some cases for some reson. Just ignoring it
508− , autofillOnPageLoad :: JOpt (JNullable Boolean)
562+ , autofillOnPageLoad :: JOpt (JNullable Boolean)
509− }
563+ }
510
564
511−type LoginUriApi
565+type LoginUriApi =
512− = { uri :: EncryptedString
566+ { uri :: JNullable EncryptedString
513− , match :: JNullable UriMatchType
567+ , match :: JNullable UriMatchType
514− }
568+ }
515
569
516uriMatchTypeDomain = 0 :: UriMatchType
570uriMatchTypeDomain = 0 :: UriMatchType
517
571
@@ -525,87 +579,85 @@ uriMatchTypeRegularExpression = 4 :: UriMatchType
525
579
526uriMatchTypeNever = 5 :: UriMatchType
580uriMatchTypeNever = 5 :: UriMatchType
527
581
528−type UriMatchType
582+type UriMatchType = Int
529− = Int
583+
530−
531−type CardApi
584+type CardApi =
532− = { cardholderName :: JNullable EncryptedString
585+ { cardholderName :: JNullable EncryptedString
533− , brand :: JNullable EncryptedString
586+ , brand :: JNullable EncryptedString
534− , number :: JNullable EncryptedString
587+ , number :: JNullable EncryptedString
535− , expMonth :: JNullable EncryptedString
588+ , expMonth :: JNullable EncryptedString
536− , expYear :: JNullable EncryptedString
589+ , expYear :: JNullable EncryptedString
537− , code :: JNullable EncryptedString
590+ , code :: JNullable EncryptedString
538− }
591+ }
539−
592+
540−type IdentityApi
593+type IdentityApi =
541− = { title :: JNullable EncryptedString
594+ { title :: JNullable EncryptedString
542− , firstName :: JNullable EncryptedString
595+ , firstName :: JNullable EncryptedString
543− , middleName :: JNullable EncryptedString
596+ , middleName :: JNullable EncryptedString
544− , lastName :: JNullable EncryptedString
597+ , lastName :: JNullable EncryptedString
545− , address1 :: JNullable EncryptedString
598+ , address1 :: JNullable EncryptedString
546− , address2 :: JNullable EncryptedString
599+ , address2 :: JNullable EncryptedString
547− , address3 :: JNullable EncryptedString
600+ , address3 :: JNullable EncryptedString
548− , city :: JNullable EncryptedString
601+ , city :: JNullable EncryptedString
549− , state :: JNullable EncryptedString
602+ , state :: JNullable EncryptedString
550− , postalCode :: JNullable EncryptedString
603+ , postalCode :: JNullable EncryptedString
551− , country :: JNullable EncryptedString
604+ , country :: JNullable EncryptedString
552− , company :: JNullable EncryptedString
605+ , company :: JNullable EncryptedString
553− , email :: JNullable EncryptedString
606+ , email :: JNullable EncryptedString
554− , phone :: JNullable EncryptedString
607+ , phone :: JNullable EncryptedString
555− , ssn :: JNullable EncryptedString
608+ , ssn :: JNullable EncryptedString
556− , username :: JNullable EncryptedString
609+ , username :: JNullable EncryptedString
557− , passportNumber :: JNullable EncryptedString
610+ , passportNumber :: JNullable EncryptedString
558− , licenseNumber :: JNullable EncryptedString
611+ , licenseNumber :: JNullable EncryptedString
559− }
612+ }
560
613
561secureNoteTypeGeneric = 0 :: SecureNoteType
614secureNoteTypeGeneric = 0 :: SecureNoteType
562
615
563−type SecureNoteType
616+type SecureNoteType = Int
564− = Int
565
617
566−type SecureNoteApi = {
618+type SecureNoteApi =
619+ {
567− type :: SecureNoteType
620+ -- type :: SecureNoteType
568−}
621+ }
569
622
570−type AttachmentResponse
623+type AttachmentResponse =
571− = { id :: String
624+ { id :: String
572− , url :: String
625+ , url :: JNullable String
573− , fileName :: String
626+ , fileName :: JNullable String
574− , key :: String
627+ , key :: JNullable String
575− , size :: String
628+ , size :: JNullable String
576− , sizeName :: String
629+ , sizeName :: JNullable String
577− }
630+ }
578
631
579−type PasswordHistoryResponse
632+type PasswordHistoryResponse =
580− = { password :: String
633+ { password :: String
581− , lastUsedDate :: String
634+ , lastUsedDate :: JNullable String
582− }
635+ }
583
636
584cipherRepromptTypeNone = 0 :: CipherRepromptType
637cipherRepromptTypeNone = 0 :: CipherRepromptType
585
638
586cipherRepromptTypePassword = 1 :: CipherRepromptType
639cipherRepromptTypePassword = 1 :: CipherRepromptType
587
640
588−type CipherRepromptType
641+type CipherRepromptType = Int
589− = Int
590
642
591−type DomainsResponse
643+type DomainsResponse =
592− = { equivalentDomains :: Array (Array String)
644+ { equivalentDomains :: Array (Array String)
593− , globalEquivalentDomains :: Array GlobalDomainResponse
645+ , globalEquivalentDomains :: Array GlobalDomainResponse
594− }
646+ }
595
647
596−type GlobalDomainResponse
648+type GlobalDomainResponse =
597− = { type :: Int
649+ { type :: Int
598− , domains :: Array String
650+ , domains :: Array String
599− , excluded :: Boolean
651+ , excluded :: Boolean
600− }
652+ }
601
653
602−type PolicyResponse
654+type PolicyResponse =
603− = { id :: String
655+ { id :: String
604− , organizationId :: JNullable String
656+ , organizationId :: JNullable String
605− , type :: PolicyType
657+ , type :: PolicyType
606− , data :: ShowableJson
658+ , data :: ShowableJson
607− , enabled :: Boolean
659+ , enabled :: Boolean
608− }
660+ }
609
661
610policyTypeTwoFactorAuthentication = 0 :: PolicyType -- Requires users to have 2fa enabled
662policyTypeTwoFactorAuthentication = 0 :: PolicyType -- Requires users to have 2fa enabled
611
663
@@ -629,43 +681,46 @@ policyTypeMaximumVaultTimeout = 9 :: PolicyType -- Sets the maximum allowed vaul
629
681
630policyTypeDisablePersonalVaultExport = 10 :: PolicyType -- Disable personal vault export
682policyTypeDisablePersonalVaultExport = 10 :: PolicyType -- Disable personal vault export
631
683
632−type PolicyType
684+type PolicyType = Int
633− = Int
685+
634−
635−type SendResponse
686+type SendResponse =
636− = { id :: String
687+ { id :: String
637− , accessId :: String
688+ , accessId :: JNullable String
638− , type :: SendType
689+ , type :: SendType
639− , name :: String
690+ , name :: JNullable String
640− , notes :: JNullable String
691+ , notes :: JNullable String
641− , file :: JNullable SendFileApi
692+ , file :: JNullable SendFileApi
642− , text :: JNullable SendTextApi
693+ , text :: JNullable SendTextApi
643− , key :: String
694+ , key :: JNullable String
644− , maxAccessCount :: JNullable Int
695+ , maxAccessCount :: JNullable Int
645− , accessCount :: Int
696+ , accessCount :: JNullable Int
646− , revisionDate :: String
697+ , revisionDate :: JNullable String
647− , expirationDate :: String
698+ , expirationDate :: JNullable String
648− , deletionDate :: JNullable String
699+ , deletionDate :: JNullable String
649− , password :: String
700+ , password :: JNullable String
650− , disable :: Boolean
701+ , disable :: Boolean
651− , hideEmail :: Boolean
702+ , hideEmail :: Boolean
652− }
703+ }
653
704
654sendTypeText = 0 :: SendType
705sendTypeText = 0 :: SendType
655
706
656sendTypeFile = 1 :: SendType
707sendTypeFile = 1 :: SendType
657
708
658−type SendType
709+type SendType = Int
659− = Int
710+
711+type SendFileApi =
712+ { id :: String
713+ , fileName :: JNullable String
714+ , size :: JNullable String
715+ , sizeName :: JNullable String
716+ }
660
717
661−type SendFileApi
718+type SendTextApi =
662− = { id :: String
719+ { text :: JNullable String
663− , fileName :: String
720+ , hidden :: Boolean
664− , size :: String
665− , sizeName :: String
666− }
721+ }
667
722
668−type SendTextApi
723+type TwoFactorEmailRequest =
669− = { text :: String
724+ { email :: Email
670− , hidden :: Boolean
725+ , masterPasswordHash :: StringHash
671− }
726+ }
backend/Bridge.purs+128−1
@@ -362,6 +362,18 @@ derive instance genericCipher_LoginCipher_password_Maybe :: Generic Cipher_Login
362derive instance eqCipher_LoginCipher_password_Maybe :: Eq Cipher_LoginCipher_password_Maybe
362derive instance eqCipher_LoginCipher_password_Maybe :: Eq Cipher_LoginCipher_password_Maybe
363derive instance ordCipher_LoginCipher_password_Maybe :: Ord Cipher_LoginCipher_password_Maybe
363derive instance ordCipher_LoginCipher_password_Maybe :: Ord Cipher_LoginCipher_password_Maybe
364
364
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+
365newtype Cipher_LoginCipher_uris_List =
377newtype Cipher_LoginCipher_uris_List =
366 Cipher_LoginCipher_uris_List (Array String)
378 Cipher_LoginCipher_uris_List (Array String)
367
379
@@ -389,6 +401,7 @@ derive instance ordCipher_LoginCipher_username_Maybe :: Ord Cipher_LoginCipher_u
389newtype Cipher_LoginCipher =
401newtype Cipher_LoginCipher =
390 Cipher_LoginCipher {
402 Cipher_LoginCipher {
391 password :: Cipher_LoginCipher_password_Maybe
403 password :: Cipher_LoginCipher_password_Maybe
404+ , totp :: Cipher_LoginCipher_totp_Maybe
392 , uris :: Cipher_LoginCipher_uris_List
405 , uris :: Cipher_LoginCipher_uris_List
393 , username :: Cipher_LoginCipher_username_Maybe
406 , username :: Cipher_LoginCipher_username_Maybe
394 }
407 }
@@ -430,10 +443,57 @@ derive instance genericCipherType :: Generic CipherType _
430derive instance eqCipherType :: Eq CipherType
443derive instance eqCipherType :: Eq CipherType
431derive instance ordCipherType :: Ord CipherType
444derive instance ordCipherType :: Ord CipherType
432
445
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+
433newtype Cmd_Login =
492newtype Cmd_Login =
434 Cmd_Login {
493 Cmd_Login {
435 email :: String
494 email :: String
436 , password :: String
495 , password :: String
496+ , secondFactor :: Cmd_Login_secondFactor_Maybe
437 , server :: String
497 , server :: String
438 }
498 }
439
499
@@ -447,7 +507,10 @@ derive instance eqCmd_Login :: Eq Cmd_Login
447derive instance ordCmd_Login :: Ord Cmd_Login
507derive instance ordCmd_Login :: Ord Cmd_Login
448
508
449data Cmd =
509data Cmd =
510+ ChooseSecondFactor Cmd_ChooseSecondFactor
450− Copy String
511+ | Copy String
512+ | CreateCipher FullCipher
513+ | DeleteCipher FullCipher
451 | GeneratePassword PasswordGeneratorConfig
514 | GeneratePassword PasswordGeneratorConfig
452 | Init
515 | Init
453 | Login Cmd_Login
516 | Login Cmd_Login
@@ -456,6 +519,7 @@ data Cmd =
456 | NeedsReset
519 | NeedsReset
457 | Open String
520 | Open String
458 | RequestCipher String
521 | RequestCipher String
522+ | RequestTotp String
459 | SendMasterPassword String
523 | SendMasterPassword String
460 | UpdateCipher FullCipher
524 | UpdateCipher FullCipher
461
525
@@ -467,9 +531,22 @@ derive instance genericCmd :: Generic Cmd _
467derive instance eqCmd :: Eq Cmd
531derive instance eqCmd :: Eq Cmd
468derive instance ordCmd :: Ord Cmd
532derive instance ordCmd :: Ord Cmd
469
533
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+
470newtype FullCipher =
546newtype FullCipher =
471 FullCipher {
547 FullCipher {
472 cipher :: Cipher
548 cipher :: Cipher
549+ , collectionIds :: FullCipher_collectionIds_List
473 , favorite :: Boolean
550 , favorite :: Boolean
474 , id :: String
551 , id :: String
475 , name :: String
552 , name :: String
@@ -557,9 +634,38 @@ derive instance genericSub_NeedsMasterPassword :: Generic Sub_NeedsMasterPasswor
557derive instance eqSub_NeedsMasterPassword :: Eq Sub_NeedsMasterPassword
634derive instance eqSub_NeedsMasterPassword :: Eq Sub_NeedsMasterPassword
558derive instance ordSub_NeedsMasterPassword :: Ord Sub_NeedsMasterPassword
635derive instance ordSub_NeedsMasterPassword :: Ord Sub_NeedsMasterPassword
559
636
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+
560data Sub =
665data Sub =
561 CaptchaDone
666 CaptchaDone
562 | CipherChanged FullCipher
667 | CipherChanged FullCipher
668+ | CipherDeleted FullCipher
563 | Error String
669 | Error String
564 | GeneratedPassword String
670 | GeneratedPassword String
565 | LoadCipher FullCipher
671 | LoadCipher FullCipher
@@ -568,8 +674,11 @@ data Sub =
568 | NeedsCaptcha String
674 | NeedsCaptcha String
569 | NeedsLogin
675 | NeedsLogin
570 | NeedsMasterPassword Sub_NeedsMasterPassword
676 | NeedsMasterPassword Sub_NeedsMasterPassword
677+ | NeedsSecondFactor Sub_NeedsSecondFactor_List
571 | RecieveEmail String
678 | RecieveEmail String
572 | Reset
679 | Reset
680+ | Totp Sub_Totp
681+ | WrongPassword
573
682
574instance encodeJsonSub :: EncodeJson Sub where
683instance encodeJsonSub :: EncodeJson Sub where
575 encodeJson = genericEncodeAeson Argonaut.defaultOptions
684 encodeJson = genericEncodeAeson Argonaut.defaultOptions
@@ -579,3 +688,21 @@ derive instance genericSub :: Generic Sub _
579derive instance eqSub :: Eq Sub
688derive instance eqSub :: Eq Sub
580derive instance ordSub :: Ord Sub
689derive instance ordSub :: Ord Sub
581
690
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"
2
2
3export function fromString(str) {
3export function fromString(str) {
4− console.log("fromString " + str)
5 const x = new EncString(str)
4 const x = new EncString(str)
6 return x
5 return x
7}
6}
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'
2
2
3export function fromArrayBuffer(arr) {
3export function fromArrayBuffer(arr) {
4− console.log("fromArrayBuffer " + arr)
5 return new SymmetricCryptoKey(arr)
4 return new SymmetricCryptoKey(arr)
6}
5}
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+
1export function startElm(element) {
16export function startElm(element) {
2 return function () {
17 return function () {
3− return Elm.Main.init({
18+ return searchForInit(Elm)({
4 node: document.getElementById(element)
19 node: document.getElementById(element)
5 })
20 })
6 }
21 }
backend/Main.purs+220−122
@@ -3,18 +3,20 @@ module Main
3 ) where
3 ) where
4
4
5import Prelude
5import Prelude
6+
6import BW (ApiService, CryptoFunctions, Services, CryptoService)
7import BW (ApiService, CryptoFunctions, Services, CryptoService)
7import BW as WB
8import BW as WB
8−import BW.Logic (decodeCipher, decrypt, encodeCipher, hashPassword, liftPromise)
9+import BW.Logic (bwPasswordStringHash, decodeCipher, decrypt, encodeCipher, hashPassword, liftPromise, supportedCipher)
9import BW.Logic as Logic
10import 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)
11import Bridge as Bridge
12import Bridge as Bridge
12−import Control.Monad.Error.Class (catchError, throwError)
13+import Control.Monad.Error.Class (catchError)
13import Data.Argonaut (class DecodeJson)
14import Data.Argonaut (class DecodeJson)
14import Data.Array as Array
15import Data.Array as Array
15import Data.Clipboard as Clipboard
16import Data.Clipboard as Clipboard
16import Data.DateTime (DateTime)
17import Data.DateTime (DateTime)
17import Data.Either (Either(..))
18import Data.Either (Either(..))
19+import Data.Foldable (for_)
18import Data.HCaptcha (bindHCaptchToken)
20import Data.HCaptcha (bindHCaptchToken)
19import Data.JNullable (jnull, nullify)
21import Data.JNullable (jnull, nullify)
20import Data.JNullable as JNullable
22import Data.JNullable as JNullable
@@ -28,7 +30,7 @@ import Effect (Effect)
28import Effect.Aff (Aff, runAff_, try)
30import Effect.Aff (Aff, runAff_, try)
29import Effect.Class (liftEffect)
31import Effect.Class (liftEffect)
30import Effect.Class.Console (log)
32import Effect.Class.Console (log)
31−import Effect.Exception (error)
33+import Effect.Exception (throw)
32import Effect.Exception as Exc
34import Effect.Exception as Exc
33import Effect.Ref as Ref
35import Effect.Ref as Ref
34import Elm as Elm
36import Elm as Elm
@@ -60,19 +62,19 @@ main = do
60 let
62 let
61 runAff act = runElmAff app $ run $ act
63 runAff act = runElmAff app $ run $ act
62
64
63− run ::
65+ run
64− forall a.
66+ :: forall a
65− Run
67+ . Run
66− ( app :: Reader Elm
68+ ( app :: Reader Elm
67− , aff :: Aff
69+ , aff :: Aff
68− , effect :: Effect
70+ , effect :: Effect
69− , crypto :: Reader CryptoService
71+ , crypto :: Reader CryptoService
70− , storage :: Reader Storage
72+ , storage :: Reader Storage
71− , services :: Reader Services
73+ , services :: Reader Services
72− , cryptoFunctions :: Reader CryptoFunctions
74+ , cryptoFunctions :: Reader CryptoFunctions
73− )
75+ )
74− a ->
76+ a
75− Run (EFFECT + AFF + ()) a
77+ -> Run (EFFECT + AFF + ()) a
76 run act =
78 run act =
77 runReaderAt (Proxy :: _ "app") app
79 runReaderAt (Proxy :: _ "app") app
78 $ runReaderAt (Proxy :: _ "crypto") services.crypto
80 $ runReaderAt (Proxy :: _ "crypto") services.crypto
@@ -81,19 +83,19 @@ main = do
81 $ runReaderAt (Proxy :: _ "cryptoFunctions") services.cryptoFunctions
83 $ runReaderAt (Proxy :: _ "cryptoFunctions") services.cryptoFunctions
82 $ act
84 $ act
83
85
84− runWithDecryptionKey ::
86+ runWithDecryptionKey
85− Run
87+ :: Run
86− ( app :: Reader Elm
88+ ( app :: Reader Elm
87− , aff :: Aff
89+ , aff :: Aff
88− , effect :: Effect
90+ , effect :: Effect
89− , key :: Reader SymmetricCryptoKey
91+ , key :: Reader SymmetricCryptoKey
90− , crypto :: Reader CryptoService
92+ , crypto :: Reader CryptoService
91− , storage :: Reader Storage
93+ , storage :: Reader Storage
92− , services :: Reader Services
94+ , services :: Reader Services
93− , cryptoFunctions :: Reader CryptoFunctions
95+ , cryptoFunctions :: Reader CryptoFunctions
94− )
96+ )
95− Unit ->
97+ Unit
96− Effect Unit
98+ -> Effect Unit
97 runWithDecryptionKey act = do
99 runWithDecryptionKey act = do
98 maybeKey <- Ref.read masterKeyRef
100 maybeKey <- Ref.read masterKeyRef
99 runAff
101 runAff
@@ -104,8 +106,10 @@ main = do
104 runReaderAt (Proxy :: _ "key") key $ act
106 runReaderAt (Proxy :: _ "key") key $ act
105 Nothing -> requestMasterPassword
107 Nothing -> requestMasterPassword
106 Elm.subscribe app \cmd -> case cmd of
108 Elm.subscribe app \cmd -> case cmd of
107− Bridge.Login (Bridge.Cmd_Login { email, server, password }) -> do
109+ Bridge.Login (Bridge.Cmd_Login { email: email', server, password, secondFactor }) -> do
110+ let
111+ email = Email email'
108− captchaToken <- Ref.read hCaptchaTokenRef
112+ captchaResponse <- Ref.read hCaptchaTokenRef
109 Ref.write Nothing hCaptchaTokenRef
113 Ref.write Nothing hCaptchaTokenRef
110 let
114 let
111 urls = baseUrl server
115 urls = baseUrl server
@@ -116,18 +120,27 @@ main = do
116 loginResponse <-
120 loginResponse <-
117 liftAff
121 liftAff
118 $ catchError
122 $ catchError
123+ ( do
124+ resp <-
119− ( runBaseAff' $ run $ runReaderAt (Proxy :: _ "api") unauthedApi
125+ runBaseAff' $ run $ runReaderAt (Proxy :: _ "api") unauthedApi
120− $ Logic.getLogInRequestToken prelogin (Email email) (Password password) captchaToken
126+ $ Logic.getLogInRequestToken
127+ { prelogin
128+ , email
129+ , password: Password password
130+ , captchaResponse
131+ , secondFactor
132+ }
133+ pure $ Right resp
121 )
134 )
122− ( const
135+ ( \err -> do
123− $ liftEffect do
136+ log $ show err
124− Elm.send app Bridge.NeedsLogin
137+ pure $ Left unit
125− Exc.throw "Could not log in. Please check your data and try again."
126 )
138 )
127− case toEither1 loginResponse of
139+ case map (toEither1 >>> map toEither1) $ loginResponse of
140+ Left _ -> send Bridge.WrongPassword
128− Left { siteKey } -> send $ Bridge.NeedsCaptcha siteKey
141+ Right (Left { siteKey }) -> send $ Bridge.NeedsCaptcha siteKey
129− Right token -> do
142+ Right (Right (Right token)) -> do
130− masterKey <- Logic.makePreloginKey prelogin (Email email) (Password password)
143+ masterKey <- Logic.makePreloginKey prelogin email (Password password)
131 api <- liftPromise $ services.getApi urls (nullify token)
144 api <- liftPromise $ services.getApi urls (nullify token)
132 sync <- liftPromise $ api.getSync unit
145 sync <- liftPromise $ api.getSync unit
133 hash <- hashPassword (Password password)
146 hash <- hashPassword (Password password)
@@ -140,7 +153,20 @@ main = do
140 Storage.store storage SyncKey sync
153 Storage.store storage SyncKey sync
141 Storage.store storage TokenKey token
154 Storage.store storage TokenKey token
142 send Bridge.LoginSuccessful
155 send Bridge.LoginSuccessful
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
143− Bridge.NeedCiphersList -> runWithDecryptionKey performSync
166+ Bridge.NeedCiphersList ->
167+ runWithDecryptionKey do
168+ sendCiphers
169+ performSync
144 Bridge.NeedsReset -> do
170 Bridge.NeedsReset -> do
145 WebStorage.clear storage
171 WebStorage.clear storage
146 Elm.send app Bridge.Reset
172 Elm.send app Bridge.Reset
@@ -160,9 +186,9 @@ main = do
160 Bridge.Init -> do
186 Bridge.Init -> do
161 Storage.get storage TokenKey
187 Storage.get storage TokenKey
162 >>= \x ->
188 >>= \x ->
163− Elm.send app case x of
189+ Elm.send app case x of
164− Just _ -> Bridge.LoginSuccessful
190+ Just _ -> Bridge.LoginSuccessful
165− Nothing -> Bridge.NeedsLogin
191+ Nothing -> Bridge.NeedsLogin
166 Bridge.RequestCipher id ->
192 Bridge.RequestCipher id ->
167 runWithDecryptionKey do
193 runWithDecryptionKey do
168 sync <- getOrReset SyncKey
194 sync <- getOrReset SyncKey
@@ -170,8 +196,7 @@ main = do
170 Nothing -> pure unit
196 Nothing -> pure unit
171 Just cipherResponse -> do
197 Just cipherResponse -> do
172 cipher <- decodeCipher cipherResponse
198 cipher <- decodeCipher cipherResponse
173− send $ Bridge.LoadCipher cipher
199+ for_ cipher $ send <<< Bridge.LoadCipher
174− pure unit
175 pure unit
200 pure unit
176 Bridge.NeedEmail ->
201 Bridge.NeedEmail ->
177 runWithDecryptionKey do
202 runWithDecryptionKey do
@@ -187,7 +212,7 @@ main = do
187 api <- getAuthedApi
212 api <- getAuthedApi
188 cipher <- encodeCipher fullCipher
213 cipher <- encodeCipher fullCipher
189 newCipher <- liftPromise (api.putCipher cipher) >>= decodeCipher
214 newCipher <- liftPromise (api.putCipher cipher) >>= decodeCipher
190− send $ Bridge.CipherChanged newCipher
215+ for_ newCipher $ send <<< Bridge.CipherChanged
191 performSync
216 performSync
192 pure unit
217 pure unit
193 Bridge.GeneratePassword cfg ->
218 Bridge.GeneratePassword cfg ->
@@ -195,18 +220,71 @@ main = do
195 password <- liftPromise $ services.passwordGeneration.generatePassword cfg
220 password <- liftPromise $ services.passwordGeneration.generatePassword cfg
196 send $ Bridge.GeneratedPassword password
221 send $ Bridge.GeneratedPassword password
197 pure unit
222 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
198
275
199−processCipher ::
276+processCipher
200− forall r.
277+ :: forall r
201− CipherResponse ->
278+ . CipherResponse
202− Run
279+ -> Run
203− ( crypto :: Reader CryptoService
280+ ( crypto :: Reader CryptoService
204− , key :: Reader SymmetricCryptoKey
281+ , key :: Reader SymmetricCryptoKey
205− , aff :: Aff
282+ , aff :: Aff
206− , effect :: Effect
283+ , effect :: Effect
207− | r
284+ | r
208− )
285+ )
209− { cipher :: Bridge.Sub_LoadCiphers, date :: DateTime }
286+ (Maybe { cipher :: Bridge.Sub_LoadCiphers, date :: DateTime })
287+processCipher cipher | not (supportedCipher cipher) = pure Nothing
210processCipher cipher = do
288processCipher cipher = do
211 name <- decrypt cipher.name
289 name <- decrypt cipher.name
212 cipherType <- case cipher.type of
290 cipherType <- case cipher.type of
@@ -218,10 +296,12 @@ processCipher cipher = do
218 | n == cipherTypeCard -> pure Bridge.CardType
296 | n == cipherTypeCard -> pure Bridge.CardType
219 n
297 n
220 | n == cipherTypeIdentity -> pure Bridge.IdentityType
298 | n == cipherTypeIdentity -> pure Bridge.IdentityType
221− n -> liftEffect $ throwError $ error $ "Unsupported cipher type: " <> show n
299+ n -> liftEffect $ throw $ "Unsupported cipher type: " <> show n
222 date <- liftEffect $ maybe (pure bottom) (Timestamp.toDateTime) $ JNullable.toMaybe cipher.revisionDate
300 date <- liftEffect $ maybe (pure bottom) (Timestamp.toDateTime) $ JNullable.toMaybe cipher.revisionDate
223 pure
301 pure
302+ $ Just
303+ $
224− $ { cipher:
304+ { cipher:
225 Bridge.Sub_LoadCiphers
305 Bridge.Sub_LoadCiphers
226 { name: name
306 { name: name
227 , date: maybe "" Timestamp.toLocalDateTimeString $ JNullable.toMaybe cipher.revisionDate
307 , date: maybe "" Timestamp.toLocalDateTimeString $ JNullable.toMaybe cipher.revisionDate
@@ -255,18 +335,18 @@ baseUrl server =
255 , keyConnector: jnull
335 , keyConnector: jnull
256 }
336 }
257
337
258−getOrReset ::
338+getOrReset
259− forall k t r.
339+ :: forall k t r
260− StorageKey k t =>
340+ . StorageKey k t
261− DecodeJson t =>
341+ => DecodeJson t
262− k ->
342+ => k
263− Run
343+ -> Run
264− ( storage :: Reader Storage
344+ ( storage :: Reader Storage
265− , app :: Reader Elm
345+ , app :: Reader Elm
266− , effect :: Effect
346+ , effect :: Effect
267− | r
347+ | r
268− )
348+ )
269− t
349+ t
270getOrReset key = do
350getOrReset key = do
271 storage <- askAt (Proxy :: _ "storage")
351 storage <- askAt (Proxy :: _ "storage")
272 app <- askAt (Proxy :: _ "app")
352 app <- askAt (Proxy :: _ "app")
@@ -277,75 +357,93 @@ getOrReset key = do
277 Exc.throw "Encountered an internal error. Resetting the app."
357 Exc.throw "Encountered an internal error. Resetting the app."
278 liftEffect $ try (Storage.get storage key)
358 liftEffect $ try (Storage.get storage key)
279 >>= \x -> case x of
359 >>= \x -> case x of
280− Left err -> do
360+ Left err -> do
281− log $ show err
361+ log $ show err
282− handleError
362+ handleError
283− Right Nothing -> handleError
363+ Right Nothing -> handleError
284− Right (Just y) -> pure y
364+ Right (Just y) -> pure y
285
365
286−getAuthedApi ::
366+getAuthedApi
287− forall r.
367+ :: forall r
288− Run
368+ . Run
289− ( storage :: Reader Storage
369+ ( storage :: Reader Storage
290− , app :: Reader Elm
370+ , app :: Reader Elm
291− , aff :: Aff
371+ , aff :: Aff
292− , effect :: Effect
372+ , effect :: Effect
293− , services :: Reader Services
373+ , services :: Reader Services
294− | r
374+ | r
295− )
375+ )
296− ApiService
376+ ApiService
297getAuthedApi = do
377getAuthedApi = do
298 services <- askAt (Proxy :: _ "services")
378 services <- askAt (Proxy :: _ "services")
299 token <- getOrReset TokenKey
379 token <- getOrReset TokenKey
300 url <- getOrReset UrlsKey
380 url <- getOrReset UrlsKey
301 liftPromise $ services.getApi (baseUrl url) (nullify token)
381 liftPromise $ services.getApi (baseUrl url) (nullify token)
302
382
303−requestMasterPassword ::
383+requestMasterPassword
304− forall r.
384+ :: forall r
305− Run
385+ . Run
306− ( storage :: Reader Storage
386+ ( storage :: Reader Storage
307− , app :: Reader Elm
387+ , app :: Reader Elm
308− , effect :: Effect
388+ , effect :: Effect
309− | r
389+ | r
310− )
390+ )
311− Unit
391+ Unit
312requestMasterPassword = do
392requestMasterPassword = do
313 url <- getOrReset UrlsKey
393 url <- getOrReset UrlsKey
314 sync <- getOrReset SyncKey
394 sync <- getOrReset SyncKey
315 send $ Bridge.NeedsMasterPassword $ Bridge.Sub_NeedsMasterPassword { server: url, login: sync.profile.email }
395 send $ Bridge.NeedsMasterPassword $ Bridge.Sub_NeedsMasterPassword { server: url, login: sync.profile.email }
316
396
317−send ::
397+send
318− forall r.
398+ :: forall r
319− Bridge.Sub ->
399+ . Bridge.Sub
320− Run
400+ -> Run
321− ( app :: Reader Elm
401+ ( app :: Reader Elm
322− , effect :: Effect
402+ , effect :: Effect
323− | r
403+ | r
324− )
404+ )
325− Unit
405+ Unit
326send sub = do
406send sub = do
327 app <- askAt (Proxy :: _ "app")
407 app <- askAt (Proxy :: _ "app")
328 liftEffect $ Elm.send app sub
408 liftEffect $ Elm.send app sub
329
409
330−performSync ::
410+performSync
331− forall r.
411+ :: forall r
332− Run
412+ . Run
333− ( storage :: Reader Storage
413+ ( storage :: Reader Storage
334− , app :: Reader Elm
414+ , app :: Reader Elm
335− , aff :: Aff
415+ , aff :: Aff
336− , effect :: Effect
416+ , effect :: Effect
337− , services :: Reader Services
417+ , services :: Reader Services
338− , crypto :: Reader CryptoService
418+ , crypto :: Reader CryptoService
339− , key :: Reader SymmetricCryptoKey
419+ , key :: Reader SymmetricCryptoKey
340− | r
420+ | r
341− )
421+ )
342− Unit
422+ Unit
343performSync = do
423performSync = do
344 api <- getAuthedApi
424 api <- getAuthedApi
345 storage <- askAt (Proxy :: _ "storage")
425 storage <- askAt (Proxy :: _ "storage")
346 newSync <- liftPromise $ api.getSync unit
426 newSync <- liftPromise $ api.getSync unit
347 liftEffect $ Storage.store storage SyncKey newSync
427 liftEffect $ Storage.store storage SyncKey newSync
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
348− ciphers <- traverse processCipher newSync.ciphers
445+ ciphers <- Array.catMaybes <$> traverse processCipher sync.ciphers
349 let
446 let
350 sortedCiphers = Array.sortWith (_.date >>> Down) ciphers
447 sortedCiphers = Array.sortWith (_.date >>> Down) ciphers
351 send $ Bridge.LoadCiphers $ Bridge.Sub_LoadCiphers_List $ map _.cipher sortedCiphers
448 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
2
2
3import Prelude
3import Prelude
4
4
5−import BW (Hash(..))
5+import BW (Hash)
6import BW.Types (IdentityTokenResponse, SyncResponse, PreloginResponse)
6import BW.Types (IdentityTokenResponse, SyncResponse, PreloginResponse)
7import Localstorage (class StorageKey)
7import Localstorage (class StorageKey)
8
8
@@ -35,3 +35,9 @@ data MasterPasswordHashKey
35
35
36instance StorageKey MasterPasswordHashKey Hash where
36instance StorageKey MasterPasswordHashKey Hash where
37 storageKey MasterPasswordHashKey = "master-password-hash"
37 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 @@
1Sub:
1Sub:
2−- NeedsLogin
2+ - NeedsLogin
3−- NeedsCaptcha: String
3+ - NeedsCaptcha: String
4+ - NeedsSecondFactor: { List: TwoFactorProviderType }
4−- CaptchaDone
5+ - CaptchaDone
5−- Error: String
6+ - Error: String
6−- LoadCiphers:
7+ - LoadCiphers:
7− List:
8+ List:
8 name: String
9 name: String
9 date: String
10 date: String
10 id: String
11 id: String
11 cipherType: CipherType
12 cipherType: CipherType
12−- LoginSuccessful
13+ - LoginSuccessful
14+ - WrongPassword
13−- NeedsMasterPassword:
15+ - NeedsMasterPassword:
14− server: String
16+ server: String
15− login: String
17+ login: String
16−- Reset
18+ - Reset
17−- LoadCipher: FullCipher
19+ - LoadCipher: FullCipher
18−- RecieveEmail: String
20+ - RecieveEmail: String
19−- CipherChanged: FullCipher
21+ - CipherChanged: FullCipher
20−- GeneratedPassword: String
22+ - GeneratedPassword: String
23+ - CipherDeleted: FullCipher
24+ - Totp:
25+ source: String
26+ code: String
27+ interval: Int
21
28
22Cmd:
29Cmd:
23−- Init
30+ - Init
24−- Login:
31+ - Login:
25− email: String
32+ email: String
26− server: String
33+ server: String
27− password: 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
28−- NeedCiphersList
46+ - NeedCiphersList
29−- SendMasterPassword: String
47+ - SendMasterPassword: String
30−- NeedsReset
48+ - NeedsReset
31−- RequestCipher: String
49+ - RequestCipher: String
32−- NeedEmail
50+ - NeedEmail
33−- Copy: String
51+ - Copy: String
34−- Open: String
52+ - Open: String
35−- UpdateCipher: FullCipher
53+ - UpdateCipher: FullCipher
54+ - CreateCipher: FullCipher
36−- GeneratePassword: PasswordGeneratorConfig
55+ - GeneratePassword: PasswordGeneratorConfig
56+ - DeleteCipher: FullCipher
57+ - RequestTotp: String
37
58
38FullCipher:
59FullCipher:
39− reprompt: Int
60+ reprompt: Int
40− favorite: Bool
61+ favorite: Bool
41− id: String
62+ id: String
42− name: String
63+ name: String
43− cipher: Cipher
64+ cipher: Cipher
65+ collectionIds: { List: String }
44
66
45Cipher:
67Cipher:
46−- LoginCipher:
68+ - LoginCipher:
47− uris: {List: String}
69+ uris: { List: String }
48− username:
70+ username:
49 Maybe: String
71 Maybe: String
50− password:
72+ password:
51 Maybe: String
73 Maybe: String
52−- NoteCipher: String
74+ totp:
53−- CardCipher:
54− cardholderName:
55 Maybe: String
75 Maybe: String
56− brand:
76+ - NoteCipher: String
77+ - CardCipher:
78+ cardholderName:
57 Maybe: String
79 Maybe: String
58− number:
80+ brand:
59 Maybe: String
81 Maybe: String
60− expMonth:
82+ number:
61 Maybe: String
83 Maybe: String
62− expYear:
84+ expMonth:
63 Maybe: String
85 Maybe: String
64− code:
86+ expYear:
65 Maybe: String
87 Maybe: String
66−- IdentityCipher:
88+ code:
67− title:
68 Maybe: String
89 Maybe: String
69− firstName:
90+ - IdentityCipher:
91+ title:
70 Maybe: String
92 Maybe: String
71− middleName:
93+ firstName:
72 Maybe: String
94 Maybe: String
73− lastName:
95+ middleName:
74 Maybe: String
96 Maybe: String
75− address1:
97+ lastName:
76 Maybe: String
98 Maybe: String
77− address2:
99+ address1:
78 Maybe: String
100 Maybe: String
79− address3:
101+ address2:
80 Maybe: String
102 Maybe: String
81− city:
103+ address3:
82 Maybe: String
104 Maybe: String
83− state:
105+ city:
84 Maybe: String
106 Maybe: String
85− postalCode:
107+ state:
86 Maybe: String
108 Maybe: String
87− country:
109+ postalCode:
88 Maybe: String
110 Maybe: String
89− company:
111+ country:
90 Maybe: String
112 Maybe: String
91− email:
113+ company:
92 Maybe: String
114 Maybe: String
93− phone:
115+ email:
94 Maybe: String
116 Maybe: String
95− ssn:
117+ phone:
96 Maybe: String
118 Maybe: String
97− username:
119+ ssn:
98 Maybe: String
120 Maybe: String
99− passportNumber:
121+ username:
100 Maybe: String
122 Maybe: String
123+ passportNumber:
124+ Maybe: String
101− licenseNumber:
125+ licenseNumber:
102 Maybe: String
126 Maybe: String
103
127
104CipherType:
128CipherType:
105−- LoginType
129+ - LoginType
106−- CardType
130+ - CardType
107−- NoteType
131+ - NoteType
108−- IdentityType
132+ - IdentityType
109
133
110PasswordGeneratorConfig:
134PasswordGeneratorConfig:
111− length: Int
135+ length: Int
112− ambiguous: Bool
136+ ambiguous: Bool
113− number: Bool
137+ number: Bool
114− minNumber: Int
138+ minNumber: Int
115− uppercase: Bool
139+ uppercase: Bool
116− minUppercase: Int
140+ minUppercase: Int
117− lowercase: Bool
141+ lowercase: Bool
118− minLowercase: Int
142+ minLowercase: Int
119− special: Bool
143+ special: Bool
120− minSpecial: Int
144+ minSpecial: Int
121− type: String
145+ type: String
122− numWords: Int
146+ numWords: Int
123− wordSeparator: String
147+ wordSeparator: String
124− capitalize: Bool
148+ capitalize: Bool
125− includeNumber: 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
2
2
3set -e
3set -e
4
4
5−TEMP=$(mktemp -d)
5+cp -r "$SRC_DIR/dist"/* "$INSTALL_DIR"
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")
clickable.yaml+2−10
@@ -1,16 +1,8 @@
1clickable_minimum_required: 7.1.2
1clickable_minimum_required: 7.1.2
2+framework: ubuntu-sdk-20.04
2builder: custom
3builder: custom
3build: $SRC_DIR/build.sh
4build: $SRC_DIR/build.sh
4kill: webapp-container*
5kill: webapp-container*
5−image_setup:
6+docker_image: clickable/amd64-20.04-amd64
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
15
7
16restrict_arch: all
8restrict_arch: all
deps/bwdeleted−1

Submodule changed.

dev/build-backend.sh+7−1
@@ -1,3 +1,9 @@
1#!/bin/sh
1#!/bin/sh
2
2
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 @@
1#!/bin/sh
1#!/bin/sh
2
2
3+set -e
4+
3(cd deps/bw && npm install)
5(cd deps/bw && npm install)
4
6
5(cd deps/bw/libs/common/ && npm run clean && npm exec -c "tsc -m es2020 -t es2018")
7(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 @@
1#!/bin/sh
1#!/bin/sh
2
2
3+set -e
4+
5+nix build .#vanilla -o vanilla
6+
3−elm-live frontend/Main.elm --start-page=index.html --proxy-prefix=/api --proxy-host=http://localhost:6009 --pushstate -- --debug --output=elm.js
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 @@
1#!/bin/sh
1#!/bin/sh
2
2
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 @@
1#!/bin/sh
1#!/bin/sh
2
2
3+if test -x "$(which chromium)"; then
4+ chromium --args --user-data-dir="/tmp/chrome_dev_test" --disable-web-security
3−/Applications/Chromium.app/Contents/MacOS/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 @@
13 "elm/html": "1.0.0",
13 "elm/html": "1.0.0",
14 "elm/json": "1.1.3",
14 "elm/json": "1.1.3",
15 "elm/time": "1.0.0",
15 "elm/time": "1.0.0",
16+ "mdgriffith/elm-ui": "1.1.8",
16 "mgold/elm-nonempty-list": "4.2.0"
17 "mgold/elm-nonempty-list": "4.2.0"
17 },
18 },
18 "indirect": {
19 "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
383
383
384
384
385
385
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+
386type alias Cipher_LoginCipher_uris_List = (List String)
397type alias Cipher_LoginCipher_uris_List = (List String)
387
398
388jsonDecCipher_LoginCipher_uris_List : Json.Decode.Decoder ( Cipher_LoginCipher_uris_List )
399jsonDecCipher_LoginCipher_uris_List : Json.Decode.Decoder ( Cipher_LoginCipher_uris_List )
@@ -407,14 +418,16 @@ jsonEncCipher_LoginCipher_username_Maybe val = (maybeEncode (Json.Encode.string
407
418
408type alias Cipher_LoginCipher =
419type alias Cipher_LoginCipher =
409 { password: Cipher_LoginCipher_password_Maybe
420 { password: Cipher_LoginCipher_password_Maybe
421+ , totp: Cipher_LoginCipher_totp_Maybe
410 , uris: Cipher_LoginCipher_uris_List
422 , uris: Cipher_LoginCipher_uris_List
411 , username: Cipher_LoginCipher_username_Maybe
423 , username: Cipher_LoginCipher_username_Maybe
412 }
424 }
413
425
414jsonDecCipher_LoginCipher : Json.Decode.Decoder ( Cipher_LoginCipher )
426jsonDecCipher_LoginCipher : Json.Decode.Decoder ( Cipher_LoginCipher )
415jsonDecCipher_LoginCipher =
427jsonDecCipher_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})
417 |> required "password" (jsonDecCipher_LoginCipher_password_Maybe)
429 |> required "password" (jsonDecCipher_LoginCipher_password_Maybe)
430+ |> required "totp" (jsonDecCipher_LoginCipher_totp_Maybe)
418 |> required "uris" (jsonDecCipher_LoginCipher_uris_List)
431 |> required "uris" (jsonDecCipher_LoginCipher_uris_List)
419 |> required "username" (jsonDecCipher_LoginCipher_username_Maybe)
432 |> required "username" (jsonDecCipher_LoginCipher_username_Maybe)
420
433
@@ -422,6 +435,7 @@ jsonEncCipher_LoginCipher : Cipher_LoginCipher -> Value
422jsonEncCipher_LoginCipher val =
435jsonEncCipher_LoginCipher val =
423 Json.Encode.object
436 Json.Encode.object
424 [ ("password", jsonEncCipher_LoginCipher_password_Maybe val.password)
437 [ ("password", jsonEncCipher_LoginCipher_password_Maybe val.password)
438+ , ("totp", jsonEncCipher_LoginCipher_totp_Maybe val.totp)
425 , ("uris", jsonEncCipher_LoginCipher_uris_List val.uris)
439 , ("uris", jsonEncCipher_LoginCipher_uris_List val.uris)
426 , ("username", jsonEncCipher_LoginCipher_username_Maybe val.username)
440 , ("username", jsonEncCipher_LoginCipher_username_Maybe val.username)
427 ]
441 ]
@@ -477,17 +491,82 @@ jsonEncCipherType val =
477
491
478
492
479
493
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+
480type alias Cmd_Login =
557type alias Cmd_Login =
481 { email: String
558 { email: String
482 , password: String
559 , password: String
560+ , secondFactor: Cmd_Login_secondFactor_Maybe
483 , server: String
561 , server: String
484 }
562 }
485
563
486jsonDecCmd_Login : Json.Decode.Decoder ( Cmd_Login )
564jsonDecCmd_Login : Json.Decode.Decoder ( Cmd_Login )
487jsonDecCmd_Login =
565jsonDecCmd_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})
489 |> required "email" (Json.Decode.string)
567 |> required "email" (Json.Decode.string)
490 |> required "password" (Json.Decode.string)
568 |> required "password" (Json.Decode.string)
569+ |> required "secondFactor" (jsonDecCmd_Login_secondFactor_Maybe)
491 |> required "server" (Json.Decode.string)
570 |> required "server" (Json.Decode.string)
492
571
493jsonEncCmd_Login : Cmd_Login -> Value
572jsonEncCmd_Login : Cmd_Login -> Value
@@ -495,13 +574,17 @@ jsonEncCmd_Login val =
495 Json.Encode.object
574 Json.Encode.object
496 [ ("email", Json.Encode.string val.email)
575 [ ("email", Json.Encode.string val.email)
497 , ("password", Json.Encode.string val.password)
576 , ("password", Json.Encode.string val.password)
577+ , ("secondFactor", jsonEncCmd_Login_secondFactor_Maybe val.secondFactor)
498 , ("server", Json.Encode.string val.server)
578 , ("server", Json.Encode.string val.server)
499 ]
579 ]
500
580
501
581
502
582
503type Cmd =
583type Cmd =
584+ ChooseSecondFactor Cmd_ChooseSecondFactor
504− Copy String
585+ | Copy String
586+ | CreateCipher FullCipher
587+ | DeleteCipher FullCipher
505 | GeneratePassword PasswordGeneratorConfig
588 | GeneratePassword PasswordGeneratorConfig
506 | Init
589 | Init
507 | Login Cmd_Login
590 | Login Cmd_Login
@@ -510,13 +593,17 @@ type Cmd =
510 | NeedsReset
593 | NeedsReset
511 | Open String
594 | Open String
512 | RequestCipher String
595 | RequestCipher String
596+ | RequestTotp String
513 | SendMasterPassword String
597 | SendMasterPassword String
514 | UpdateCipher FullCipher
598 | UpdateCipher FullCipher
515
599
516jsonDecCmd : Json.Decode.Decoder ( Cmd )
600jsonDecCmd : Json.Decode.Decoder ( Cmd )
517jsonDecCmd =
601jsonDecCmd =
518 let jsonDecDictCmd = Dict.fromList
602 let jsonDecDictCmd = Dict.fromList
603+ [ ("ChooseSecondFactor", Json.Decode.lazy (\_ -> Json.Decode.map ChooseSecondFactor (jsonDecCmd_ChooseSecondFactor)))
519− [ ("Copy", Json.Decode.lazy (\_ -> Json.Decode.map Copy (Json.Decode.string)))
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)))
520 , ("GeneratePassword", Json.Decode.lazy (\_ -> Json.Decode.map GeneratePassword (jsonDecPasswordGeneratorConfig)))
607 , ("GeneratePassword", Json.Decode.lazy (\_ -> Json.Decode.map GeneratePassword (jsonDecPasswordGeneratorConfig)))
521 , ("Init", Json.Decode.lazy (\_ -> Json.Decode.succeed Init))
608 , ("Init", Json.Decode.lazy (\_ -> Json.Decode.succeed Init))
522 , ("Login", Json.Decode.lazy (\_ -> Json.Decode.map Login (jsonDecCmd_Login)))
609 , ("Login", Json.Decode.lazy (\_ -> Json.Decode.map Login (jsonDecCmd_Login)))
@@ -525,6 +612,7 @@ jsonDecCmd =
525 , ("NeedsReset", Json.Decode.lazy (\_ -> Json.Decode.succeed NeedsReset))
612 , ("NeedsReset", Json.Decode.lazy (\_ -> Json.Decode.succeed NeedsReset))
526 , ("Open", Json.Decode.lazy (\_ -> Json.Decode.map Open (Json.Decode.string)))
613 , ("Open", Json.Decode.lazy (\_ -> Json.Decode.map Open (Json.Decode.string)))
527 , ("RequestCipher", Json.Decode.lazy (\_ -> Json.Decode.map RequestCipher (Json.Decode.string)))
614 , ("RequestCipher", Json.Decode.lazy (\_ -> Json.Decode.map RequestCipher (Json.Decode.string)))
615+ , ("RequestTotp", Json.Decode.lazy (\_ -> Json.Decode.map RequestTotp (Json.Decode.string)))
528 , ("SendMasterPassword", Json.Decode.lazy (\_ -> Json.Decode.map SendMasterPassword (Json.Decode.string)))
616 , ("SendMasterPassword", Json.Decode.lazy (\_ -> Json.Decode.map SendMasterPassword (Json.Decode.string)))
529 , ("UpdateCipher", Json.Decode.lazy (\_ -> Json.Decode.map UpdateCipher (jsonDecFullCipher)))
617 , ("UpdateCipher", Json.Decode.lazy (\_ -> Json.Decode.map UpdateCipher (jsonDecFullCipher)))
530 ]
618 ]
@@ -534,7 +622,10 @@ jsonDecCmd =
534jsonEncCmd : Cmd -> Value
622jsonEncCmd : Cmd -> Value
535jsonEncCmd val =
623jsonEncCmd val =
536 let keyval v = case v of
624 let keyval v = case v of
625+ ChooseSecondFactor v1 -> ("ChooseSecondFactor", encodeValue (jsonEncCmd_ChooseSecondFactor v1))
537 Copy v1 -> ("Copy", encodeValue (Json.Encode.string v1))
626 Copy v1 -> ("Copy", encodeValue (Json.Encode.string v1))
627+ CreateCipher v1 -> ("CreateCipher", encodeValue (jsonEncFullCipher v1))
628+ DeleteCipher v1 -> ("DeleteCipher", encodeValue (jsonEncFullCipher v1))
538 GeneratePassword v1 -> ("GeneratePassword", encodeValue (jsonEncPasswordGeneratorConfig v1))
629 GeneratePassword v1 -> ("GeneratePassword", encodeValue (jsonEncPasswordGeneratorConfig v1))
539 Init -> ("Init", encodeValue (Json.Encode.list identity []))
630 Init -> ("Init", encodeValue (Json.Encode.list identity []))
540 Login v1 -> ("Login", encodeValue (jsonEncCmd_Login v1))
631 Login v1 -> ("Login", encodeValue (jsonEncCmd_Login v1))
@@ -543,14 +634,27 @@ jsonEncCmd val =
543 NeedsReset -> ("NeedsReset", encodeValue (Json.Encode.list identity []))
634 NeedsReset -> ("NeedsReset", encodeValue (Json.Encode.list identity []))
544 Open v1 -> ("Open", encodeValue (Json.Encode.string v1))
635 Open v1 -> ("Open", encodeValue (Json.Encode.string v1))
545 RequestCipher v1 -> ("RequestCipher", encodeValue (Json.Encode.string v1))
636 RequestCipher v1 -> ("RequestCipher", encodeValue (Json.Encode.string v1))
637+ RequestTotp v1 -> ("RequestTotp", encodeValue (Json.Encode.string v1))
546 SendMasterPassword v1 -> ("SendMasterPassword", encodeValue (Json.Encode.string v1))
638 SendMasterPassword v1 -> ("SendMasterPassword", encodeValue (Json.Encode.string v1))
547 UpdateCipher v1 -> ("UpdateCipher", encodeValue (jsonEncFullCipher v1))
639 UpdateCipher v1 -> ("UpdateCipher", encodeValue (jsonEncFullCipher v1))
548 in encodeSumTaggedObject "tag" "contents" keyval val
640 in encodeSumTaggedObject "tag" "contents" keyval val
549
641
550
642
551
643
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+
552type alias FullCipher =
655type alias FullCipher =
553 { cipher: Cipher
656 { cipher: Cipher
657+ , collectionIds: FullCipher_collectionIds_List
554 , favorite: Bool
658 , favorite: Bool
555 , id: String
659 , id: String
556 , name: String
660 , name: String
@@ -559,8 +663,9 @@ type alias FullCipher =
559
663
560jsonDecFullCipher : Json.Decode.Decoder ( FullCipher )
664jsonDecFullCipher : Json.Decode.Decoder ( FullCipher )
561jsonDecFullCipher =
665jsonDecFullCipher =
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})
563 |> required "cipher" (jsonDecCipher)
667 |> required "cipher" (jsonDecCipher)
668+ |> required "collectionIds" (jsonDecFullCipher_collectionIds_List)
564 |> required "favorite" (Json.Decode.bool)
669 |> required "favorite" (Json.Decode.bool)
565 |> required "id" (Json.Decode.string)
670 |> required "id" (Json.Decode.string)
566 |> required "name" (Json.Decode.string)
671 |> required "name" (Json.Decode.string)
@@ -570,6 +675,7 @@ jsonEncFullCipher : FullCipher -> Value
570jsonEncFullCipher val =
675jsonEncFullCipher val =
571 Json.Encode.object
676 Json.Encode.object
572 [ ("cipher", jsonEncCipher val.cipher)
677 [ ("cipher", jsonEncCipher val.cipher)
678+ , ("collectionIds", jsonEncFullCipher_collectionIds_List val.collectionIds)
573 , ("favorite", Json.Encode.bool val.favorite)
679 , ("favorite", Json.Encode.bool val.favorite)
574 , ("id", Json.Encode.string val.id)
680 , ("id", Json.Encode.string val.id)
575 , ("name", Json.Encode.string val.name)
681 , ("name", Json.Encode.string val.name)
@@ -694,9 +800,44 @@ jsonEncSub_NeedsMasterPassword val =
694
800
695
801
696
802
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+
697type Sub =
837type Sub =
698 CaptchaDone
838 CaptchaDone
699 | CipherChanged FullCipher
839 | CipherChanged FullCipher
840+ | CipherDeleted FullCipher
700 | Error String
841 | Error String
701 | GeneratedPassword String
842 | GeneratedPassword String
702 | LoadCipher FullCipher
843 | LoadCipher FullCipher
@@ -705,14 +846,18 @@ type Sub =
705 | NeedsCaptcha String
846 | NeedsCaptcha String
706 | NeedsLogin
847 | NeedsLogin
707 | NeedsMasterPassword Sub_NeedsMasterPassword
848 | NeedsMasterPassword Sub_NeedsMasterPassword
849+ | NeedsSecondFactor Sub_NeedsSecondFactor_List
708 | RecieveEmail String
850 | RecieveEmail String
709 | Reset
851 | Reset
852+ | Totp Sub_Totp
853+ | WrongPassword
710
854
711jsonDecSub : Json.Decode.Decoder ( Sub )
855jsonDecSub : Json.Decode.Decoder ( Sub )
712jsonDecSub =
856jsonDecSub =
713 let jsonDecDictSub = Dict.fromList
857 let jsonDecDictSub = Dict.fromList
714 [ ("CaptchaDone", Json.Decode.lazy (\_ -> Json.Decode.succeed CaptchaDone))
858 [ ("CaptchaDone", Json.Decode.lazy (\_ -> Json.Decode.succeed CaptchaDone))
715 , ("CipherChanged", Json.Decode.lazy (\_ -> Json.Decode.map CipherChanged (jsonDecFullCipher)))
859 , ("CipherChanged", Json.Decode.lazy (\_ -> Json.Decode.map CipherChanged (jsonDecFullCipher)))
860+ , ("CipherDeleted", Json.Decode.lazy (\_ -> Json.Decode.map CipherDeleted (jsonDecFullCipher)))
716 , ("Error", Json.Decode.lazy (\_ -> Json.Decode.map Error (Json.Decode.string)))
861 , ("Error", Json.Decode.lazy (\_ -> Json.Decode.map Error (Json.Decode.string)))
717 , ("GeneratedPassword", Json.Decode.lazy (\_ -> Json.Decode.map GeneratedPassword (Json.Decode.string)))
862 , ("GeneratedPassword", Json.Decode.lazy (\_ -> Json.Decode.map GeneratedPassword (Json.Decode.string)))
718 , ("LoadCipher", Json.Decode.lazy (\_ -> Json.Decode.map LoadCipher (jsonDecFullCipher)))
863 , ("LoadCipher", Json.Decode.lazy (\_ -> Json.Decode.map LoadCipher (jsonDecFullCipher)))
@@ -721,8 +866,11 @@ jsonDecSub =
721 , ("NeedsCaptcha", Json.Decode.lazy (\_ -> Json.Decode.map NeedsCaptcha (Json.Decode.string)))
866 , ("NeedsCaptcha", Json.Decode.lazy (\_ -> Json.Decode.map NeedsCaptcha (Json.Decode.string)))
722 , ("NeedsLogin", Json.Decode.lazy (\_ -> Json.Decode.succeed NeedsLogin))
867 , ("NeedsLogin", Json.Decode.lazy (\_ -> Json.Decode.succeed NeedsLogin))
723 , ("NeedsMasterPassword", Json.Decode.lazy (\_ -> Json.Decode.map NeedsMasterPassword (jsonDecSub_NeedsMasterPassword)))
868 , ("NeedsMasterPassword", Json.Decode.lazy (\_ -> Json.Decode.map NeedsMasterPassword (jsonDecSub_NeedsMasterPassword)))
869+ , ("NeedsSecondFactor", Json.Decode.lazy (\_ -> Json.Decode.map NeedsSecondFactor (jsonDecSub_NeedsSecondFactor_List)))
724 , ("RecieveEmail", Json.Decode.lazy (\_ -> Json.Decode.map RecieveEmail (Json.Decode.string)))
870 , ("RecieveEmail", Json.Decode.lazy (\_ -> Json.Decode.map RecieveEmail (Json.Decode.string)))
725 , ("Reset", Json.Decode.lazy (\_ -> Json.Decode.succeed Reset))
871 , ("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))
726 ]
874 ]
727 jsonDecObjectSetSub = Set.fromList []
875 jsonDecObjectSetSub = Set.fromList []
728 in decodeSumTaggedObject "Sub" "tag" "contents" jsonDecDictSub jsonDecObjectSetSub
876 in decodeSumTaggedObject "Sub" "tag" "contents" jsonDecDictSub jsonDecObjectSetSub
@@ -732,6 +880,7 @@ jsonEncSub val =
732 let keyval v = case v of
880 let keyval v = case v of
733 CaptchaDone -> ("CaptchaDone", encodeValue (Json.Encode.list identity []))
881 CaptchaDone -> ("CaptchaDone", encodeValue (Json.Encode.list identity []))
734 CipherChanged v1 -> ("CipherChanged", encodeValue (jsonEncFullCipher v1))
882 CipherChanged v1 -> ("CipherChanged", encodeValue (jsonEncFullCipher v1))
883+ CipherDeleted v1 -> ("CipherDeleted", encodeValue (jsonEncFullCipher v1))
735 Error v1 -> ("Error", encodeValue (Json.Encode.string v1))
884 Error v1 -> ("Error", encodeValue (Json.Encode.string v1))
736 GeneratedPassword v1 -> ("GeneratedPassword", encodeValue (Json.Encode.string v1))
885 GeneratedPassword v1 -> ("GeneratedPassword", encodeValue (Json.Encode.string v1))
737 LoadCipher v1 -> ("LoadCipher", encodeValue (jsonEncFullCipher v1))
886 LoadCipher v1 -> ("LoadCipher", encodeValue (jsonEncFullCipher v1))
@@ -740,7 +889,39 @@ jsonEncSub val =
740 NeedsCaptcha v1 -> ("NeedsCaptcha", encodeValue (Json.Encode.string v1))
889 NeedsCaptcha v1 -> ("NeedsCaptcha", encodeValue (Json.Encode.string v1))
741 NeedsLogin -> ("NeedsLogin", encodeValue (Json.Encode.list identity []))
890 NeedsLogin -> ("NeedsLogin", encodeValue (Json.Encode.list identity []))
742 NeedsMasterPassword v1 -> ("NeedsMasterPassword", encodeValue (jsonEncSub_NeedsMasterPassword v1))
891 NeedsMasterPassword v1 -> ("NeedsMasterPassword", encodeValue (jsonEncSub_NeedsMasterPassword v1))
892+ NeedsSecondFactor v1 -> ("NeedsSecondFactor", encodeValue (jsonEncSub_NeedsSecondFactor_List v1))
743 RecieveEmail v1 -> ("RecieveEmail", encodeValue (Json.Encode.string v1))
893 RecieveEmail v1 -> ("RecieveEmail", encodeValue (Json.Encode.string v1))
744 Reset -> ("Reset", encodeValue (Json.Encode.list identity []))
894 Reset -> ("Reset", encodeValue (Json.Encode.list identity []))
895+ Totp v1 -> ("Totp", encodeValue (jsonEncSub_Totp v1))
896+ WrongPassword -> ("WrongPassword", encodeValue (Json.Encode.list identity []))
745 in encodeSumTaggedObject "tag" "contents" keyval val
897 in encodeSumTaggedObject "tag" "contents" keyval val
746
898
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
6type Event
6type Event
7 = UpdateCipher Bridge.FullCipher
7 = UpdateCipher Bridge.FullCipher
8 | GeneratedPassword String
8 | GeneratedPassword String
9+ | DecodedTotp Bridge.Sub_Totp
frontend/Logic/Cipher.elm+3−2
@@ -49,14 +49,15 @@ normalizeIdentityCipher { address1, address2, address3, city, company, country,
49 , ssn = normalizeStringMaybe ssn
49 , ssn = normalizeStringMaybe ssn
50 , state = normalizeStringMaybe state
50 , state = normalizeStringMaybe state
51 , title = normalizeStringMaybe title
51 , title = normalizeStringMaybe title
52− , username = username
52+ , username = normalizeStringMaybe username
53 }
53 }
54
54
55
55
56normalizeLoginCipher : Bridge.Cipher_LoginCipher -> Bridge.Cipher_LoginCipher
56normalizeLoginCipher : Bridge.Cipher_LoginCipher -> Bridge.Cipher_LoginCipher
57−normalizeLoginCipher { password, uris, username } =
57+normalizeLoginCipher { password, totp, uris, username } =
58 { password = password
58 { password = password
59 , uris = uris |> List.map String.trim |> List.filter String.isEmpty
59 , uris = uris |> List.map String.trim |> List.filter String.isEmpty
60+ , totp = normalizeStringMaybe totp
60 , username = username
61 , username = username
61 }
62 }
62
63
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)
2
2
3import Bridge
3import Bridge
4import Browser
4import Browser
@@ -16,6 +16,8 @@ import Pages.Loader exposing (loader)
16import Pages.Login as Login
16import Pages.Login as Login
17import Pages.MasterPassword as MasterPassword
17import Pages.MasterPassword as MasterPassword
18import Pages.Navigation as Navigation exposing (TopButton(..))
18import Pages.Navigation as Navigation exposing (TopButton(..))
19+import Pages.SecondFactor as SecondFactor
20+import Pages.SecondFactorSelect as SecondFactorSelect
19import Task
21import Task
20import Time
22import Time
21import Types exposing (CipherId)
23import Types exposing (CipherId)
@@ -28,38 +30,51 @@ notificationLingerSeconds =
28
30
29
31
30type Msg
32type Msg
31− = Noop
32− | RecieveMessage String
33− | ShowLoginPage
33+ = ShowLoginPage
34− | ShowError String
34+ | ShowError String String
35 | ShowInfo String String
35 | ShowInfo String String
36 | CloseNotification
36 | CloseNotification
37− | SubmitLogin { email : String, password : String, server : String }
37+ | SubmitLogin Bridge.Cmd_Login
38− | LoginMsg Login.Msg
39− | CiphersMsg Ciphers.Msg
40 | LoadCiphers Bridge.Sub_LoadCiphers_List
38 | LoadCiphers Bridge.Sub_LoadCiphers_List
41− | OpenCiphersScreen
39+ | LoginSuccessful
42 | Reset
40 | Reset
43 | NeedsReset
41 | NeedsReset
44− | MasterPasswordMsg MasterPassword.Msg
45 | SendMasterPassword { password : String }
42 | SendMasterPassword { password : String }
46 | ShowMasterPasswordPage { server : String, login : String }
43 | ShowMasterPasswordPage { server : String, login : String }
47− | CipherMsg Cipher.Msg
48 | ShowCipherPage Bridge.FullCipher
44 | ShowCipherPage Bridge.FullCipher
49 | RequestCipher CipherId
45 | RequestCipher CipherId
50 | PopView
46 | PopView
51 | RecieveEmail String
47 | RecieveEmail String
52 | Copy String
48 | Copy String
53 | Open String
49 | Open String
54− | CaptchaMsg Captcha.Msg
55− | EditCipherMsg EditCipher.Msg
56 | ShowCaptcha Captcha.HCaptchSiteKey
50 | ShowCaptcha Captcha.HCaptchSiteKey
57 | ClearNotification { currentTime : Time.Posix }
51 | ClearNotification { currentTime : Time.Posix }
58 | UpdateLastNotificatioTime Time.Posix
52 | UpdateLastNotificatioTime Time.Posix
59 | EditCipher Bridge.FullCipher
53 | EditCipher Bridge.FullCipher
60 | UpdateCipher Bridge.FullCipher
54 | UpdateCipher Bridge.FullCipher
55+ | CreateCipher Bridge.FullCipher
61 | FireGlobalEvent Event
56 | FireGlobalEvent Event
62 | GeneratePassword Bridge.PasswordGeneratorConfig
57 | 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
63
78
64
79
65type alias Model =
80type alias Model =
@@ -70,19 +85,45 @@ type alias Model =
70 }
85 }
71
86
72
87
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+
73appendNotification : Notification.Config -> Model -> ( Model, Cmd Msg )
112appendNotification : Notification.Config -> Model -> ( Model, Cmd Msg )
74appendNotification cfg model =
113appendNotification cfg model =
75 ( { model | notifications = cfg :: model.notifications }, Task.perform UpdateLastNotificatioTime Time.now )
114 ( { model | notifications = cfg :: model.notifications }, Task.perform UpdateLastNotificatioTime Time.now )
76
115
77
116
78type PageModel
117type PageModel
79− = LoginModel Login.Model
118+ = LoginModel (Login.Model Msg)
80 | LoadingPage
119 | LoadingPage
81− | CiphersModel Ciphers.Model
120+ | CiphersModel (Ciphers.Model Msg)
82− | EditCipherModel EditCipher.Model
121+ | EditCipherModel (EditCipher.Model Msg)
83− | MasterPasswordModel MasterPassword.Model
122+ | MasterPasswordModel (MasterPassword.Model Msg)
84− | CipherModel Cipher.Model
123+ | CipherModel (Cipher.Model Msg)
85 | CaptchaModel Captcha.Model
124 | CaptchaModel Captcha.Model
125+ | SecondFactorSelectModel (SecondFactorSelect.Model Msg)
126+ | SecondFactorModel (SecondFactor.Model Msg)
86
127
87
128
88showPage :
129showPage :
@@ -97,31 +138,43 @@ showPage email page =
97 let
138 let
98 simpleBackButton =
139 simpleBackButton =
99 BackButton { action = PopView, icon = Nothing }
140 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+ }
100 in
159 in
101 case page of
160 case page of
102 LoginModel model ->
161 LoginModel model ->
103− let
162+ liftPageWith LoginMsg
104− p =
105− Login.page loginCallbacks LoginMsg
106− in
107− { title = p.title model
163+ { title = [ text Login.title ]
108− , body = p.view model
164+ , body = Login.view model
109− , topButton = Nothing
165+ , topButton = Nothing
110− }
166+ }
111
167
112 CiphersModel model ->
168 CiphersModel model ->
113− let
169+ liftPageWith CiphersMsg
114− p =
115− Ciphers.page ciphersCallbacks CiphersMsg
116− in
117− { title = p.title model
170+ { title = Ciphers.title model
118− , body = p.view model
171+ , body = Ciphers.view model
119− , topButton =
172+ , topButton =
120− Ciphers.menuConfig email model
173+ Ciphers.menuConfig email model
121− |> Navigation.mapMenuConfig CiphersMsg
174+ |> Navigation.mapMenuConfig (CiphersMsg >> PageMsg)
122− |> Navigation.MenuButton
175+ |> Navigation.MenuButton
123− |> Just
176+ |> Just
124− }
177+ }
125
178
126 LoadingPage ->
179 LoadingPage ->
127 { title = []
180 { title = []
@@ -130,44 +183,45 @@ showPage email page =
130 }
183 }
131
184
132 MasterPasswordModel model ->
185 MasterPasswordModel model ->
133− let
186+ liftPageWith MasterPasswordMsg
134− p =
135− MasterPassword.page masterPasswordCallbacks MasterPasswordMsg
136− in
137− { title = p.title model
187+ { title = [ text MasterPassword.title ]
138− , body = p.view model
188+ , body = MasterPassword.view model
139− , topButton = Nothing
189+ , topButton = Nothing
140− }
190+ }
141
191
142 CipherModel model ->
192 CipherModel model ->
143− let
193+ liftPageWith CipherMsg
144− p =
145− Cipher.page cipherCallbacks CipherMsg
146− in
147− { title = p.title model
194+ { title = Cipher.title model
148− , body = p.view model
195+ , body = Cipher.view model
149− , topButton = Just simpleBackButton
196+ , topButton = Just simpleBackButton
150− }
197+ }
151
198
152 CaptchaModel model ->
199 CaptchaModel model ->
153− let
154− p =
155− Captcha.page captchaCallbacks CaptchaMsg
156− in
157− { title = p.title model
200+ { title = [ text Captcha.title ]
158− , body = p.view model
201+ , body = Captcha.view model
159 , topButton = Just simpleBackButton
202 , topButton = Just simpleBackButton
160 }
203 }
161
204
162 EditCipherModel model ->
205 EditCipherModel model ->
163− let
206+ liftPageWith EditCipherMsg
164− p =
165− EditCipher.page editCipherCallbacks EditCipherMsg
166− in
167− { title = p.title model
207+ { title = EditCipher.title model
168− , body = p.view model
208+ , body = EditCipher.view model
169− , topButton = Just (BackButton { action = PopView, icon = Just "close" })
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
170− }
224+ }
171
225
172
226
173main : Program () Model Msg
227main : Program () Model Msg
@@ -183,20 +237,20 @@ main =
183subscriptions : Model -> Sub Msg
237subscriptions : Model -> Sub Msg
184subscriptions model =
238subscriptions model =
185 Sub.batch
239 Sub.batch
186− ([ getBridge ShowError
240+ ([ getBridge (ShowError "Something went wrong!")
187 (\msg ->
241 (\msg ->
188 case msg of
242 case msg of
189 Bridge.NeedsLogin ->
243 Bridge.NeedsLogin ->
190 ShowLoginPage
244 ShowLoginPage
191
245
192 Bridge.Error err ->
246 Bridge.Error err ->
193− ShowError err
247+ ShowError "Something went wrong!" err
194
248
195 Bridge.LoadCiphers ciphers ->
249 Bridge.LoadCiphers ciphers ->
196 LoadCiphers ciphers
250 LoadCiphers ciphers
197
251
198 Bridge.LoginSuccessful ->
252 Bridge.LoginSuccessful ->
199− OpenCiphersScreen
253+ LoginSuccessful
200
254
201 Bridge.Reset ->
255 Bridge.Reset ->
202 Reset
256 Reset
@@ -217,13 +271,66 @@ subscriptions model =
217 PopView
271 PopView
218
272
219 Bridge.CipherChanged c ->
273 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.")
221
275
222 Bridge.GeneratedPassword password ->
276 Bridge.GeneratedPassword password ->
223 GlobalEvents.GeneratedPassword password |> FireGlobalEvent
277 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
224 )
298 )
225 ]
299 ]
226 ++ optional (List.isEmpty model.notifications |> not) (Time.every 1000 (\t -> ClearNotification { currentTime = t }))
300 ++ 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+ )
227 )
334 )
228
335
229
336
@@ -242,7 +349,10 @@ view : Model -> Html Msg
242view model =
349view model =
243 div []
350 div []
244 (maybeList (List.head model.notifications) (Lazy.lazy2 Notification.notification CloseNotification)
351 (maybeList (List.head model.notifications) (Lazy.lazy2 Notification.notification CloseNotification)
352+ ++ [ Lazy.lazy2 Navigation.showNavigationView
353+ model.pageStack
245− ++ [ Lazy.lazy2 Navigation.showNavigationView model.pageStack (showPage (Maybe.withDefault "" model.userEmail)) ]
354+ (showPage (Maybe.withDefault "" model.userEmail))
355+ ]
246 )
356 )
247
357
248
358
@@ -254,12 +364,6 @@ mapHead f (Nonempty x xx) =
254doNotStoreInHistory : PageModel -> Bool
364doNotStoreInHistory : PageModel -> Bool
255doNotStoreInHistory page =
365doNotStoreInHistory page =
256 case page of
366 case page of
257− LoginModel _ ->
258− True
259−
260− MasterPasswordModel _ ->
261− True
262−
263 LoadingPage ->
367 LoadingPage ->
264 True
368 True
265
369
@@ -270,19 +374,9 @@ doNotStoreInHistory page =
270update : Msg -> Model -> ( Model, Cmd Msg )
374update : Msg -> Model -> ( Model, Cmd Msg )
271update msg model =
375update msg model =
272 let
376 let
273− processPage : (pageModel -> PageModel) -> ( Result String pageModel, Cmd Msg ) -> ( Model, Cmd Msg )
377+ updatePageStackHead : PageModel -> Model
274− processPage liftModel ( resultModel, cmd ) =
378+ updatePageStackHead mdl =
275− case resultModel of
276− Ok mdl ->
277− ( { model | pageStack = mapHead (always (liftModel mdl)) model.pageStack }, cmd )
379+ { model | pageStack = mapHead (always mdl) model.pageStack }
278−
279− Err err ->
280− appendNotification
281− { title = "An error had occured"
282− , message = err
283− , severity = Notification.Error
284− }
285− model
286
380
287 appendPageStack : PageModel -> Model
381 appendPageStack : PageModel -> Model
288 appendPageStack mdl =
382 appendPageStack mdl =
@@ -291,22 +385,11 @@ update msg model =
291 keepStackWith : PageModel -> Model
385 keepStackWith : PageModel -> Model
292 keepStackWith mdl =
386 keepStackWith mdl =
293 { model | pageStack = Nonempty.cons mdl model.pageStack }
387 { model | pageStack = Nonempty.cons mdl model.pageStack }
294−
295− currentPage =
296− model.pageStack |> Nonempty.head
297 in
388 in
298 case msg of
389 case msg of
299− RecieveMessage x ->
390+ ShowError title err ->
300 appendNotification
391 appendNotification
301− { title = "Got a message!"
392+ { title = title
302− , message = x
303− , severity = Notification.Info
304− }
305− model
306−
307− ShowError err ->
308− appendNotification
309− { title = "Something went wrong"
310 , message = err
393 , message = err
311 , severity = Notification.Error
394 , severity = Notification.Error
312 }
395 }
@@ -326,29 +409,14 @@ update msg model =
326 )
409 )
327
410
328 SubmitLogin data ->
411 SubmitLogin data ->
329− ( appendPageStack <| LoadingPage
412+ ( keepStackWith <| LoadingPage
330 , FFI.sendBridge (Bridge.Login data)
413 , FFI.sendBridge (Bridge.Login data)
331 )
414 )
332
415
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−
349 ShowLoginPage ->
416 ShowLoginPage ->
350− (Login.page loginCallbacks LoginMsg).init ()
417+ ( Login.init loginCallbacks |> LoginModel |> appendPageStack
351− |> Tuple.mapFirst (\pageModel -> appendPageStack <| LoginModel pageModel)
418+ , Cmd.none
419+ )
352
420
353 LoadCiphers ciphers ->
421 LoadCiphers ciphers ->
354 let
422 let
@@ -364,9 +432,6 @@ update msg model =
364 _ ->
432 _ ->
365 False
433 False
366 )
434 )
367−
368− ( pageModel, cmd ) =
369− (Ciphers.page ciphersCallbacks CiphersMsg).init ciphers
370 in
435 in
371 ( if ciphersShown then
436 ( if ciphersShown then
372 { model
437 { model
@@ -375,8 +440,8 @@ update msg model =
375 |> Nonempty.map
440 |> Nonempty.map
376 (\x ->
441 (\x ->
377 case x of
442 case x of
378− CiphersModel _ ->
443+ CiphersModel pageModel ->
379− CiphersModel pageModel
444+ CiphersModel (Ciphers.updateCiphers ciphers pageModel)
380
445
381 other ->
446 other ->
382 other
447 other
@@ -384,21 +449,24 @@ update msg model =
384 }
449 }
385
450
386 else
451 else
387− appendPageStack <| CiphersModel pageModel
452+ appendPageStack <| CiphersModel (Ciphers.init ciphersCallbacks ciphers)
388− , cmd
453+ , Cmd.none
389 )
454 )
390
455
391− -- (Ciphers.page ciphersCallbacks CiphersMsg).init ciphers
456+ LoginSuccessful ->
392− -- |> Tuple.mapFirst (\pageModel -> appendPageStack <| CiphersModel pageModel)
457+ ( { model | pageStack = Nonempty.singleton LoadingPage }
393− OpenCiphersScreen ->
394− ( appendPageStack <| LoadingPage, Cmd.batch [ sendBridge Bridge.NeedCiphersList, sendBridge Bridge.NeedEmail ] )
458+ , Cmd.batch [ sendBridge Bridge.NeedCiphersList, sendBridge Bridge.NeedEmail ]
459+ )
395
460
396 Reset ->
461 Reset ->
397 init
462 init
398
463
399 ShowMasterPasswordPage { server, login } ->
464 ShowMasterPasswordPage { server, login } ->
400− (MasterPassword.page masterPasswordCallbacks MasterPasswordMsg).init { server = server, login = login }
465+ ( MasterPassword.init masterPasswordCallbacks { server = server, login = login }
401− |> Tuple.mapFirst (\pageModel -> appendPageStack <| MasterPasswordModel pageModel)
466+ |> MasterPasswordModel
467+ |> appendPageStack
468+ , Cmd.none
469+ )
402
470
403 NeedsReset ->
471 NeedsReset ->
404 ( appendPageStack <| LoadingPage, sendBridge Bridge.NeedsReset )
472 ( appendPageStack <| LoadingPage, sendBridge Bridge.NeedsReset )
@@ -406,32 +474,8 @@ update msg model =
406 SendMasterPassword { password } ->
474 SendMasterPassword { password } ->
407 ( appendPageStack <| LoadingPage, sendBridge (Bridge.SendMasterPassword password) )
475 ( appendPageStack <| LoadingPage, sendBridge (Bridge.SendMasterPassword password) )
408
476
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−
433 ShowCipherPage cipher ->
477 ShowCipherPage cipher ->
434− (Cipher.page cipherCallbacks CipherMsg).init cipher
478+ Cipher.init cipherCallbacks cipher
435 |> Tuple.mapFirst (\pageModel -> appendPageStack <| CipherModel pageModel)
479 |> Tuple.mapFirst (\pageModel -> appendPageStack <| CipherModel pageModel)
436
480
437 RequestCipher id ->
481 RequestCipher id ->
@@ -452,16 +496,7 @@ update msg model =
452 ( model, FFI.sendBridge (Bridge.Open uri) )
496 ( model, FFI.sendBridge (Bridge.Open uri) )
453
497
454 ShowCaptcha uri ->
498 ShowCaptcha uri ->
455− (Captcha.page captchaCallbacks CaptchaMsg).init uri
499+ ( Captcha.init uri |> CaptchaModel |> appendPageStack, Cmd.none )
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 )
465
500
466 ClearNotification { currentTime } ->
501 ClearNotification { currentTime } ->
467 if Time.posixToMillis model.lastNotificationTime + notificationLingerSeconds * 1000 > Time.posixToMillis currentTime then
502 if Time.posixToMillis model.lastNotificationTime + notificationLingerSeconds * 1000 > Time.posixToMillis currentTime then
@@ -475,11 +510,8 @@ update msg model =
475 UpdateLastNotificatioTime time ->
510 UpdateLastNotificatioTime time ->
476 ( { model | lastNotificationTime = time }, Cmd.none )
511 ( { model | lastNotificationTime = time }, Cmd.none )
477
512
478− Noop ->
479− ( model, Cmd.none )
480−
481 EditCipher cipher ->
513 EditCipher cipher ->
482− (EditCipher.page editCipherCallbacks EditCipherMsg).init cipher
514+ EditCipher.init { fullCipher = cipher, callbacks = editCipherCallbacks }
483 |> Tuple.mapFirst (\pageModel -> keepStackWith <| EditCipherModel pageModel)
515 |> Tuple.mapFirst (\pageModel -> keepStackWith <| EditCipherModel pageModel)
484
516
485 UpdateCipher cipher ->
517 UpdateCipher cipher ->
@@ -491,6 +523,14 @@ update msg model =
491 ]
523 ]
492 )
524 )
493
525
526+ CreateCipher cipher ->
527+ ( model
528+ , Cmd.batch
529+ [ Bridge.CreateCipher cipher |> sendBridge
530+ , PopView |> pureCmd
531+ ]
532+ )
533+
494 FireGlobalEvent ev ->
534 FireGlobalEvent ev ->
495 ( { model
535 ( { model
496 | pageStack =
536 | pageStack =
@@ -502,22 +542,28 @@ update msg model =
502 LoadingPage
542 LoadingPage
503
543
504 LoginModel m ->
544 LoginModel m ->
505− (Login.page loginCallbacks LoginMsg).event m ev |> LoginModel
545+ LoginModel m
506
546
507 CiphersModel m ->
547 CiphersModel m ->
508− (Ciphers.page ciphersCallbacks CiphersMsg).event m ev |> CiphersModel
548+ Ciphers.event m ev |> CiphersModel
509
549
510 MasterPasswordModel m ->
550 MasterPasswordModel m ->
511− (MasterPassword.page masterPasswordCallbacks MasterPasswordMsg).event m ev |> MasterPasswordModel
551+ MasterPasswordModel m
512
552
513 CipherModel m ->
553 CipherModel m ->
514− (Cipher.page cipherCallbacks CipherMsg).event m ev |> CipherModel
554+ Cipher.event m ev |> CipherModel
515
555
516 EditCipherModel m ->
556 EditCipherModel m ->
517− (EditCipher.page editCipherCallbacks EditCipherMsg).event m ev |> EditCipherModel
557+ EditCipher.event m ev |> EditCipherModel
518
558
519 CaptchaModel m ->
559 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
521 )
567 )
522 }
568 }
523 , Cmd.none
569 , Cmd.none
@@ -526,30 +572,197 @@ update msg model =
526 GeneratePassword cfg ->
572 GeneratePassword cfg ->
527 ( model, FFI.sendBridge (Bridge.GeneratePassword cfg) )
573 ( model, FFI.sendBridge (Bridge.GeneratePassword cfg) )
528
574
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+
529
730
530loginCallbacks : Login.Callbacks Msg
731loginCallbacks : Login.Callbacks Msg
531loginCallbacks =
732loginCallbacks =
733+ { submit =
734+ \{ email, server, password } ->
532− { submit = SubmitLogin }
735+ SubmitLogin
736+ { email = email
737+ , password = password
738+ , server = server
739+ , secondFactor = Nothing
740+ }
741+ }
533
742
534
743
535ciphersCallbacks : Ciphers.Callbacks Msg
744ciphersCallbacks : Ciphers.Callbacks Msg
536ciphersCallbacks =
745ciphersCallbacks =
537− { selected = RequestCipher, logOut = NeedsReset }
746+ { selected = RequestCipher
747+ , logOut = NeedsReset
748+ , createNewCipher = OpenNewCipherEditPage
749+ , lift = CiphersMsg >> PageMsg
750+ }
538
751
539
752
540cipherCallbacks : Cipher.Callbacks Msg
753cipherCallbacks : Cipher.Callbacks Msg
541cipherCallbacks =
754cipherCallbacks =
542− { copy = Copy, open = Open, edit = EditCipher }
755+ { copy = Copy, open = Open, edit = EditCipher, needTotp = RequestTotp }
543
756
544
757
545editCipherCallbacks : EditCipher.Callbacks Msg
758editCipherCallbacks : EditCipher.Callbacks Msg
546editCipherCallbacks =
759editCipherCallbacks =
547− { save = UpdateCipher, generatePassword = GeneratePassword }
760+ { save = UpdateCipher, generatePassword = GeneratePassword, delete = Just DeleteCipher }
548
761
549
762
550−captchaCallbacks : Captcha.Callbacks
763+createCipherCallbacks : EditCipher.Callbacks Msg
551−captchaCallbacks =
764+createCipherCallbacks =
552− {}
765+ { save = CreateCipher, generatePassword = GeneratePassword, delete = Nothing }
553
766
554
767
555masterPasswordCallbacks : MasterPassword.Callbacks Msg
768masterPasswordCallbacks : MasterPassword.Callbacks Msg
@@ -557,3 +770,11 @@ masterPasswordCallbacks =
557 { submit = SendMasterPassword
770 { submit = SendMasterPassword
558 , reset = NeedsReset
771 , reset = NeedsReset
559 }
772 }
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 @@
1module Page exposing (..)
1module Page exposing (..)
2
2
3−import GlobalEvents exposing (Event)
3+import Browser
4import Html exposing (..)
4import Html exposing (..)
5import Html.Events exposing (..)
5import Html.Events exposing (..)
6
6
7
7
8−type alias Env =
8+page :
9− {}
9+ { init : model
10−
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 )
11
24
12−type alias Page init model imsg emsg =
25+ Just msg ->
13− (imsg -> emsg)
26+ update msg model
14− -> InitializedPage init model imsg emsg
27+ }
15
28
16
29
17−type alias InitializedPage init model imsg emsg =
30+noUpdate : a -> model -> ( model, Cmd msg )
31+noUpdate _ model =
18− { init : init -> ( model, Cmd emsg )
32+ ( model, Cmd.none )
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− }
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)
2
2
3import Html exposing (..)
3import Html exposing (..)
4import Html.Attributes as Attr
4import Html.Attributes as Attr
@@ -6,6 +6,15 @@ import Page exposing (..)
6import Utils exposing (..)
6import Utils exposing (..)
7
7
8
8
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+
9type alias HCaptchSiteKey =
18type alias HCaptchSiteKey =
10 String
19 String
11
20
@@ -15,44 +24,22 @@ type alias Model =
15 }
24 }
16
25
17
26
18−type Msg
27+title : String
19− = Noop
28+title =
20−
29+ "Captcha"
21
30
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− }
35
31
36−
37−init : HCaptchSiteKey -> ( Model, Cmd Msg )
32+init : HCaptchSiteKey -> Model
38init siteKey =
33init siteKey =
39− ( { siteKey = siteKey }, Cmd.none )
34+ { siteKey = siteKey }
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
52
35
53
36
54−view : Model -> List (Html Msg)
37+view : Model -> List (Html msg)
55view { siteKey } =
38view { siteKey } =
56− [ div [ Attr.class "u-embedded-media" ]
39+ [ iframe
57− [ iframe [ Attr.src ("https://iko.soy/warden/hcaptcha.html?" ++ siteKey) ] [] ]
40+ [ Attr.src ("https://iko.soy/warden/hcaptcha.html?" ++ siteKey)
41+ , Attr.style "width" "100%"
42+ , Attr.style "height" "80vh"
43+ ]
44+ []
58 ]
45 ]
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+ )
2
12
3import Bridge
13import Bridge
4import GlobalEvents
14import GlobalEvents
@@ -6,13 +16,16 @@ import Html exposing (..)
6import Html.Attributes as Attr
16import Html.Attributes as Attr
7import Html.Events as Ev
17import Html.Events as Ev
8import Page exposing (..)
18import Page exposing (..)
19+import Time
9import Utils exposing (..)
20import Utils exposing (..)
10
21
11
22
12−type alias Model =
23+type alias Model emsg =
13 { cipher : Bridge.FullCipher
24 { cipher : Bridge.FullCipher
14 , passwordHidden : Bool
25 , passwordHidden : Bool
15 , cvvHidden : Bool
26 , cvvHidden : Bool
27+ , decodedTotp : Maybe { code : String, interval : Int }
28+ , callbacks : Callbacks emsg
16 }
29 }
17
30
18
31
@@ -21,73 +34,107 @@ type Msg
21 | ToggleCVVVisiblity
34 | ToggleCVVVisiblity
22 | Copy String
35 | Copy String
23 | Open String
36 | Open String
37+ | Edit Bridge.FullCipher
24
38
25
39
26type alias Callbacks msg =
40type alias Callbacks msg =
27 { copy : String -> msg
41 { copy : String -> msg
28 , open : String -> msg
42 , open : String -> msg
29 , edit : Bridge.FullCipher -> msg
43 , edit : Bridge.FullCipher -> msg
44+ , needTotp : String -> msg
30 }
45 }
31
46
32
47
33−page : Callbacks emsg -> Page Bridge.FullCipher Model Msg emsg
48+event : Model emsg -> GlobalEvents.Event -> Model 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 ->
49+event model ev =
46− case ev of
50+ case ev of
47− GlobalEvents.UpdateCipher c ->
51+ GlobalEvents.UpdateCipher c ->
48− { model
52+ { model
49− | cipher =
53+ | cipher =
50− if c.id == model.cipher.id then
54+ if c.id == model.cipher.id then
51− c
55+ c
52−
56+
53− else
57+ else
58+ model.cipher
59+ }
60+
61+ GlobalEvents.DecodedTotp { source, code, interval } ->
54− model.cipher
62+ case model.cipher.cipher of
55− }
63+ Bridge.LoginCipher { totp } ->
64+ if totp == Just source then
65+ { model | decodedTotp = Just { code = code, interval = interval } }
66+
67+ else
68+ model
56
69
57 _ ->
70 _ ->
58 model
71 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+ ]
60
82
61
83
62−init : Bridge.FullCipher -> ( Model, Cmd Msg )
84+init : Callbacks emsg -> Bridge.FullCipher -> ( Model emsg, Cmd emsg )
63−init cipher =
85+init callbacks cipher =
64 ( { cipher = cipher
86 ( { cipher = cipher
65 , passwordHidden = True
87 , passwordHidden = True
66 , cvvHidden = True
88 , cvvHidden = True
89+ , decodedTotp = Nothing
90+ , callbacks = callbacks
67 }
91 }
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+ _ ->
68− , Cmd.none
102+ Cmd.none
69 )
103 )
70
104
71
105
72−update : Callbacks emsg -> (Msg -> emsg) -> Msg -> Model -> ( Result String Model, Cmd emsg )
106+update : Msg -> Model emsg -> ( Model emsg, Cmd emsg )
73−update { copy, open } _ msg model =
107+update msg model =
74 case msg of
108 case msg of
75 TogglePasswordVisiblity ->
109 TogglePasswordVisiblity ->
76− ( Ok { model | passwordHidden = not model.passwordHidden }, Cmd.none )
110+ ( { model | passwordHidden = not model.passwordHidden }, Cmd.none )
77
111
78 ToggleCVVVisiblity ->
112 ToggleCVVVisiblity ->
79− ( Ok { model | cvvHidden = not model.cvvHidden }, Cmd.none )
113+ ( { model | cvvHidden = not model.cvvHidden }, Cmd.none )
80
114
81 Copy text ->
115 Copy text ->
82− ( Ok model, copy text |> pureCmd )
116+ ( model, model.callbacks.copy text |> pureCmd )
83
117
84 Open uri ->
118 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))
86
132
133+ Nothing ->
134+ Sub.none
87
135
88−subscriptions : Model -> Sub Msg
136+ _ ->
89−subscriptions _ =
90− Sub.none
137+ Sub.none
91
138
92
139
93row : { name : String, value : String, nameIcon : String, icons : List ( String, msg ) } -> Html msg
140row : { name : String, value : String, nameIcon : String, icons : List ( String, msg ) } -> Html msg
@@ -100,10 +147,10 @@ row { name, value, nameIcon, icons } =
100 ]
147 ]
101
148
102
149
103−view : Model -> List (Html Msg)
150+view : Model emsg -> List (Html Msg)
104−view { passwordHidden, cipher, cvvHidden } =
151+view { passwordHidden, cipher, cvvHidden, decodedTotp } =
105 (case cipher.cipher of
152 (case cipher.cipher of
106− Bridge.LoginCipher { username, password, uris } ->
153+ Bridge.LoginCipher { username, password, uris, totp } ->
107 maybeList username
154 maybeList username
108 (\x ->
155 (\x ->
109 row
156 row
@@ -123,11 +170,36 @@ view { passwordHidden, cipher, cvvHidden } =
123 , icons = [ ( hiddenButtonIcon passwordHidden, TogglePasswordVisiblity ), ( "copy", Copy x ) ]
170 , icons = [ ( hiddenButtonIcon passwordHidden, TogglePasswordVisiblity ), ( "copy", Copy x ) ]
124 }
171 }
125 )
172 )
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+ )
126 ++ (uris
194 ++ (uris
127 |> List.map
195 |> List.map
128 (\uri ->
196 (\uri ->
129 row
197 row
198+ { name = "URL"
199+ , value = uri
200+ , nameIcon = "get-link"
130− { name = "URL", value = uri, nameIcon = "get-link", icons = [ ( "external-link", Open uri ), ( "copy", Copy uri ) ] }
201+ , icons = [ ( "external-link", Open uri ), ( "copy", Copy uri ) ]
202+ }
131 )
203 )
132 )
204 )
133
205
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+ )
2
13
3import Bridge
14import Bridge
4import GlobalEvents
15import GlobalEvents
@@ -14,16 +25,23 @@ import Search
14import Utils exposing (..)
25import Utils exposing (..)
15
26
16
27
17−type alias Model =
28+type alias Model emsg =
18 { ciphers : Bridge.Sub_LoadCiphers_List
29 { ciphers : Bridge.Sub_LoadCiphers_List
19 , search : String
30 , search : String
20 , menuVisible : Bool
31 , menuVisible : Bool
21 , ciphersListFilter : CiphersFilter
32 , ciphersListFilter : CiphersFilter
22 , scroll : InfiniteScroll.Model Msg
33 , scroll : InfiniteScroll.Model Msg
23 , sublist : { end : Int }
34 , sublist : { end : Int }
35+ , cipherSelectDropdownVisivble : Bool
36+ , callbacks : Callbacks emsg
24 }
37 }
25
38
26
39
40+updateCiphers : Bridge.Sub_LoadCiphers_List -> Model emsg -> Model emsg
41+updateCiphers ciphers model =
42+ { model | ciphers = ciphers }
43+
44+
27type CiphersFilter
45type CiphersFilter
28 = AllCiphers
46 = AllCiphers
29 | SpecificCiphers Bridge.CipherType
47 | SpecificCiphers Bridge.CipherType
@@ -39,7 +57,7 @@ applyCipherFilter filter list =
39 list |> List.filter (.cipherType >> (==) t)
57 list |> List.filter (.cipherType >> (==) t)
40
58
41
59
42−menuConfig : String -> Model -> Navigation.MenuConfig Msg
60+menuConfig : String -> Model emsg -> Navigation.MenuConfig Msg
43menuConfig email { ciphersListFilter, menuVisible } =
61menuConfig email { ciphersListFilter, menuVisible } =
44 { title = email
62 { title = email
45 , items =
63 , items =
@@ -64,7 +82,7 @@ menuConfig email { ciphersListFilter, menuVisible } =
64 , current = ciphersListFilter == SpecificCiphers Bridge.CardType
82 , current = ciphersListFilter == SpecificCiphers Bridge.CardType
65 }
83 }
66 , { icon = "user"
84 , { icon = "user"
67− , name = "Identities"
85+ , name = "Contacts"
68 , trigger = UpdateFilter (SpecificCiphers Bridge.IdentityType)
86 , trigger = UpdateFilter (SpecificCiphers Bridge.IdentityType)
69 , current = ciphersListFilter == SpecificCiphers Bridge.IdentityType
87 , current = ciphersListFilter == SpecificCiphers Bridge.IdentityType
70 }
88 }
@@ -83,94 +101,137 @@ type Msg
83 | LogOut
101 | LogOut
84 | LoadMore InfiniteScroll.Direction
102 | LoadMore InfiniteScroll.Direction
85 | InfiniteScrollMsg InfiniteScroll.Msg
103 | InfiniteScrollMsg InfiniteScroll.Msg
104+ | CreateNewCipher Bridge.CipherType
105+ | ToggleSelectCipherTypeVisible
106+ | SelectCipher String
86
107
87
108
88type alias Callbacks msg =
109type alias Callbacks msg =
89 { selected : String -> msg
110 { selected : String -> msg
90 , logOut : msg
111 , logOut : msg
112+ , createNewCipher : Bridge.CipherType -> msg
113+ , lift : Msg -> msg
91 }
114 }
92
115
93
116
94−page : Callbacks emsg -> Page Bridge.Sub_LoadCiphers_List Model Msg emsg
117+event : Model emsg -> GlobalEvents.Event -> Model emsg
95−page callbacks liftMsg =
118+event model ev =
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
119+ case ev of
125− GlobalEvents.UpdateCipher c ->
120+ GlobalEvents.UpdateCipher c ->
126− { model
121+ { model
127− | ciphers =
122+ | ciphers =
128− model.ciphers
123+ model.ciphers
129− |> List.map
124+ |> List.map
130− (\cipher ->
125+ (\cipher ->
131− if cipher.id == c.id then
126+ if cipher.id == c.id then
132− { cipher | name = c.name }
127+ { cipher | name = c.name }
133−
134− else
135− cipher
136− )
137− }
138−
139− _ ->
140− model
141− }
142
128
129+ else
130+ cipher
131+ )
132+ }
133+
134+ _ ->
135+ model
143
136
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− )
155
137
138+title : Model emsg -> List (Html Msg)
139+title { ciphersListFilter, cipherSelectDropdownVisivble } =
140+ [ text
141+ (case ciphersListFilter of
142+ AllCiphers ->
143+ "All items"
156
144
157−update : Callbacks emsg -> (Msg -> emsg) -> Msg -> Model -> ( Result String Model, Cmd emsg )
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 )
158−update { logOut } liftMsg msg model =
219+update msg model =
159 case msg of
220 case msg of
160 Noop ->
221 Noop ->
161− ( Ok model, Cmd.none )
222+ ( model, Cmd.none )
162
223
163 UpdateSearch s ->
224 UpdateSearch s ->
164− ( Ok { model | search = s }, Cmd.none )
225+ ( { model | search = s }, Cmd.none )
165
226
166 ToggleMenu ->
227 ToggleMenu ->
167− ( Ok { model | menuVisible = not model.menuVisible }, Cmd.none )
228+ ( { model | menuVisible = not model.menuVisible }, Cmd.none )
168
229
169 UpdateFilter filter ->
230 UpdateFilter filter ->
170− ( Ok { model | ciphersListFilter = filter, menuVisible = False }, Cmd.none )
231+ ( { model | ciphersListFilter = filter, menuVisible = False }, Cmd.none )
171
232
172 LogOut ->
233 LogOut ->
173− ( Ok model, pureCmd logOut )
234+ ( model, pureCmd model.callbacks.logOut )
174
235
175 LoadMore direction ->
236 LoadMore direction ->
176 let
237 let
@@ -182,48 +243,54 @@ update { logOut } liftMsg msg model =
182 InfiniteScroll.Bottom ->
243 InfiniteScroll.Bottom ->
183 { end = min (model.sublist.end + 10) (List.length model.ciphers) }
244 { end = min (model.sublist.end + 10) (List.length model.ciphers) }
184 in
245 in
185− ( Ok { model | sublist = newSublist, scroll = InfiniteScroll.stopLoading model.scroll }, Cmd.none )
246+ ( { model | sublist = newSublist, scroll = InfiniteScroll.stopLoading model.scroll }, Cmd.none )
186
247
187 InfiniteScrollMsg msg_ ->
248 InfiniteScrollMsg msg_ ->
188 let
249 let
189 ( scroll, cmd ) =
250 ( scroll, cmd ) =
190 InfiniteScroll.update InfiniteScrollMsg msg_ model.scroll
251 InfiniteScroll.update InfiniteScrollMsg msg_ model.scroll
191 in
252 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) )
193
257
258+ ToggleSelectCipherTypeVisible ->
259+ ( { model | cipherSelectDropdownVisivble = not model.cipherSelectDropdownVisivble }
260+ , Cmd.none
261+ )
194
262
195−subscriptions : Model -> Sub Msg
263+ SelectCipher id ->
196−subscriptions _ =
264+ ( model, model.callbacks.selected id |> pureCmd )
197− Sub.none
198
265
199
266
200−view : Callbacks emsg -> (Msg -> emsg) -> Model -> List (Html emsg)
267+view : Model emsg -> List (Html Msg)
201−view { selected } liftMsg { ciphers, search, ciphersListFilter, sublist } =
268+view { ciphers, search, ciphersListFilter, sublist } =
202 [ input
269 [ input
203 [ Attr.type_ "search"
270 [ Attr.type_ "search"
204 , Attr.value search
271 , Attr.value search
205− , Ev.onInput (liftMsg << UpdateSearch)
272+ , Ev.onInput UpdateSearch
206 , Attr.placeholder "Search"
273 , Attr.placeholder "Search"
207 , Attr.attribute "autocomplete" "off"
274 , Attr.attribute "autocomplete" "off"
208 ]
275 ]
209 []
276 []
210 , ciphers
277 , ciphers
211 |> applyCipherFilter ciphersListFilter
278 |> 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)
213 ]
280 ]
214
281
215
282
216−showCiphers : (Msg -> msg) -> (String -> msg) -> Bridge.Sub_LoadCiphers_List -> Html msg
283+showCiphers : Bridge.Sub_LoadCiphers_List -> Html Msg
217−showCiphers liftMsg selected ciphers =
284+showCiphers ciphers =
218− Keyed.ul [ Attr.class "p-list--divided", InfiniteScroll.infiniteScroll (InfiniteScrollMsg >> liftMsg), Attr.class "ciphers-list no-scroll-bar" ]
285+ Keyed.ul [ Attr.class "p-list--divided", InfiniteScroll.infiniteScroll InfiniteScrollMsg, Attr.class "ciphers-list no-scroll-bar" ]
219 (ciphers
286 (ciphers
220 |> List.map
287 |> List.map
221 (\{ name, date, id } ->
288 (\{ name, date, id } ->
222 ( id
289 ( 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) ]
224 [ div [ Attr.class "cipher-row-container" ]
291 [ div [ Attr.class "cipher-row-container" ]
225 [ p [] [ text name ]
292 [ 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 ]
227 ]
294 ]
228 ]
295 ]
229 )
296 )
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)
2
2
3import Bridge
3import Bridge
4import GlobalEvents
4import GlobalEvents
5import Html exposing (..)
5import Html exposing (..)
6import Html.Attributes as Attr
6import Html.Attributes as Attr
7import Html.Events as Ev
7import Html.Events as Ev
8+import Logic.Cipher exposing (normalizeCipher)
8import Modal exposing (modal)
9import Modal exposing (modal)
9import Page exposing (..)
10import Page exposing (..)
11+import Pages.MasterPassword exposing (Msg)
10import Pages.PasswordGenerator as PasswordGenerator
12import Pages.PasswordGenerator as PasswordGenerator
11import Utils exposing (..)
13import Utils exposing (..)
12
14
13
15
14−type alias Model =
16+type alias Model msg =
15 { fullCipher : Bridge.FullCipher
17 { fullCipher : Bridge.FullCipher
16 , passwordGenerator : Maybe PasswordGenerator.Model
18 , passwordGenerator : Maybe PasswordGenerator.Model
19+ , callbacks : Callbacks msg
17 }
20 }
18
21
19
22
@@ -23,95 +26,109 @@ type Msg
23 | OpenGeneratePasword
26 | OpenGeneratePasword
24 | CloseGeneratePassword
27 | CloseGeneratePassword
25 | GeneratePassword Bridge.PasswordGeneratorConfig
28 | GeneratePassword Bridge.PasswordGeneratorConfig
29+ | Delete
30+ | Save
26
31
27
32
28type alias Callbacks msg =
33type alias Callbacks msg =
29 { save : Bridge.FullCipher -> msg
34 { save : Bridge.FullCipher -> msg
30 , generatePassword : Bridge.PasswordGeneratorConfig -> msg
35 , generatePassword : Bridge.PasswordGeneratorConfig -> msg
36+ , delete : Maybe (Bridge.FullCipher -> msg)
31 }
37 }
32
38
33
39
34−page : Callbacks emsg -> Page Bridge.FullCipher Model Msg emsg
40+title : Model msg -> List (Html Msg)
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 } ->
41+title { fullCipher } =
42− [ text fullCipher.name
42+ [ text fullCipher.name
43− , span [ Attr.class "u-float-right" ] [ iconButton "task-outstanding" (callbacks.save fullCipher) ]
43+ , span [ Attr.class "u-float-right" ]
44+ [ iconButton "task-outstanding" Save
45+ ]
44− ]
46+ ]
45− , event =
47+
48+
49+event : Model msg -> GlobalEvents.Event -> Model msg
46− \model ev ->
50+event model ev =
47− case ev of
51+ case ev of
48− GlobalEvents.UpdateCipher c ->
52+ GlobalEvents.UpdateCipher c ->
49− { model
53+ { model
50− | fullCipher =
54+ | fullCipher =
51− if c.id == model.fullCipher.id then
55+ if c.id == model.fullCipher.id then
52− c
56+ c
53−
57+
54− else
58+ else
55− model.fullCipher
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
56 }
79 }
80+ }
57
81
58− GlobalEvents.GeneratedPassword password ->
82+ _ ->
59− let
60− fullCipher =
61− model.fullCipher
62− in
63− { model
83+ 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− }
78
84
79
85
80−init : Bridge.FullCipher -> ( Model, Cmd Msg )
86+init : { fullCipher : Bridge.FullCipher, callbacks : Callbacks emsg } -> ( Model emsg, Cmd emsg )
81−init cipher =
87+init { fullCipher, callbacks } =
82− ( { fullCipher = cipher
88+ ( { fullCipher = fullCipher
83 , passwordGenerator = Nothing
89 , passwordGenerator = Nothing
90+ , callbacks = callbacks
84 }
91 }
85 , Cmd.none
92 , Cmd.none
86 )
93 )
87
94
88
95
89−update : Callbacks emsg -> (Msg -> emsg) -> Msg -> Model -> ( Result String Model, Cmd emsg )
96+update : Msg -> Model emsg -> ( Model emsg, Cmd emsg )
90−update { generatePassword } _ msg model =
97+update msg model =
91 case msg of
98 case msg of
92 EditCipher fullCipher ->
99 EditCipher fullCipher ->
93− ( Ok { model | fullCipher = fullCipher }, Cmd.none )
100+ ( { model | fullCipher = fullCipher }, Cmd.none )
94
101
95 PasswordGeneratorMsg m ->
102 PasswordGeneratorMsg m ->
96− ( Ok { model | passwordGenerator = Maybe.map (PasswordGenerator.update m) model.passwordGenerator }
103+ ( { model | passwordGenerator = Maybe.map (PasswordGenerator.update m) model.passwordGenerator }
97 , Cmd.none
104 , Cmd.none
98 )
105 )
99
106
100 OpenGeneratePasword ->
107 OpenGeneratePasword ->
101− ( Ok { model | passwordGenerator = Just PasswordGenerator.init }
108+ ( { model | passwordGenerator = Just PasswordGenerator.init }
102 , Cmd.none
109 , Cmd.none
103 )
110 )
104
111
105 CloseGeneratePassword ->
112 CloseGeneratePassword ->
106− ( Ok { model | passwordGenerator = Nothing }, Cmd.none )
113+ ( { model | passwordGenerator = Nothing }, Cmd.none )
107
114
108 GeneratePassword cfg ->
115 GeneratePassword cfg ->
109− ( Ok model, pureCmd (generatePassword cfg) )
116+ ( model, pureCmd (model.callbacks.generatePassword cfg) )
110
117
118+ Delete ->
119+ ( model
120+ , Maybe.withDefault Cmd.none
121+ (Maybe.map (\f -> f model.fullCipher |> pureCmd) model.callbacks.delete)
122+ )
111
123
112−subscriptions : Model -> Sub Msg
124+ Save ->
113−subscriptions _ =
125+ let
114− Sub.none
126+ { fullCipher } =
127+ model
128+ in
129+ ( model
130+ , model.callbacks.save { fullCipher | cipher = normalizeCipher fullCipher.cipher } |> pureCmd
131+ )
115
132
116
133
117heading : String -> String -> Html msg
134heading : String -> String -> Html msg
@@ -128,8 +145,8 @@ row { name, nameIcon, attrs } =
128 ]
145 ]
129
146
130
147
131−view : Model -> List (Html Msg)
148+view : Model emsg -> List (Html Msg)
132−view { fullCipher, passwordGenerator } =
149+view { fullCipher, passwordGenerator, callbacks } =
133 maybeList passwordGenerator
150 maybeList passwordGenerator
134 (\x ->
151 (\x ->
135 modal
152 modal
@@ -137,7 +154,7 @@ view { fullCipher, passwordGenerator } =
137 , close = CloseGeneratePassword
154 , close = CloseGeneratePassword
138 , footer =
155 , footer =
139 [ button
156 [ button
140− [ Attr.class "p-button--positive"
157+ [ Attr.class "p-button--positive u-no-margin"
141 , Ev.onClick <| GeneratePassword x.passwordConfig
158 , Ev.onClick <| GeneratePassword x.passwordConfig
142 ]
159 ]
143 [ text "Generate password" ]
160 [ text "Generate password" ]
@@ -160,7 +177,7 @@ view { fullCipher, passwordGenerator } =
160 ++ (case fullCipher.cipher of
177 ++ (case fullCipher.cipher of
161 Bridge.LoginCipher cipher ->
178 Bridge.LoginCipher cipher ->
162 let
179 let
163− { username, password, uris } =
180+ { username, password, uris, totp } =
164 cipher
181 cipher
165
182
166 edit c =
183 edit c =
@@ -189,6 +206,16 @@ view { fullCipher, passwordGenerator } =
189 []
206 []
190 , iconButton "restart" OpenGeneratePasword
207 , iconButton "restart" OpenGeneratePasword
191 ]
208 ]
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+ }
192 ]
219 ]
193 ++ (uris
220 ++ (uris
194 |> List.indexedMap
221 |> List.indexedMap
@@ -220,7 +247,7 @@ view { fullCipher, passwordGenerator } =
220 , br [] []
247 , br [] []
221 , textarea
248 , textarea
222 [ Attr.spellcheck True
249 [ Attr.spellcheck True
223− , Attr.style "height" "70vh"
250+ , Attr.style "height" "65vh"
224 , Attr.value contents
251 , Attr.value contents
225 , Ev.onInput (\x -> EditCipher { fullCipher | cipher = Bridge.NoteCipher x })
252 , Ev.onInput (\x -> EditCipher { fullCipher | cipher = Bridge.NoteCipher x })
226 ]
253 ]
@@ -254,7 +281,8 @@ view { fullCipher, passwordGenerator } =
254 , Attr.type_ "text"
281 , Attr.type_ "text"
255 , Attr.value (Maybe.withDefault "" expMonth)
282 , Attr.value (Maybe.withDefault "" expMonth)
256 , Ev.onInput (\x -> edit { cipher | expMonth = Just x })
283 , Ev.onInput (\x -> edit { cipher | expMonth = Just x })
257− , Attr.style "width" "20%"
284+ , Attr.style "width" "30%"
285+ , Attr.style "min-width" "initial"
258 , Attr.class "u-align--right"
286 , Attr.class "u-align--right"
259 ]
287 ]
260 []
288 []
@@ -264,7 +292,8 @@ view { fullCipher, passwordGenerator } =
264 , Attr.type_ "text"
292 , Attr.type_ "text"
265 , Attr.value (Maybe.withDefault "" expYear)
293 , Attr.value (Maybe.withDefault "" expYear)
266 , Ev.onInput (\x -> edit { cipher | expYear = Just x })
294 , Ev.onInput (\x -> edit { cipher | expYear = Just x })
267− , Attr.style "width" "20%"
295+ , Attr.style "width" "30%"
296+ , Attr.style "min-width" "initial"
268 ]
297 ]
269 []
298 []
270 ]
299 ]
@@ -430,6 +459,14 @@ view { fullCipher, passwordGenerator } =
430 }
459 }
431 ]
460 ]
432 )
461 )
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+ )
433
470
434
471
435iconButton : String -> msg -> Html msg
472iconButton : 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)
2
2
3import Html exposing (..)
3import Html exposing (..)
4import Html.Attributes as Attr
4import Html.Attributes as Attr
@@ -7,10 +7,11 @@ import Page exposing (..)
7import Utils exposing (..)
7import Utils exposing (..)
8
8
9
9
10−type alias Model =
10+type alias Model emsg =
11 { server : String
11 { server : String
12 , email : String
12 , email : String
13 , password : String
13 , password : String
14+ , callbacks : Callbacks emsg
14 }
15 }
15
16
16
17
@@ -26,53 +27,49 @@ type alias Callbacks emsg =
26 }
27 }
27
28
28
29
29−page : Callbacks emsg -> Page () Model Msg emsg
30+title : String
30−page callbacks liftMsg =
31+title =
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" ]
32+ "Log into bitwarden"
36− , event = \model _ -> model
37− }
38
33
39
34
40−init : ( Model, Cmd Msg )
35+init : Callbacks emsg -> Model emsg
41−init =
36+init callbacks =
42− ( { server = "https://vault.bitwarden.com"
37+ { server = "https://vault.bitwarden.com"
43− , email = "ilyakooo0@gmail.com"
38+ , email = ""
44− , password = ""
39+ , password = ""
40+ , callbacks = callbacks
45− }
41+ }
46− , Cmd.none
47− )
48
42
49
43
50−update : Callbacks emsg -> (Msg -> emsg) -> Msg -> Model -> ( Result String Model, Cmd emsg )
44+update : Msg -> Model emsg -> ( Model emsg, Cmd emsg )
51−update { submit } liftMsg msg model =
45+update msg model =
52 case msg of
46 case msg of
53 UpdateEmail email ->
47 UpdateEmail email ->
54− ( Ok { model | email = email }, Cmd.none )
48+ ( { model | email = email }, Cmd.none )
55
49
56 UpdateServer server ->
50 UpdateServer server ->
57− ( Ok { model | server = server }, Cmd.none )
51+ ( { model | server = server }, Cmd.none )
58
52
59 UpdatePassword password ->
53 UpdatePassword password ->
60− ( Ok { model | password = password }, Cmd.none )
54+ ( { model | password = password }, Cmd.none )
61
55
62 Submit ->
56 Submit ->
57+ ( model
63− ( Ok model, pureCmd (submit model) )
58+ , pureCmd
64−
59+ (model.callbacks.submit
65−
60+ { email = model.email
66−subscriptions : Model -> Sub Msg
61+ , password = model.password
67−subscriptions model =
62+ , server = model.server
68− Sub.none
63+ }
69−
64+ )
70−
65+ )
66+
67+
71−view : Model -> List (Html Msg)
68+view : Model emsg -> List (Html Msg)
72view model =
69view model =
73 [ form [ Ev.onSubmit Submit ]
70 [ form [ Ev.onSubmit Submit ]
74− ([ label [] [ text "Server" ]
71+ [ label [] [ text "Server" ]
75− , input
72+ , input
76 [ Attr.type_ "url"
73 [ Attr.type_ "url"
77 , Attr.value model.server
74 , Attr.value model.server
78 , Ev.onInput UpdateServer
75 , Ev.onInput UpdateServer
@@ -80,8 +77,8 @@ view model =
80 , Attr.required True
77 , Attr.required True
81 ]
78 ]
82 []
79 []
83− , label [] [ text "Email" ]
80+ , label [] [ text "Email" ]
84− , input
81+ , input
85 [ Attr.type_ "email"
82 [ Attr.type_ "email"
86 , Attr.value model.email
83 , Attr.value model.email
87 , Ev.onInput UpdateEmail
84 , Ev.onInput UpdateEmail
@@ -89,8 +86,8 @@ view model =
89 , Attr.required True
86 , Attr.required True
90 ]
87 ]
91 []
88 []
92− , label [] [ text "Password" ]
89+ , label [] [ text "Password" ]
93− , input
90+ , input
94 [ Attr.type_ "password"
91 [ Attr.type_ "password"
95 , Attr.value model.password
92 , Attr.value model.password
96 , Ev.onInput UpdatePassword
93 , Ev.onInput UpdatePassword
@@ -98,7 +95,6 @@ view model =
98 , Attr.required True
95 , Attr.required True
99 ]
96 ]
100 []
97 []
101− ]
102− ++ [ alignRight [ button [ Attr.type_ "submit" ] [ text "Log in" ] ] ]
98+ , alignRight [ button [ Attr.type_ "submit" ] [ text "Log in" ] ]
103− )
99+ ]
104 ]
100 ]
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)
2
2
3import Html exposing (..)
3import Html exposing (..)
4import Html.Attributes as Attr
4import Html.Attributes as Attr
@@ -7,10 +7,11 @@ import Page exposing (..)
7import Utils exposing (..)
7import Utils exposing (..)
8
8
9
9
10−type alias Model =
10+type alias Model emsg =
11 { server : String
11 { server : String
12 , email : String
12 , email : String
13 , password : String
13 , password : String
14+ , callbacks : Callbacks emsg
14 }
15 }
15
16
16
17
@@ -26,46 +27,34 @@ type alias Callbacks emsg =
26 }
27 }
27
28
28
29
29−page : Callbacks emsg -> Page { server : String, login : String } Model Msg emsg
30+title : String
30−page callbacks liftMsg =
31+title =
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" ]
32+ "Enter master password"
36− , event = \model _ -> model
37− }
38
33
39
34
40−init : { server : String, login : String } -> ( Model, Cmd Msg )
35+init : Callbacks emsg -> { server : String, login : String } -> Model emsg
41−init { server, login } =
36+init callbacks { server, login } =
42− ( { server = server
37+ { server = server
43− , email = login
38+ , email = login
44− , password = ""
39+ , password = ""
45− }
40+ , callbacks = callbacks
46− , Cmd.none
41+ }
47− )
48
42
49
43
50−update : Callbacks emsg -> (Msg -> emsg) -> Msg -> Model -> ( Result String Model, Cmd emsg )
44+update : Msg -> Model emsg -> ( Model emsg, Cmd emsg )
51−update { submit, reset } _ msg model =
45+update msg model =
52 case msg of
46 case msg of
53 UpdatePassword password ->
47 UpdatePassword password ->
54− ( Ok { model | password = password }, Cmd.none )
48+ ( { model | password = password }, Cmd.none )
55
49
56 Submit ->
50 Submit ->
57− ( Ok model, pureCmd (submit { password = model.password }) )
51+ ( model, pureCmd (model.callbacks.submit { password = model.password }) )
58
52
59 Reset ->
53 Reset ->
60− ( Ok model, pureCmd reset )
54+ ( model, pureCmd model.callbacks.reset )
61−
62−
63−subscriptions : Model -> Sub Msg
64−subscriptions _ =
65− Sub.none
66
55
67
56
68−view : Model -> List (Html Msg)
57+view : Model emsg -> List (Html Msg)
69view model =
58view model =
70 [ alignRight [ button [ Attr.class "p-button--negative", Ev.onClick Reset ] [ text "Log out" ] ]
59 [ alignRight [ button [ Attr.class "p-button--negative", Ev.onClick Reset ] [ text "Log out" ] ]
71 , table [ Attr.class "p-table--mobile-card u-no-margin" ]
60 , 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 (..)
2
2
3import Html exposing (..)
3import Html exposing (..)
4import Html.Attributes as Attr
4import Html.Attributes as Attr
5+import Html.Events as Ev
5import Task
6import Task
6
7
7
8
@@ -144,3 +145,9 @@ spanList f aa =
144
145
145 else
146 else
146 ( [], aa )
147 ( [], 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 @@
5 <meta charset="UTF-8">
5 <meta charset="UTF-8">
6 <meta http-equiv="X-UA-Compatible" content="IE=edge">
6 <meta http-equiv="X-UA-Compatible" content="IE=edge">
7 <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
7 <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">
9 <link rel="stylesheet" href="style.css">
9 <link rel="stylesheet" href="style.css">
10− <title>Test.</title>
10+ <title>Warden</title>
11 <script src="elm.js"></script>
11 <script src="elm.js"></script>
12</head>
12</head>
13
13
manifest.json+6−6
@@ -1,15 +1,15 @@
1{
1{
2 "name": "soy.iko.warden",
2 "name": "soy.iko.warden",
3− "description": "A simple bitwarden client",
3+ "description": "A simple Bitwarden client",
4 "architecture": "@CLICK_ARCH@",
4 "architecture": "@CLICK_ARCH@",
5 "title": "Warden",
5 "title": "Warden",
6 "hooks": {
6 "hooks": {
7− "appname": {
7+ "warden": {
8− "apparmor": "appname.apparmor",
8+ "apparmor": "warden.apparmor",
9− "desktop": "appname.desktop"
9+ "desktop": "warden.desktop"
10 }
10 }
11 },
11 },
12− "version": "1.0.0",
12+ "version": "1.2.4",
13− "maintainer": "Ilia Kostiuchenko <email@iko.soy>",
13+ "maintainer": "Ilia Kostiuchenko <mail@iko.soy>",
14 "framework": "@CLICK_FRAMEWORK@"
14 "framework": "@CLICK_FRAMEWORK@"
15}
15}
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
99-------------------------------
99-------------------------------
100-}
100-}
101let upstream =
101let upstream =
102− https://github.com/purescript/package-sets/releases/download/psc-0.15.2-20220706/packages.dhall
102+ https://github.com/purescript/package-sets/releases/download/psc-0.15.4-20220808/packages.dhall
103− sha256:7a24ebdbacb2bfa27b2fc6ce3da96f048093d64e54369965a2a7b5d9892b6031
103+ sha256:60eee64b04ca0013fae3e02a69fc3b176105c6baa2f31865c67cd5f881a412fd
104
104
105in upstream
105in upstream
106 with argonaut-aeson-generic =
106 with argonaut-aeson-generic =
@@ -125,7 +125,7 @@ in upstream
125 ]
125 ]
126 , repo =
126 , repo =
127 "https://github.com/ilyakooo0/purescript-argonaut-aeson-generic.git"
127 "https://github.com/ilyakooo0/purescript-argonaut-aeson-generic.git"
128− , version = "purescript-0.14"
128+ , version = "3e388500811ef4b6718fc776789f3be050ad7b57"
129 }
129 }
130 with untagged-union =
130 with untagged-union =
131 { dependencies =
131 { dependencies =
@@ -142,12 +142,12 @@ in upstream
142 , "tuples"
142 , "tuples"
143 , "unsafe-coerce"
143 , "unsafe-coerce"
144 ]
144 ]
145− , repo = "https://github.com/ilyakooo0/purescript-untagged-union.git"
145+ , repo = "https://github.com/rowtype-yoga/purescript-untagged-union.git"
146− , version = "iko/ps-15"
146+ , version = "6c5bccef38696a16e86ba9a65cf778b4462c4a66"
147 }
147 }
148 with literals =
148 with literals =
149 { repo = "https://github.com/ilyakooo0/purescript-literals.git"
149 { repo = "https://github.com/ilyakooo0/purescript-literals.git"
150− , version = "iko/_null"
150+ , version = "6875fb28026595cfb780318305a77e79b098bb01"
151 , dependencies =
151 , dependencies =
152 [ "assert"
152 [ "assert"
153 , "effect"
153 , "effect"
registry.datnew file

Binary file not shown.

scratchpad/password_generator.html+16−1
@@ -5,7 +5,7 @@
5 <meta charset="UTF-8">
5 <meta charset="UTF-8">
6 <meta http-equiv="X-UA-Compatible" content="IE=edge">
6 <meta http-equiv="X-UA-Compatible" content="IE=edge">
7 <meta name="viewport" content="width=device-width, initial-scale=1.0">
7 <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">
9 <link rel="stylesheet" href="../style.css">
9 <link rel="stylesheet" href="../style.css">
10 <title>Password generator</title>
10 <title>Password generator</title>
11
11
@@ -20,6 +20,14 @@
20
20
21<body>
21<body>
22
22
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+
23 <div class="password-generator-group">
31 <div class="password-generator-group">
24 <p>Password length</p>
32 <p>Password length</p>
25 <input type="number" class="p-slider__input">
33 <input type="number" class="p-slider__input">
@@ -35,6 +43,13 @@
35 <input type="number" class="p-slider__input">
43 <input type="number" class="p-slider__input">
36 </div>
44 </div>
37
45
46+ <form action="">
47+ <label><input type="checkbox" name="" id="">Hahahah</label>
48+ <input type="checkbox">
49+
50+ </form>
51+
52+
38
53
39 <input type="range" min="0" max="100" value="50" step="1">
54 <input type="range" min="0" max="100" value="50" step="1">
40 <input type="range" min="0" max="100" value="50" step="1"
55 <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.
28 , "either"
28 , "either"
29 , "exceptions"
29 , "exceptions"
30 , "foldable-traversable"
30 , "foldable-traversable"
31+ , "foreign"
31 , "functions"
32 , "functions"
33+ , "integers"
34+ , "literals"
32 , "maybe"
35 , "maybe"
33 , "newtype"
36 , "newtype"
34 , "nullable"
37 , "nullable"
38+ , "numbers"
35 , "orders"
39 , "orders"
36 , "prelude"
40 , "prelude"
41+ , "random"
37 , "refs"
42 , "refs"
38 , "run"
43 , "run"
39 , "strings"
44 , "strings"
style.css+42
@@ -107,3 +107,45 @@ textarea {
107 min-width: 5rem;
107 min-width: 5rem;
108 text-align: center;
108 text-align: center;
109}
109}
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 @@
5 "audio",
5 "audio",
6 "networking"
6 "networking"
7 ],
7 ],
8− "policy_version": 16.04
8+ "policy_version": 20.04
9}
9}
appname.desktop → warden.desktoprenamed

The content is the same.