Compare commits
	
		
			1 Commits
		
	
	
		
			v0.2.3
			...
			41f8e8f2ab
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					41f8e8f2ab | 
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -5,4 +5,3 @@ src-tauri/target/
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# just in case
 | 
					# just in case
 | 
				
			||||||
credentials*
 | 
					credentials*
 | 
				
			||||||
!credentials.rs
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,9 +0,0 @@
 | 
				
			|||||||
My original plan was to use [libsodium](https://doc.libsodium.org/) to handle encryption. However, the Rust bindings for libsodium are no longer actively maintained, which left me uncomfortable with using it. Instead, I switched to the [RustCrypto](https://github.com/RustCrypto) implementations of the same (or nearly the same) cryptographic primitives provided by libsodium.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Creddy makes use of two cryptographic primitives: A key-derivation function, which is currently `argon2id`, and a symmetric encryption algorithm, currently `XChaCha20Poly1305`. 
 | 
					 | 
				
			||||||
* I chose `argon2id` because it's what libsodium uses, and because its difficulty parameters admit of very granular tuning.
 | 
					 | 
				
			||||||
* I chose `XChaCha20Poly1305` because it's _almost_ what libsodium uses - libsodium uses `XSalsa20Poly1305`, and it's my undersatnding that `XChaCha20Poly1305` is an evolution of the former. In both cases I use the eXtended variants, which make use of longer (24-byte) nonces than the non-X variants. This appealed to me because I wanted to be able to randomly generate a nonce every time I needed one, and I have seen [recommendations](https://latacora.micro.blog/2018/04/03/cryptographic-right-answers.html) that the 12-byte nonces used by the non-X variants are _juuust_ a touch small for that to be truly worry-free. The RustCrypto implementation of `XChaCha20Poly1305` has also been subject to a security audit, which is nice.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
I tuned the `argon2id` parameters so that key-derivation would take ~800ms on my Ryzen 1600X. This is probably overkill, but I don't intend for key-derivation to be a frequent occurrence - no more than once a day, under normal circumstances. Taking in the neighborhood of 1 second seemed about the longest I could reasonably go.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**DISCLAIMER**: I am not a professional cryptographer, merely an interested amateur. While I've tried to be as careful as possible with selecting and making use of the cryptographic building blocks I've chosen here, there is always the possibility that I've screwed something up. If anyone would like to sponsor an _actual_ security review of Creddy by people who _actually_ know what they're doing instead of just what they've read on the internet, please let me know.
 | 
					 | 
				
			||||||
							
								
								
									
										540
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										540
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@@ -1,12 +1,12 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "name": "creddy",
 | 
					  "name": "creddy",
 | 
				
			||||||
  "version": "0.2.2",
 | 
					  "version": "0.1.0",
 | 
				
			||||||
  "lockfileVersion": 2,
 | 
					  "lockfileVersion": 2,
 | 
				
			||||||
  "requires": true,
 | 
					  "requires": true,
 | 
				
			||||||
  "packages": {
 | 
					  "packages": {
 | 
				
			||||||
    "": {
 | 
					    "": {
 | 
				
			||||||
      "name": "creddy",
 | 
					      "name": "creddy",
 | 
				
			||||||
      "version": "0.2.2",
 | 
					      "version": "0.1.0",
 | 
				
			||||||
      "dependencies": {
 | 
					      "dependencies": {
 | 
				
			||||||
        "@tauri-apps/api": "^1.0.2",
 | 
					        "@tauri-apps/api": "^1.0.2",
 | 
				
			||||||
        "daisyui": "^2.51.5"
 | 
					        "daisyui": "^2.51.5"
 | 
				
			||||||
@@ -21,17 +21,6 @@
 | 
				
			|||||||
        "vite": "^3.0.7"
 | 
					        "vite": "^3.0.7"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/@alloc/quick-lru": {
 | 
					 | 
				
			||||||
      "version": "5.2.0",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
 | 
					 | 
				
			||||||
      "engines": {
 | 
					 | 
				
			||||||
        "node": ">=10"
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      "funding": {
 | 
					 | 
				
			||||||
        "url": "https://github.com/sponsors/sindresorhus"
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "node_modules/@esbuild/android-arm": {
 | 
					    "node_modules/@esbuild/android-arm": {
 | 
				
			||||||
      "version": "0.15.18",
 | 
					      "version": "0.15.18",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.18.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.18.tgz",
 | 
				
			||||||
@@ -166,9 +155,9 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/@tauri-apps/api": {
 | 
					    "node_modules/@tauri-apps/api": {
 | 
				
			||||||
      "version": "1.4.0",
 | 
					      "version": "1.2.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-1.4.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-1.2.0.tgz",
 | 
				
			||||||
      "integrity": "sha512-Jd6HPoTM1PZSFIzq7FB8VmMu3qSSyo/3lSwLpoapW+lQ41CL5Dow2KryLg+gyazA/58DRWI9vu/XpEeHK4uMdw==",
 | 
					      "integrity": "sha512-lsI54KI6HGf7VImuf/T9pnoejfgkNoXveP14pVV7XarrQ46rOejIVJLFqHI9sRReJMGdh2YuCoI3cc/yCWCsrw==",
 | 
				
			||||||
      "engines": {
 | 
					      "engines": {
 | 
				
			||||||
        "node": ">= 14.6.0",
 | 
					        "node": ">= 14.6.0",
 | 
				
			||||||
        "npm": ">= 6.6.0",
 | 
					        "npm": ">= 6.6.0",
 | 
				
			||||||
@@ -180,9 +169,9 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/@tauri-apps/cli": {
 | 
					    "node_modules/@tauri-apps/cli": {
 | 
				
			||||||
      "version": "1.4.0",
 | 
					      "version": "1.2.3",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-1.4.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-1.2.3.tgz",
 | 
				
			||||||
      "integrity": "sha512-VXYr2i2iVFl98etQSQsqLzXgX96bnWiNZd1YADgatqwy/qecbd6Kl5ZAPB5R4ynsgE8A1gU7Fbzh7dCEQYFfmA==",
 | 
					      "integrity": "sha512-erxtXuPhMEGJPBtnhPILD4AjuT81GZsraqpFvXAmEJZ2p8P6t7MVBifCL8LznRknznM3jn90D3M8RNBP3wcXTw==",
 | 
				
			||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "bin": {
 | 
					      "bin": {
 | 
				
			||||||
        "tauri": "tauri.js"
 | 
					        "tauri": "tauri.js"
 | 
				
			||||||
@@ -195,22 +184,21 @@
 | 
				
			|||||||
        "url": "https://opencollective.com/tauri"
 | 
					        "url": "https://opencollective.com/tauri"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      "optionalDependencies": {
 | 
					      "optionalDependencies": {
 | 
				
			||||||
        "@tauri-apps/cli-darwin-arm64": "1.4.0",
 | 
					        "@tauri-apps/cli-darwin-arm64": "1.2.3",
 | 
				
			||||||
        "@tauri-apps/cli-darwin-x64": "1.4.0",
 | 
					        "@tauri-apps/cli-darwin-x64": "1.2.3",
 | 
				
			||||||
        "@tauri-apps/cli-linux-arm-gnueabihf": "1.4.0",
 | 
					        "@tauri-apps/cli-linux-arm-gnueabihf": "1.2.3",
 | 
				
			||||||
        "@tauri-apps/cli-linux-arm64-gnu": "1.4.0",
 | 
					        "@tauri-apps/cli-linux-arm64-gnu": "1.2.3",
 | 
				
			||||||
        "@tauri-apps/cli-linux-arm64-musl": "1.4.0",
 | 
					        "@tauri-apps/cli-linux-arm64-musl": "1.2.3",
 | 
				
			||||||
        "@tauri-apps/cli-linux-x64-gnu": "1.4.0",
 | 
					        "@tauri-apps/cli-linux-x64-gnu": "1.2.3",
 | 
				
			||||||
        "@tauri-apps/cli-linux-x64-musl": "1.4.0",
 | 
					        "@tauri-apps/cli-linux-x64-musl": "1.2.3",
 | 
				
			||||||
        "@tauri-apps/cli-win32-arm64-msvc": "1.4.0",
 | 
					        "@tauri-apps/cli-win32-ia32-msvc": "1.2.3",
 | 
				
			||||||
        "@tauri-apps/cli-win32-ia32-msvc": "1.4.0",
 | 
					        "@tauri-apps/cli-win32-x64-msvc": "1.2.3"
 | 
				
			||||||
        "@tauri-apps/cli-win32-x64-msvc": "1.4.0"
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/@tauri-apps/cli-darwin-arm64": {
 | 
					    "node_modules/@tauri-apps/cli-darwin-arm64": {
 | 
				
			||||||
      "version": "1.4.0",
 | 
					      "version": "1.2.3",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-1.4.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-1.2.3.tgz",
 | 
				
			||||||
      "integrity": "sha512-nA/ml0SfUt6/CYLVbHmT500Y+ijqsuv5+s9EBnVXYSLVg9kbPUZJJHluEYK+xKuOj6xzyuT/+rZFMRapmJD3jQ==",
 | 
					      "integrity": "sha512-phJN3fN8FtZZwqXg08bcxfq1+X1JSDglLvRxOxB7VWPq+O5SuB8uLyssjJsu+PIhyZZnIhTGdjhzLSFhSXfLsw==",
 | 
				
			||||||
      "cpu": [
 | 
					      "cpu": [
 | 
				
			||||||
        "arm64"
 | 
					        "arm64"
 | 
				
			||||||
      ],
 | 
					      ],
 | 
				
			||||||
@@ -224,9 +212,9 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/@tauri-apps/cli-darwin-x64": {
 | 
					    "node_modules/@tauri-apps/cli-darwin-x64": {
 | 
				
			||||||
      "version": "1.4.0",
 | 
					      "version": "1.2.3",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-1.4.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-1.2.3.tgz",
 | 
				
			||||||
      "integrity": "sha512-ov/F6Zr+dg9B0PtRu65stFo2G0ow2TUlneqYYrkj+vA3n+moWDHfVty0raDjMLQbQt3rv3uayFMXGPMgble9OA==",
 | 
					      "integrity": "sha512-jFZ/y6z8z6v4yliIbXKBXA7BJgtZVMsITmEXSuD6s5+eCOpDhQxbRkr6CA+FFfr+/r96rWSDSgDenDQuSvPAKw==",
 | 
				
			||||||
      "cpu": [
 | 
					      "cpu": [
 | 
				
			||||||
        "x64"
 | 
					        "x64"
 | 
				
			||||||
      ],
 | 
					      ],
 | 
				
			||||||
@@ -240,9 +228,9 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
 | 
					    "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
 | 
				
			||||||
      "version": "1.4.0",
 | 
					      "version": "1.2.3",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-1.4.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-1.2.3.tgz",
 | 
				
			||||||
      "integrity": "sha512-zwjbiMncycXDV7doovymyKD7sCg53ouAmfgpUqEBOTY3vgBi9TwijyPhJOqoG5vUVWhouNBC08akGmE4dja15g==",
 | 
					      "integrity": "sha512-C7h5vqAwXzY0kRGSU00Fj8PudiDWFCiQqqUNI1N+fhCILrzWZB9TPBwdx33ZfXKt/U4+emdIoo/N34v3TiAOmQ==",
 | 
				
			||||||
      "cpu": [
 | 
					      "cpu": [
 | 
				
			||||||
        "arm"
 | 
					        "arm"
 | 
				
			||||||
      ],
 | 
					      ],
 | 
				
			||||||
@@ -256,9 +244,9 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/@tauri-apps/cli-linux-arm64-gnu": {
 | 
					    "node_modules/@tauri-apps/cli-linux-arm64-gnu": {
 | 
				
			||||||
      "version": "1.4.0",
 | 
					      "version": "1.2.3",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-1.4.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-1.2.3.tgz",
 | 
				
			||||||
      "integrity": "sha512-5MCBcziqXC72mMXnkZU68mutXIR6zavDxopArE2gQtK841IlE06bIgtLi0kUUhlFJk2nhPRgiDgdLbrPlyt7fw==",
 | 
					      "integrity": "sha512-buf1c8sdkuUzVDkGPQpyUdAIIdn5r0UgXU6+H5fGPq/Xzt5K69JzXaeo6fHsZEZghbV0hOK+taKV4J0m30UUMQ==",
 | 
				
			||||||
      "cpu": [
 | 
					      "cpu": [
 | 
				
			||||||
        "arm64"
 | 
					        "arm64"
 | 
				
			||||||
      ],
 | 
					      ],
 | 
				
			||||||
@@ -272,9 +260,9 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/@tauri-apps/cli-linux-arm64-musl": {
 | 
					    "node_modules/@tauri-apps/cli-linux-arm64-musl": {
 | 
				
			||||||
      "version": "1.4.0",
 | 
					      "version": "1.2.3",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.4.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.2.3.tgz",
 | 
				
			||||||
      "integrity": "sha512-7J3pRB6n6uNYgIfCeKt2Oz8J7oSaz2s8GGFRRH2HPxuTHrBNCinzVYm68UhVpJrL3bnGkU0ziVZLsW/iaOGfUg==",
 | 
					      "integrity": "sha512-x88wPS9W5xAyk392vc4uNHcKBBvCp0wf4H9JFMF9OBwB7vfd59LbQCFcPSu8f0BI7bPrOsyHqspWHuFL8ojQEA==",
 | 
				
			||||||
      "cpu": [
 | 
					      "cpu": [
 | 
				
			||||||
        "arm64"
 | 
					        "arm64"
 | 
				
			||||||
      ],
 | 
					      ],
 | 
				
			||||||
@@ -288,9 +276,9 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/@tauri-apps/cli-linux-x64-gnu": {
 | 
					    "node_modules/@tauri-apps/cli-linux-x64-gnu": {
 | 
				
			||||||
      "version": "1.4.0",
 | 
					      "version": "1.2.3",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-1.4.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-1.2.3.tgz",
 | 
				
			||||||
      "integrity": "sha512-Zh5gfAJxOv5AVWxcwuueaQ2vIAhlg0d6nZui6nMyfIJ8dbf3aZQ5ZzP38sYow5h/fbvgL+3GSQxZRBIa3c2E1w==",
 | 
					      "integrity": "sha512-ZMz1jxEVe0B4/7NJnlPHmwmSIuwiD6ViXKs8F+OWWz2Y4jn5TGxWKFg7DLx5OwQTRvEIZxxT7lXHi5CuTNAxKg==",
 | 
				
			||||||
      "cpu": [
 | 
					      "cpu": [
 | 
				
			||||||
        "x64"
 | 
					        "x64"
 | 
				
			||||||
      ],
 | 
					      ],
 | 
				
			||||||
@@ -304,9 +292,9 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/@tauri-apps/cli-linux-x64-musl": {
 | 
					    "node_modules/@tauri-apps/cli-linux-x64-musl": {
 | 
				
			||||||
      "version": "1.4.0",
 | 
					      "version": "1.2.3",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-1.4.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-1.2.3.tgz",
 | 
				
			||||||
      "integrity": "sha512-OLAYoICU3FaYiTdBsI+lQTKnDHeMmFMXIApN0M+xGiOkoIOQcV9CConMPjgmJQ867+NHRNgUGlvBEAh9CiJodQ==",
 | 
					      "integrity": "sha512-B/az59EjJhdbZDzawEVox0LQu2ZHCZlk8rJf85AMIktIUoAZPFbwyiUv7/zjzA/sY6Nb58OSJgaPL2/IBy7E0A==",
 | 
				
			||||||
      "cpu": [
 | 
					      "cpu": [
 | 
				
			||||||
        "x64"
 | 
					        "x64"
 | 
				
			||||||
      ],
 | 
					      ],
 | 
				
			||||||
@@ -319,26 +307,10 @@
 | 
				
			|||||||
        "node": ">= 10"
 | 
					        "node": ">= 10"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/@tauri-apps/cli-win32-arm64-msvc": {
 | 
					 | 
				
			||||||
      "version": "1.4.0",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-1.4.0.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-gZ05GENFbI6CB5MlOUsLlU0kZ9UtHn9riYtSXKT6MYs8HSPRffPHaHSL0WxsJweWh9nR5Hgh/TUU8uW3sYCzCg==",
 | 
					 | 
				
			||||||
      "cpu": [
 | 
					 | 
				
			||||||
        "arm64"
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
      "dev": true,
 | 
					 | 
				
			||||||
      "optional": true,
 | 
					 | 
				
			||||||
      "os": [
 | 
					 | 
				
			||||||
        "win32"
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
      "engines": {
 | 
					 | 
				
			||||||
        "node": ">= 10"
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "node_modules/@tauri-apps/cli-win32-ia32-msvc": {
 | 
					    "node_modules/@tauri-apps/cli-win32-ia32-msvc": {
 | 
				
			||||||
      "version": "1.4.0",
 | 
					      "version": "1.2.3",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-1.4.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-1.2.3.tgz",
 | 
				
			||||||
      "integrity": "sha512-JsetT/lTx/Zq98eo8T5CiRyF1nKeX04RO8JlJrI3ZOYsZpp/A5RJvMd/szQ17iOzwiHdge+tx7k2jHysR6oBlQ==",
 | 
					      "integrity": "sha512-ypdO1OdC5ugNJAKO2m3sb1nsd+0TSvMS9Tr5qN/ZSMvtSduaNwrcZ3D7G/iOIanrqu/Nl8t3LYlgPZGBKlw7Ng==",
 | 
				
			||||||
      "cpu": [
 | 
					      "cpu": [
 | 
				
			||||||
        "ia32"
 | 
					        "ia32"
 | 
				
			||||||
      ],
 | 
					      ],
 | 
				
			||||||
@@ -352,9 +324,9 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/@tauri-apps/cli-win32-x64-msvc": {
 | 
					    "node_modules/@tauri-apps/cli-win32-x64-msvc": {
 | 
				
			||||||
      "version": "1.4.0",
 | 
					      "version": "1.2.3",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-1.4.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-1.2.3.tgz",
 | 
				
			||||||
      "integrity": "sha512-z8Olcnwp5aYhzqUAarFjqF+oELCjuYWnB2HAJHlfsYNfDCAORY5kct3Fklz8PSsubC3U2EugWn8n42DwnThurg==",
 | 
					      "integrity": "sha512-CsbHQ+XhnV/2csOBBDVfH16cdK00gNyNYUW68isedmqcn8j+s0e9cQ1xXIqi+Hue3awp8g3ImYN5KPepf3UExw==",
 | 
				
			||||||
      "cpu": [
 | 
					      "cpu": [
 | 
				
			||||||
        "x64"
 | 
					        "x64"
 | 
				
			||||||
      ],
 | 
					      ],
 | 
				
			||||||
@@ -455,9 +427,9 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/browserslist": {
 | 
					    "node_modules/browserslist": {
 | 
				
			||||||
      "version": "4.21.9",
 | 
					      "version": "4.21.5",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz",
 | 
				
			||||||
      "integrity": "sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==",
 | 
					      "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==",
 | 
				
			||||||
      "funding": [
 | 
					      "funding": [
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          "type": "opencollective",
 | 
					          "type": "opencollective",
 | 
				
			||||||
@@ -466,17 +438,13 @@
 | 
				
			|||||||
        {
 | 
					        {
 | 
				
			||||||
          "type": "tidelift",
 | 
					          "type": "tidelift",
 | 
				
			||||||
          "url": "https://tidelift.com/funding/github/npm/browserslist"
 | 
					          "url": "https://tidelift.com/funding/github/npm/browserslist"
 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
          "type": "github",
 | 
					 | 
				
			||||||
          "url": "https://github.com/sponsors/ai"
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      ],
 | 
					      ],
 | 
				
			||||||
      "dependencies": {
 | 
					      "dependencies": {
 | 
				
			||||||
        "caniuse-lite": "^1.0.30001503",
 | 
					        "caniuse-lite": "^1.0.30001449",
 | 
				
			||||||
        "electron-to-chromium": "^1.4.431",
 | 
					        "electron-to-chromium": "^1.4.284",
 | 
				
			||||||
        "node-releases": "^2.0.12",
 | 
					        "node-releases": "^2.0.8",
 | 
				
			||||||
        "update-browserslist-db": "^1.0.11"
 | 
					        "update-browserslist-db": "^1.0.10"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      "bin": {
 | 
					      "bin": {
 | 
				
			||||||
        "browserslist": "cli.js"
 | 
					        "browserslist": "cli.js"
 | 
				
			||||||
@@ -494,9 +462,9 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/caniuse-lite": {
 | 
					    "node_modules/caniuse-lite": {
 | 
				
			||||||
      "version": "1.0.30001515",
 | 
					      "version": "1.0.30001481",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001515.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001481.tgz",
 | 
				
			||||||
      "integrity": "sha512-eEFDwUOZbE24sb+Ecsx3+OvNETqjWIdabMy52oOkIgcUtAsQifjUG9q4U9dgTHJM2mfk4uEPxc0+xuFdJ629QA==",
 | 
					      "integrity": "sha512-KCqHwRnaa1InZBtqXzP98LPg0ajCVujMKjqKDhZEthIpAsJl/YEIa3YvXjGXPVqzZVguccuu7ga9KOE1J9rKPQ==",
 | 
				
			||||||
      "funding": [
 | 
					      "funding": [
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          "type": "opencollective",
 | 
					          "type": "opencollective",
 | 
				
			||||||
@@ -620,9 +588,9 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/daisyui": {
 | 
					    "node_modules/daisyui": {
 | 
				
			||||||
      "version": "2.52.0",
 | 
					      "version": "2.51.5",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-2.52.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-2.51.5.tgz",
 | 
				
			||||||
      "integrity": "sha512-LQTA5/IVXAJHBMFoeaEMfd7/akAFPPcdQPR3O9fzzcFiczneJFM73CFPnScmW2sOgn/D83cvkP854ep2T9OfTg==",
 | 
					      "integrity": "sha512-L05dRw0tasmz2Ha+10LhftEGLq4kaA8vRR/T0wDaXfHwqcgsf81jfXDJ6NlZ63Z7Rl1k3rj7UHs0l0p7CM3aYA==",
 | 
				
			||||||
      "dependencies": {
 | 
					      "dependencies": {
 | 
				
			||||||
        "color": "^4.2",
 | 
					        "color": "^4.2",
 | 
				
			||||||
        "css-selector-tokenizer": "^0.8.0",
 | 
					        "css-selector-tokenizer": "^0.8.0",
 | 
				
			||||||
@@ -675,9 +643,9 @@
 | 
				
			|||||||
      "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="
 | 
					      "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/electron-to-chromium": {
 | 
					    "node_modules/electron-to-chromium": {
 | 
				
			||||||
      "version": "1.4.455",
 | 
					      "version": "1.4.369",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.455.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.369.tgz",
 | 
				
			||||||
      "integrity": "sha512-8tgdX0Odl24LtmLwxotpJCVjIndN559AvaOtd67u+2mo+IDsgsTF580NB+uuDCqsHw8yFg53l5+imFV9Fw3cbA=="
 | 
					      "integrity": "sha512-LfxbHXdA/S+qyoTEA4EbhxGjrxx7WK2h6yb5K2v0UCOufUKX+VZaHbl3svlzZfv9sGseym/g3Ne4DpsgRULmqg=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/esbuild": {
 | 
					    "node_modules/esbuild": {
 | 
				
			||||||
      "version": "0.15.18",
 | 
					      "version": "0.15.18",
 | 
				
			||||||
@@ -1045,9 +1013,9 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/fast-glob": {
 | 
					    "node_modules/fast-glob": {
 | 
				
			||||||
      "version": "3.3.0",
 | 
					      "version": "3.2.12",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz",
 | 
				
			||||||
      "integrity": "sha512-ChDuvbOypPuNjO8yIDf36x7BlZX1smcUMTTcyoIjycexOxd6DFsKsg21qVBzEmr3G7fUKIRy2/psii+CIUt7FA==",
 | 
					      "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==",
 | 
				
			||||||
      "dependencies": {
 | 
					      "dependencies": {
 | 
				
			||||||
        "@nodelib/fs.stat": "^2.0.2",
 | 
					        "@nodelib/fs.stat": "^2.0.2",
 | 
				
			||||||
        "@nodelib/fs.walk": "^1.2.3",
 | 
					        "@nodelib/fs.walk": "^1.2.3",
 | 
				
			||||||
@@ -1201,9 +1169,9 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/is-core-module": {
 | 
					    "node_modules/is-core-module": {
 | 
				
			||||||
      "version": "2.12.1",
 | 
					      "version": "2.12.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.0.tgz",
 | 
				
			||||||
      "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==",
 | 
					      "integrity": "sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==",
 | 
				
			||||||
      "dependencies": {
 | 
					      "dependencies": {
 | 
				
			||||||
        "has": "^1.0.3"
 | 
					        "has": "^1.0.3"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
@@ -1239,9 +1207,9 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/jiti": {
 | 
					    "node_modules/jiti": {
 | 
				
			||||||
      "version": "1.19.1",
 | 
					      "version": "1.18.2",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.19.1.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.18.2.tgz",
 | 
				
			||||||
      "integrity": "sha512-oVhqoRDaBXf7sjkll95LHVS6Myyyb1zaunVwk4Z0+WPSW4gjS0pl01zYKHScTuyEhQsFxV5L4DR5r+YqSyqyyg==",
 | 
					      "integrity": "sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==",
 | 
				
			||||||
      "bin": {
 | 
					      "bin": {
 | 
				
			||||||
        "jiti": "bin/jiti.js"
 | 
					        "jiti": "bin/jiti.js"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@@ -1345,9 +1313,9 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/node-releases": {
 | 
					    "node_modules/node-releases": {
 | 
				
			||||||
      "version": "2.0.13",
 | 
					      "version": "2.0.10",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz",
 | 
				
			||||||
      "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ=="
 | 
					      "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/normalize-path": {
 | 
					    "node_modules/normalize-path": {
 | 
				
			||||||
      "version": "3.0.0",
 | 
					      "version": "3.0.0",
 | 
				
			||||||
@@ -1427,17 +1395,17 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/pirates": {
 | 
					    "node_modules/pirates": {
 | 
				
			||||||
      "version": "4.0.6",
 | 
					      "version": "4.0.5",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz",
 | 
				
			||||||
      "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==",
 | 
					      "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==",
 | 
				
			||||||
      "engines": {
 | 
					      "engines": {
 | 
				
			||||||
        "node": ">= 6"
 | 
					        "node": ">= 6"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/postcss": {
 | 
					    "node_modules/postcss": {
 | 
				
			||||||
      "version": "8.4.25",
 | 
					      "version": "8.4.23",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.25.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz",
 | 
				
			||||||
      "integrity": "sha512-7taJ/8t2av0Z+sQEvNzCkpDynl0tX3uJMCODi6nT3PfASC7dYCWV9aQ+uiCf+KBD4SEFcu+GvJdGdwzQ6OSjCw==",
 | 
					      "integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==",
 | 
				
			||||||
      "funding": [
 | 
					      "funding": [
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          "type": "opencollective",
 | 
					          "type": "opencollective",
 | 
				
			||||||
@@ -1462,16 +1430,16 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/postcss-import": {
 | 
					    "node_modules/postcss-import": {
 | 
				
			||||||
      "version": "15.1.0",
 | 
					      "version": "14.1.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz",
 | 
				
			||||||
      "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
 | 
					      "integrity": "sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==",
 | 
				
			||||||
      "dependencies": {
 | 
					      "dependencies": {
 | 
				
			||||||
        "postcss-value-parser": "^4.0.0",
 | 
					        "postcss-value-parser": "^4.0.0",
 | 
				
			||||||
        "read-cache": "^1.0.0",
 | 
					        "read-cache": "^1.0.0",
 | 
				
			||||||
        "resolve": "^1.1.7"
 | 
					        "resolve": "^1.1.7"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      "engines": {
 | 
					      "engines": {
 | 
				
			||||||
        "node": ">=14.0.0"
 | 
					        "node": ">=10.0.0"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      "peerDependencies": {
 | 
					      "peerDependencies": {
 | 
				
			||||||
        "postcss": "^8.0.0"
 | 
					        "postcss": "^8.0.0"
 | 
				
			||||||
@@ -1496,15 +1464,15 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/postcss-load-config": {
 | 
					    "node_modules/postcss-load-config": {
 | 
				
			||||||
      "version": "4.0.1",
 | 
					      "version": "3.1.4",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.1.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz",
 | 
				
			||||||
      "integrity": "sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==",
 | 
					      "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==",
 | 
				
			||||||
      "dependencies": {
 | 
					      "dependencies": {
 | 
				
			||||||
        "lilconfig": "^2.0.5",
 | 
					        "lilconfig": "^2.0.5",
 | 
				
			||||||
        "yaml": "^2.1.1"
 | 
					        "yaml": "^1.10.2"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      "engines": {
 | 
					      "engines": {
 | 
				
			||||||
        "node": ">= 14"
 | 
					        "node": ">= 10"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      "funding": {
 | 
					      "funding": {
 | 
				
			||||||
        "type": "opencollective",
 | 
					        "type": "opencollective",
 | 
				
			||||||
@@ -1524,11 +1492,11 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/postcss-nested": {
 | 
					    "node_modules/postcss-nested": {
 | 
				
			||||||
      "version": "6.0.1",
 | 
					      "version": "6.0.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.0.tgz",
 | 
				
			||||||
      "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==",
 | 
					      "integrity": "sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==",
 | 
				
			||||||
      "dependencies": {
 | 
					      "dependencies": {
 | 
				
			||||||
        "postcss-selector-parser": "^6.0.11"
 | 
					        "postcss-selector-parser": "^6.0.10"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      "engines": {
 | 
					      "engines": {
 | 
				
			||||||
        "node": ">=12.0"
 | 
					        "node": ">=12.0"
 | 
				
			||||||
@@ -1542,9 +1510,9 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/postcss-selector-parser": {
 | 
					    "node_modules/postcss-selector-parser": {
 | 
				
			||||||
      "version": "6.0.13",
 | 
					      "version": "6.0.11",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz",
 | 
				
			||||||
      "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==",
 | 
					      "integrity": "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==",
 | 
				
			||||||
      "dependencies": {
 | 
					      "dependencies": {
 | 
				
			||||||
        "cssesc": "^3.0.0",
 | 
					        "cssesc": "^3.0.0",
 | 
				
			||||||
        "util-deprecate": "^1.0.2"
 | 
					        "util-deprecate": "^1.0.2"
 | 
				
			||||||
@@ -1577,6 +1545,17 @@
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
      ]
 | 
					      ]
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/quick-lru": {
 | 
				
			||||||
 | 
					      "version": "5.1.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=10"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "funding": {
 | 
				
			||||||
 | 
					        "url": "https://github.com/sponsors/sindresorhus"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/read-cache": {
 | 
					    "node_modules/read-cache": {
 | 
				
			||||||
      "version": "1.0.0",
 | 
					      "version": "1.0.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
 | 
				
			||||||
@@ -1714,61 +1693,65 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/svelte": {
 | 
					    "node_modules/svelte": {
 | 
				
			||||||
      "version": "3.59.2",
 | 
					      "version": "3.58.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.59.2.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.58.0.tgz",
 | 
				
			||||||
      "integrity": "sha512-vzSyuGr3eEoAtT/A6bmajosJZIUWySzY2CzB3w2pgPvnkUjGqlDnsNnA0PMO+mMAhuyMul6C2uuZzY6ELSkzyA==",
 | 
					      "integrity": "sha512-brIBNNB76mXFmU/Kerm4wFnkskBbluBDCjx/8TcpYRb298Yh2dztS2kQ6bhtjMcvUhd5ynClfwpz5h2gnzdQ1A==",
 | 
				
			||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "engines": {
 | 
					      "engines": {
 | 
				
			||||||
        "node": ">= 8"
 | 
					        "node": ">= 8"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/svelte-hmr": {
 | 
					    "node_modules/svelte-hmr": {
 | 
				
			||||||
      "version": "0.15.2",
 | 
					      "version": "0.15.1",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.2.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.1.tgz",
 | 
				
			||||||
      "integrity": "sha512-q/bAruCvFLwvNbeE1x3n37TYFb3mTBJ6TrCq6p2CoFbSTNhDE9oAtEfpy+wmc9So8AG0Tja+X0/mJzX9tSfvIg==",
 | 
					      "integrity": "sha512-BiKB4RZ8YSwRKCNVdNxK/GfY+r4Kjgp9jCLEy0DuqAKfmQtpL38cQK3afdpjw4sqSs4PLi3jIPJIFp259NkZtA==",
 | 
				
			||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "engines": {
 | 
					      "engines": {
 | 
				
			||||||
        "node": "^12.20 || ^14.13.1 || >= 16"
 | 
					        "node": "^12.20 || ^14.13.1 || >= 16"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      "peerDependencies": {
 | 
					      "peerDependencies": {
 | 
				
			||||||
        "svelte": "^3.19.0 || ^4.0.0-next.0"
 | 
					        "svelte": ">=3.19.0"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/tailwindcss": {
 | 
					    "node_modules/tailwindcss": {
 | 
				
			||||||
      "version": "3.3.2",
 | 
					      "version": "3.3.1",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.2.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.1.tgz",
 | 
				
			||||||
      "integrity": "sha512-9jPkMiIBXvPc2KywkraqsUfbfj+dHDb+JPWtSJa9MLFdrPyazI7q6WX2sUrm7R9eVR7qqv3Pas7EvQFzxKnI6w==",
 | 
					      "integrity": "sha512-Vkiouc41d4CEq0ujXl6oiGFQ7bA3WEhUZdTgXAhtKxSy49OmKs8rEfQmupsfF0IGW8fv2iQkp1EVUuapCFrZ9g==",
 | 
				
			||||||
      "dependencies": {
 | 
					      "dependencies": {
 | 
				
			||||||
        "@alloc/quick-lru": "^5.2.0",
 | 
					 | 
				
			||||||
        "arg": "^5.0.2",
 | 
					        "arg": "^5.0.2",
 | 
				
			||||||
        "chokidar": "^3.5.3",
 | 
					        "chokidar": "^3.5.3",
 | 
				
			||||||
 | 
					        "color-name": "^1.1.4",
 | 
				
			||||||
        "didyoumean": "^1.2.2",
 | 
					        "didyoumean": "^1.2.2",
 | 
				
			||||||
        "dlv": "^1.1.3",
 | 
					        "dlv": "^1.1.3",
 | 
				
			||||||
        "fast-glob": "^3.2.12",
 | 
					        "fast-glob": "^3.2.12",
 | 
				
			||||||
        "glob-parent": "^6.0.2",
 | 
					        "glob-parent": "^6.0.2",
 | 
				
			||||||
        "is-glob": "^4.0.3",
 | 
					        "is-glob": "^4.0.3",
 | 
				
			||||||
        "jiti": "^1.18.2",
 | 
					        "jiti": "^1.17.2",
 | 
				
			||||||
        "lilconfig": "^2.1.0",
 | 
					        "lilconfig": "^2.0.6",
 | 
				
			||||||
        "micromatch": "^4.0.5",
 | 
					        "micromatch": "^4.0.5",
 | 
				
			||||||
        "normalize-path": "^3.0.0",
 | 
					        "normalize-path": "^3.0.0",
 | 
				
			||||||
        "object-hash": "^3.0.0",
 | 
					        "object-hash": "^3.0.0",
 | 
				
			||||||
        "picocolors": "^1.0.0",
 | 
					        "picocolors": "^1.0.0",
 | 
				
			||||||
        "postcss": "^8.4.23",
 | 
					        "postcss": "^8.0.9",
 | 
				
			||||||
        "postcss-import": "^15.1.0",
 | 
					        "postcss-import": "^14.1.0",
 | 
				
			||||||
        "postcss-js": "^4.0.1",
 | 
					        "postcss-js": "^4.0.0",
 | 
				
			||||||
        "postcss-load-config": "^4.0.1",
 | 
					        "postcss-load-config": "^3.1.4",
 | 
				
			||||||
        "postcss-nested": "^6.0.1",
 | 
					        "postcss-nested": "6.0.0",
 | 
				
			||||||
        "postcss-selector-parser": "^6.0.11",
 | 
					        "postcss-selector-parser": "^6.0.11",
 | 
				
			||||||
        "postcss-value-parser": "^4.2.0",
 | 
					        "postcss-value-parser": "^4.2.0",
 | 
				
			||||||
        "resolve": "^1.22.2",
 | 
					        "quick-lru": "^5.1.1",
 | 
				
			||||||
        "sucrase": "^3.32.0"
 | 
					        "resolve": "^1.22.1",
 | 
				
			||||||
 | 
					        "sucrase": "^3.29.0"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      "bin": {
 | 
					      "bin": {
 | 
				
			||||||
        "tailwind": "lib/cli.js",
 | 
					        "tailwind": "lib/cli.js",
 | 
				
			||||||
        "tailwindcss": "lib/cli.js"
 | 
					        "tailwindcss": "lib/cli.js"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      "engines": {
 | 
					      "engines": {
 | 
				
			||||||
        "node": ">=14.0.0"
 | 
					        "node": ">=12.13.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "peerDependencies": {
 | 
				
			||||||
 | 
					        "postcss": "^8.0.9"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/thenify": {
 | 
					    "node_modules/thenify": {
 | 
				
			||||||
@@ -1841,9 +1824,9 @@
 | 
				
			|||||||
      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
 | 
					      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/vite": {
 | 
					    "node_modules/vite": {
 | 
				
			||||||
      "version": "3.2.7",
 | 
					      "version": "3.2.6",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.7.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.6.tgz",
 | 
				
			||||||
      "integrity": "sha512-29pdXjk49xAP0QBr0xXqu2s5jiQIXNvE/xwd0vUizYT2Hzqe4BksNNoWllFVXJf4eLZ+UlVQmXfB4lWrc+t18g==",
 | 
					      "integrity": "sha512-nTXTxYVvaQNLoW5BQ8PNNQ3lPia57gzsQU/Khv+JvzKPku8kNZL6NMUR/qwXhMG6E+g1idqEPanomJ+VZgixEg==",
 | 
				
			||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "dependencies": {
 | 
					      "dependencies": {
 | 
				
			||||||
        "esbuild": "^0.15.9",
 | 
					        "esbuild": "^0.15.9",
 | 
				
			||||||
@@ -1909,20 +1892,15 @@
 | 
				
			|||||||
      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
 | 
					      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/yaml": {
 | 
					    "node_modules/yaml": {
 | 
				
			||||||
      "version": "2.3.1",
 | 
					      "version": "1.10.2",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
 | 
				
			||||||
      "integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==",
 | 
					      "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
 | 
				
			||||||
      "engines": {
 | 
					      "engines": {
 | 
				
			||||||
        "node": ">= 14"
 | 
					        "node": ">= 6"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "dependencies": {
 | 
					  "dependencies": {
 | 
				
			||||||
    "@alloc/quick-lru": {
 | 
					 | 
				
			||||||
      "version": "5.2.0",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "@esbuild/android-arm": {
 | 
					    "@esbuild/android-arm": {
 | 
				
			||||||
      "version": "0.15.18",
 | 
					      "version": "0.15.18",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.18.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.18.tgz",
 | 
				
			||||||
@@ -2016,95 +1994,87 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "@tauri-apps/api": {
 | 
					    "@tauri-apps/api": {
 | 
				
			||||||
      "version": "1.4.0",
 | 
					      "version": "1.2.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-1.4.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-1.2.0.tgz",
 | 
				
			||||||
      "integrity": "sha512-Jd6HPoTM1PZSFIzq7FB8VmMu3qSSyo/3lSwLpoapW+lQ41CL5Dow2KryLg+gyazA/58DRWI9vu/XpEeHK4uMdw=="
 | 
					      "integrity": "sha512-lsI54KI6HGf7VImuf/T9pnoejfgkNoXveP14pVV7XarrQ46rOejIVJLFqHI9sRReJMGdh2YuCoI3cc/yCWCsrw=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "@tauri-apps/cli": {
 | 
					    "@tauri-apps/cli": {
 | 
				
			||||||
      "version": "1.4.0",
 | 
					      "version": "1.2.3",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-1.4.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-1.2.3.tgz",
 | 
				
			||||||
      "integrity": "sha512-VXYr2i2iVFl98etQSQsqLzXgX96bnWiNZd1YADgatqwy/qecbd6Kl5ZAPB5R4ynsgE8A1gU7Fbzh7dCEQYFfmA==",
 | 
					      "integrity": "sha512-erxtXuPhMEGJPBtnhPILD4AjuT81GZsraqpFvXAmEJZ2p8P6t7MVBifCL8LznRknznM3jn90D3M8RNBP3wcXTw==",
 | 
				
			||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "requires": {
 | 
					      "requires": {
 | 
				
			||||||
        "@tauri-apps/cli-darwin-arm64": "1.4.0",
 | 
					        "@tauri-apps/cli-darwin-arm64": "1.2.3",
 | 
				
			||||||
        "@tauri-apps/cli-darwin-x64": "1.4.0",
 | 
					        "@tauri-apps/cli-darwin-x64": "1.2.3",
 | 
				
			||||||
        "@tauri-apps/cli-linux-arm-gnueabihf": "1.4.0",
 | 
					        "@tauri-apps/cli-linux-arm-gnueabihf": "1.2.3",
 | 
				
			||||||
        "@tauri-apps/cli-linux-arm64-gnu": "1.4.0",
 | 
					        "@tauri-apps/cli-linux-arm64-gnu": "1.2.3",
 | 
				
			||||||
        "@tauri-apps/cli-linux-arm64-musl": "1.4.0",
 | 
					        "@tauri-apps/cli-linux-arm64-musl": "1.2.3",
 | 
				
			||||||
        "@tauri-apps/cli-linux-x64-gnu": "1.4.0",
 | 
					        "@tauri-apps/cli-linux-x64-gnu": "1.2.3",
 | 
				
			||||||
        "@tauri-apps/cli-linux-x64-musl": "1.4.0",
 | 
					        "@tauri-apps/cli-linux-x64-musl": "1.2.3",
 | 
				
			||||||
        "@tauri-apps/cli-win32-arm64-msvc": "1.4.0",
 | 
					        "@tauri-apps/cli-win32-ia32-msvc": "1.2.3",
 | 
				
			||||||
        "@tauri-apps/cli-win32-ia32-msvc": "1.4.0",
 | 
					        "@tauri-apps/cli-win32-x64-msvc": "1.2.3"
 | 
				
			||||||
        "@tauri-apps/cli-win32-x64-msvc": "1.4.0"
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "@tauri-apps/cli-darwin-arm64": {
 | 
					    "@tauri-apps/cli-darwin-arm64": {
 | 
				
			||||||
      "version": "1.4.0",
 | 
					      "version": "1.2.3",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-1.4.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-1.2.3.tgz",
 | 
				
			||||||
      "integrity": "sha512-nA/ml0SfUt6/CYLVbHmT500Y+ijqsuv5+s9EBnVXYSLVg9kbPUZJJHluEYK+xKuOj6xzyuT/+rZFMRapmJD3jQ==",
 | 
					      "integrity": "sha512-phJN3fN8FtZZwqXg08bcxfq1+X1JSDglLvRxOxB7VWPq+O5SuB8uLyssjJsu+PIhyZZnIhTGdjhzLSFhSXfLsw==",
 | 
				
			||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "optional": true
 | 
					      "optional": true
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "@tauri-apps/cli-darwin-x64": {
 | 
					    "@tauri-apps/cli-darwin-x64": {
 | 
				
			||||||
      "version": "1.4.0",
 | 
					      "version": "1.2.3",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-1.4.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-1.2.3.tgz",
 | 
				
			||||||
      "integrity": "sha512-ov/F6Zr+dg9B0PtRu65stFo2G0ow2TUlneqYYrkj+vA3n+moWDHfVty0raDjMLQbQt3rv3uayFMXGPMgble9OA==",
 | 
					      "integrity": "sha512-jFZ/y6z8z6v4yliIbXKBXA7BJgtZVMsITmEXSuD6s5+eCOpDhQxbRkr6CA+FFfr+/r96rWSDSgDenDQuSvPAKw==",
 | 
				
			||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "optional": true
 | 
					      "optional": true
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "@tauri-apps/cli-linux-arm-gnueabihf": {
 | 
					    "@tauri-apps/cli-linux-arm-gnueabihf": {
 | 
				
			||||||
      "version": "1.4.0",
 | 
					      "version": "1.2.3",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-1.4.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-1.2.3.tgz",
 | 
				
			||||||
      "integrity": "sha512-zwjbiMncycXDV7doovymyKD7sCg53ouAmfgpUqEBOTY3vgBi9TwijyPhJOqoG5vUVWhouNBC08akGmE4dja15g==",
 | 
					      "integrity": "sha512-C7h5vqAwXzY0kRGSU00Fj8PudiDWFCiQqqUNI1N+fhCILrzWZB9TPBwdx33ZfXKt/U4+emdIoo/N34v3TiAOmQ==",
 | 
				
			||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "optional": true
 | 
					      "optional": true
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "@tauri-apps/cli-linux-arm64-gnu": {
 | 
					    "@tauri-apps/cli-linux-arm64-gnu": {
 | 
				
			||||||
      "version": "1.4.0",
 | 
					      "version": "1.2.3",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-1.4.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-1.2.3.tgz",
 | 
				
			||||||
      "integrity": "sha512-5MCBcziqXC72mMXnkZU68mutXIR6zavDxopArE2gQtK841IlE06bIgtLi0kUUhlFJk2nhPRgiDgdLbrPlyt7fw==",
 | 
					      "integrity": "sha512-buf1c8sdkuUzVDkGPQpyUdAIIdn5r0UgXU6+H5fGPq/Xzt5K69JzXaeo6fHsZEZghbV0hOK+taKV4J0m30UUMQ==",
 | 
				
			||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "optional": true
 | 
					      "optional": true
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "@tauri-apps/cli-linux-arm64-musl": {
 | 
					    "@tauri-apps/cli-linux-arm64-musl": {
 | 
				
			||||||
      "version": "1.4.0",
 | 
					      "version": "1.2.3",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.4.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.2.3.tgz",
 | 
				
			||||||
      "integrity": "sha512-7J3pRB6n6uNYgIfCeKt2Oz8J7oSaz2s8GGFRRH2HPxuTHrBNCinzVYm68UhVpJrL3bnGkU0ziVZLsW/iaOGfUg==",
 | 
					      "integrity": "sha512-x88wPS9W5xAyk392vc4uNHcKBBvCp0wf4H9JFMF9OBwB7vfd59LbQCFcPSu8f0BI7bPrOsyHqspWHuFL8ojQEA==",
 | 
				
			||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "optional": true
 | 
					      "optional": true
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "@tauri-apps/cli-linux-x64-gnu": {
 | 
					    "@tauri-apps/cli-linux-x64-gnu": {
 | 
				
			||||||
      "version": "1.4.0",
 | 
					      "version": "1.2.3",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-1.4.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-1.2.3.tgz",
 | 
				
			||||||
      "integrity": "sha512-Zh5gfAJxOv5AVWxcwuueaQ2vIAhlg0d6nZui6nMyfIJ8dbf3aZQ5ZzP38sYow5h/fbvgL+3GSQxZRBIa3c2E1w==",
 | 
					      "integrity": "sha512-ZMz1jxEVe0B4/7NJnlPHmwmSIuwiD6ViXKs8F+OWWz2Y4jn5TGxWKFg7DLx5OwQTRvEIZxxT7lXHi5CuTNAxKg==",
 | 
				
			||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "optional": true
 | 
					      "optional": true
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "@tauri-apps/cli-linux-x64-musl": {
 | 
					    "@tauri-apps/cli-linux-x64-musl": {
 | 
				
			||||||
      "version": "1.4.0",
 | 
					      "version": "1.2.3",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-1.4.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-1.2.3.tgz",
 | 
				
			||||||
      "integrity": "sha512-OLAYoICU3FaYiTdBsI+lQTKnDHeMmFMXIApN0M+xGiOkoIOQcV9CConMPjgmJQ867+NHRNgUGlvBEAh9CiJodQ==",
 | 
					      "integrity": "sha512-B/az59EjJhdbZDzawEVox0LQu2ZHCZlk8rJf85AMIktIUoAZPFbwyiUv7/zjzA/sY6Nb58OSJgaPL2/IBy7E0A==",
 | 
				
			||||||
      "dev": true,
 | 
					 | 
				
			||||||
      "optional": true
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "@tauri-apps/cli-win32-arm64-msvc": {
 | 
					 | 
				
			||||||
      "version": "1.4.0",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-1.4.0.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-gZ05GENFbI6CB5MlOUsLlU0kZ9UtHn9riYtSXKT6MYs8HSPRffPHaHSL0WxsJweWh9nR5Hgh/TUU8uW3sYCzCg==",
 | 
					 | 
				
			||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "optional": true
 | 
					      "optional": true
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "@tauri-apps/cli-win32-ia32-msvc": {
 | 
					    "@tauri-apps/cli-win32-ia32-msvc": {
 | 
				
			||||||
      "version": "1.4.0",
 | 
					      "version": "1.2.3",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-1.4.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-1.2.3.tgz",
 | 
				
			||||||
      "integrity": "sha512-JsetT/lTx/Zq98eo8T5CiRyF1nKeX04RO8JlJrI3ZOYsZpp/A5RJvMd/szQ17iOzwiHdge+tx7k2jHysR6oBlQ==",
 | 
					      "integrity": "sha512-ypdO1OdC5ugNJAKO2m3sb1nsd+0TSvMS9Tr5qN/ZSMvtSduaNwrcZ3D7G/iOIanrqu/Nl8t3LYlgPZGBKlw7Ng==",
 | 
				
			||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "optional": true
 | 
					      "optional": true
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "@tauri-apps/cli-win32-x64-msvc": {
 | 
					    "@tauri-apps/cli-win32-x64-msvc": {
 | 
				
			||||||
      "version": "1.4.0",
 | 
					      "version": "1.2.3",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-1.4.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-1.2.3.tgz",
 | 
				
			||||||
      "integrity": "sha512-z8Olcnwp5aYhzqUAarFjqF+oELCjuYWnB2HAJHlfsYNfDCAORY5kct3Fklz8PSsubC3U2EugWn8n42DwnThurg==",
 | 
					      "integrity": "sha512-CsbHQ+XhnV/2csOBBDVfH16cdK00gNyNYUW68isedmqcn8j+s0e9cQ1xXIqi+Hue3awp8g3ImYN5KPepf3UExw==",
 | 
				
			||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "optional": true
 | 
					      "optional": true
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
@@ -2168,14 +2138,14 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "browserslist": {
 | 
					    "browserslist": {
 | 
				
			||||||
      "version": "4.21.9",
 | 
					      "version": "4.21.5",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz",
 | 
				
			||||||
      "integrity": "sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==",
 | 
					      "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==",
 | 
				
			||||||
      "requires": {
 | 
					      "requires": {
 | 
				
			||||||
        "caniuse-lite": "^1.0.30001503",
 | 
					        "caniuse-lite": "^1.0.30001449",
 | 
				
			||||||
        "electron-to-chromium": "^1.4.431",
 | 
					        "electron-to-chromium": "^1.4.284",
 | 
				
			||||||
        "node-releases": "^2.0.12",
 | 
					        "node-releases": "^2.0.8",
 | 
				
			||||||
        "update-browserslist-db": "^1.0.11"
 | 
					        "update-browserslist-db": "^1.0.10"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "camelcase-css": {
 | 
					    "camelcase-css": {
 | 
				
			||||||
@@ -2184,9 +2154,9 @@
 | 
				
			|||||||
      "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="
 | 
					      "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "caniuse-lite": {
 | 
					    "caniuse-lite": {
 | 
				
			||||||
      "version": "1.0.30001515",
 | 
					      "version": "1.0.30001481",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001515.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001481.tgz",
 | 
				
			||||||
      "integrity": "sha512-eEFDwUOZbE24sb+Ecsx3+OvNETqjWIdabMy52oOkIgcUtAsQifjUG9q4U9dgTHJM2mfk4uEPxc0+xuFdJ629QA=="
 | 
					      "integrity": "sha512-KCqHwRnaa1InZBtqXzP98LPg0ajCVujMKjqKDhZEthIpAsJl/YEIa3YvXjGXPVqzZVguccuu7ga9KOE1J9rKPQ=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "chokidar": {
 | 
					    "chokidar": {
 | 
				
			||||||
      "version": "3.5.3",
 | 
					      "version": "3.5.3",
 | 
				
			||||||
@@ -2269,9 +2239,9 @@
 | 
				
			|||||||
      "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="
 | 
					      "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "daisyui": {
 | 
					    "daisyui": {
 | 
				
			||||||
      "version": "2.52.0",
 | 
					      "version": "2.51.5",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-2.52.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-2.51.5.tgz",
 | 
				
			||||||
      "integrity": "sha512-LQTA5/IVXAJHBMFoeaEMfd7/akAFPPcdQPR3O9fzzcFiczneJFM73CFPnScmW2sOgn/D83cvkP854ep2T9OfTg==",
 | 
					      "integrity": "sha512-L05dRw0tasmz2Ha+10LhftEGLq4kaA8vRR/T0wDaXfHwqcgsf81jfXDJ6NlZ63Z7Rl1k3rj7UHs0l0p7CM3aYA==",
 | 
				
			||||||
      "requires": {
 | 
					      "requires": {
 | 
				
			||||||
        "color": "^4.2",
 | 
					        "color": "^4.2",
 | 
				
			||||||
        "css-selector-tokenizer": "^0.8.0",
 | 
					        "css-selector-tokenizer": "^0.8.0",
 | 
				
			||||||
@@ -2305,9 +2275,9 @@
 | 
				
			|||||||
      "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="
 | 
					      "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "electron-to-chromium": {
 | 
					    "electron-to-chromium": {
 | 
				
			||||||
      "version": "1.4.455",
 | 
					      "version": "1.4.369",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.455.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.369.tgz",
 | 
				
			||||||
      "integrity": "sha512-8tgdX0Odl24LtmLwxotpJCVjIndN559AvaOtd67u+2mo+IDsgsTF580NB+uuDCqsHw8yFg53l5+imFV9Fw3cbA=="
 | 
					      "integrity": "sha512-LfxbHXdA/S+qyoTEA4EbhxGjrxx7WK2h6yb5K2v0UCOufUKX+VZaHbl3svlzZfv9sGseym/g3Ne4DpsgRULmqg=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "esbuild": {
 | 
					    "esbuild": {
 | 
				
			||||||
      "version": "0.15.18",
 | 
					      "version": "0.15.18",
 | 
				
			||||||
@@ -2485,9 +2455,9 @@
 | 
				
			|||||||
      "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw=="
 | 
					      "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "fast-glob": {
 | 
					    "fast-glob": {
 | 
				
			||||||
      "version": "3.3.0",
 | 
					      "version": "3.2.12",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz",
 | 
				
			||||||
      "integrity": "sha512-ChDuvbOypPuNjO8yIDf36x7BlZX1smcUMTTcyoIjycexOxd6DFsKsg21qVBzEmr3G7fUKIRy2/psii+CIUt7FA==",
 | 
					      "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==",
 | 
				
			||||||
      "requires": {
 | 
					      "requires": {
 | 
				
			||||||
        "@nodelib/fs.stat": "^2.0.2",
 | 
					        "@nodelib/fs.stat": "^2.0.2",
 | 
				
			||||||
        "@nodelib/fs.walk": "^1.2.3",
 | 
					        "@nodelib/fs.walk": "^1.2.3",
 | 
				
			||||||
@@ -2605,9 +2575,9 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "is-core-module": {
 | 
					    "is-core-module": {
 | 
				
			||||||
      "version": "2.12.1",
 | 
					      "version": "2.12.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.0.tgz",
 | 
				
			||||||
      "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==",
 | 
					      "integrity": "sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==",
 | 
				
			||||||
      "requires": {
 | 
					      "requires": {
 | 
				
			||||||
        "has": "^1.0.3"
 | 
					        "has": "^1.0.3"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@@ -2631,9 +2601,9 @@
 | 
				
			|||||||
      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
 | 
					      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "jiti": {
 | 
					    "jiti": {
 | 
				
			||||||
      "version": "1.19.1",
 | 
					      "version": "1.18.2",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.19.1.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.18.2.tgz",
 | 
				
			||||||
      "integrity": "sha512-oVhqoRDaBXf7sjkll95LHVS6Myyyb1zaunVwk4Z0+WPSW4gjS0pl01zYKHScTuyEhQsFxV5L4DR5r+YqSyqyyg=="
 | 
					      "integrity": "sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "kleur": {
 | 
					    "kleur": {
 | 
				
			||||||
      "version": "4.1.5",
 | 
					      "version": "4.1.5",
 | 
				
			||||||
@@ -2704,9 +2674,9 @@
 | 
				
			|||||||
      "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA=="
 | 
					      "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node-releases": {
 | 
					    "node-releases": {
 | 
				
			||||||
      "version": "2.0.13",
 | 
					      "version": "2.0.10",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz",
 | 
				
			||||||
      "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ=="
 | 
					      "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "normalize-path": {
 | 
					    "normalize-path": {
 | 
				
			||||||
      "version": "3.0.0",
 | 
					      "version": "3.0.0",
 | 
				
			||||||
@@ -2762,14 +2732,14 @@
 | 
				
			|||||||
      "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="
 | 
					      "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "pirates": {
 | 
					    "pirates": {
 | 
				
			||||||
      "version": "4.0.6",
 | 
					      "version": "4.0.5",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz",
 | 
				
			||||||
      "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg=="
 | 
					      "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "postcss": {
 | 
					    "postcss": {
 | 
				
			||||||
      "version": "8.4.25",
 | 
					      "version": "8.4.23",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.25.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz",
 | 
				
			||||||
      "integrity": "sha512-7taJ/8t2av0Z+sQEvNzCkpDynl0tX3uJMCODi6nT3PfASC7dYCWV9aQ+uiCf+KBD4SEFcu+GvJdGdwzQ6OSjCw==",
 | 
					      "integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==",
 | 
				
			||||||
      "requires": {
 | 
					      "requires": {
 | 
				
			||||||
        "nanoid": "^3.3.6",
 | 
					        "nanoid": "^3.3.6",
 | 
				
			||||||
        "picocolors": "^1.0.0",
 | 
					        "picocolors": "^1.0.0",
 | 
				
			||||||
@@ -2777,9 +2747,9 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "postcss-import": {
 | 
					    "postcss-import": {
 | 
				
			||||||
      "version": "15.1.0",
 | 
					      "version": "14.1.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz",
 | 
				
			||||||
      "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
 | 
					      "integrity": "sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==",
 | 
				
			||||||
      "requires": {
 | 
					      "requires": {
 | 
				
			||||||
        "postcss-value-parser": "^4.0.0",
 | 
					        "postcss-value-parser": "^4.0.0",
 | 
				
			||||||
        "read-cache": "^1.0.0",
 | 
					        "read-cache": "^1.0.0",
 | 
				
			||||||
@@ -2795,26 +2765,26 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "postcss-load-config": {
 | 
					    "postcss-load-config": {
 | 
				
			||||||
      "version": "4.0.1",
 | 
					      "version": "3.1.4",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.1.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz",
 | 
				
			||||||
      "integrity": "sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==",
 | 
					      "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==",
 | 
				
			||||||
      "requires": {
 | 
					      "requires": {
 | 
				
			||||||
        "lilconfig": "^2.0.5",
 | 
					        "lilconfig": "^2.0.5",
 | 
				
			||||||
        "yaml": "^2.1.1"
 | 
					        "yaml": "^1.10.2"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "postcss-nested": {
 | 
					    "postcss-nested": {
 | 
				
			||||||
      "version": "6.0.1",
 | 
					      "version": "6.0.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.0.tgz",
 | 
				
			||||||
      "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==",
 | 
					      "integrity": "sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==",
 | 
				
			||||||
      "requires": {
 | 
					      "requires": {
 | 
				
			||||||
        "postcss-selector-parser": "^6.0.11"
 | 
					        "postcss-selector-parser": "^6.0.10"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "postcss-selector-parser": {
 | 
					    "postcss-selector-parser": {
 | 
				
			||||||
      "version": "6.0.13",
 | 
					      "version": "6.0.11",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz",
 | 
				
			||||||
      "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==",
 | 
					      "integrity": "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==",
 | 
				
			||||||
      "requires": {
 | 
					      "requires": {
 | 
				
			||||||
        "cssesc": "^3.0.0",
 | 
					        "cssesc": "^3.0.0",
 | 
				
			||||||
        "util-deprecate": "^1.0.2"
 | 
					        "util-deprecate": "^1.0.2"
 | 
				
			||||||
@@ -2830,6 +2800,11 @@
 | 
				
			|||||||
      "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
 | 
				
			||||||
      "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="
 | 
					      "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "quick-lru": {
 | 
				
			||||||
 | 
					      "version": "5.1.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "read-cache": {
 | 
					    "read-cache": {
 | 
				
			||||||
      "version": "1.0.0",
 | 
					      "version": "1.0.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
 | 
				
			||||||
@@ -2917,46 +2892,47 @@
 | 
				
			|||||||
      "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="
 | 
					      "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "svelte": {
 | 
					    "svelte": {
 | 
				
			||||||
      "version": "3.59.2",
 | 
					      "version": "3.58.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.59.2.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.58.0.tgz",
 | 
				
			||||||
      "integrity": "sha512-vzSyuGr3eEoAtT/A6bmajosJZIUWySzY2CzB3w2pgPvnkUjGqlDnsNnA0PMO+mMAhuyMul6C2uuZzY6ELSkzyA==",
 | 
					      "integrity": "sha512-brIBNNB76mXFmU/Kerm4wFnkskBbluBDCjx/8TcpYRb298Yh2dztS2kQ6bhtjMcvUhd5ynClfwpz5h2gnzdQ1A==",
 | 
				
			||||||
      "dev": true
 | 
					      "dev": true
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "svelte-hmr": {
 | 
					    "svelte-hmr": {
 | 
				
			||||||
      "version": "0.15.2",
 | 
					      "version": "0.15.1",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.2.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.1.tgz",
 | 
				
			||||||
      "integrity": "sha512-q/bAruCvFLwvNbeE1x3n37TYFb3mTBJ6TrCq6p2CoFbSTNhDE9oAtEfpy+wmc9So8AG0Tja+X0/mJzX9tSfvIg==",
 | 
					      "integrity": "sha512-BiKB4RZ8YSwRKCNVdNxK/GfY+r4Kjgp9jCLEy0DuqAKfmQtpL38cQK3afdpjw4sqSs4PLi3jIPJIFp259NkZtA==",
 | 
				
			||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "requires": {}
 | 
					      "requires": {}
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "tailwindcss": {
 | 
					    "tailwindcss": {
 | 
				
			||||||
      "version": "3.3.2",
 | 
					      "version": "3.3.1",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.2.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.1.tgz",
 | 
				
			||||||
      "integrity": "sha512-9jPkMiIBXvPc2KywkraqsUfbfj+dHDb+JPWtSJa9MLFdrPyazI7q6WX2sUrm7R9eVR7qqv3Pas7EvQFzxKnI6w==",
 | 
					      "integrity": "sha512-Vkiouc41d4CEq0ujXl6oiGFQ7bA3WEhUZdTgXAhtKxSy49OmKs8rEfQmupsfF0IGW8fv2iQkp1EVUuapCFrZ9g==",
 | 
				
			||||||
      "requires": {
 | 
					      "requires": {
 | 
				
			||||||
        "@alloc/quick-lru": "^5.2.0",
 | 
					 | 
				
			||||||
        "arg": "^5.0.2",
 | 
					        "arg": "^5.0.2",
 | 
				
			||||||
        "chokidar": "^3.5.3",
 | 
					        "chokidar": "^3.5.3",
 | 
				
			||||||
 | 
					        "color-name": "^1.1.4",
 | 
				
			||||||
        "didyoumean": "^1.2.2",
 | 
					        "didyoumean": "^1.2.2",
 | 
				
			||||||
        "dlv": "^1.1.3",
 | 
					        "dlv": "^1.1.3",
 | 
				
			||||||
        "fast-glob": "^3.2.12",
 | 
					        "fast-glob": "^3.2.12",
 | 
				
			||||||
        "glob-parent": "^6.0.2",
 | 
					        "glob-parent": "^6.0.2",
 | 
				
			||||||
        "is-glob": "^4.0.3",
 | 
					        "is-glob": "^4.0.3",
 | 
				
			||||||
        "jiti": "^1.18.2",
 | 
					        "jiti": "^1.17.2",
 | 
				
			||||||
        "lilconfig": "^2.1.0",
 | 
					        "lilconfig": "^2.0.6",
 | 
				
			||||||
        "micromatch": "^4.0.5",
 | 
					        "micromatch": "^4.0.5",
 | 
				
			||||||
        "normalize-path": "^3.0.0",
 | 
					        "normalize-path": "^3.0.0",
 | 
				
			||||||
        "object-hash": "^3.0.0",
 | 
					        "object-hash": "^3.0.0",
 | 
				
			||||||
        "picocolors": "^1.0.0",
 | 
					        "picocolors": "^1.0.0",
 | 
				
			||||||
        "postcss": "^8.4.23",
 | 
					        "postcss": "^8.0.9",
 | 
				
			||||||
        "postcss-import": "^15.1.0",
 | 
					        "postcss-import": "^14.1.0",
 | 
				
			||||||
        "postcss-js": "^4.0.1",
 | 
					        "postcss-js": "^4.0.0",
 | 
				
			||||||
        "postcss-load-config": "^4.0.1",
 | 
					        "postcss-load-config": "^3.1.4",
 | 
				
			||||||
        "postcss-nested": "^6.0.1",
 | 
					        "postcss-nested": "6.0.0",
 | 
				
			||||||
        "postcss-selector-parser": "^6.0.11",
 | 
					        "postcss-selector-parser": "^6.0.11",
 | 
				
			||||||
        "postcss-value-parser": "^4.2.0",
 | 
					        "postcss-value-parser": "^4.2.0",
 | 
				
			||||||
        "resolve": "^1.22.2",
 | 
					        "quick-lru": "^5.1.1",
 | 
				
			||||||
        "sucrase": "^3.32.0"
 | 
					        "resolve": "^1.22.1",
 | 
				
			||||||
 | 
					        "sucrase": "^3.29.0"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "thenify": {
 | 
					    "thenify": {
 | 
				
			||||||
@@ -3003,9 +2979,9 @@
 | 
				
			|||||||
      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
 | 
					      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "vite": {
 | 
					    "vite": {
 | 
				
			||||||
      "version": "3.2.7",
 | 
					      "version": "3.2.6",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.7.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.6.tgz",
 | 
				
			||||||
      "integrity": "sha512-29pdXjk49xAP0QBr0xXqu2s5jiQIXNvE/xwd0vUizYT2Hzqe4BksNNoWllFVXJf4eLZ+UlVQmXfB4lWrc+t18g==",
 | 
					      "integrity": "sha512-nTXTxYVvaQNLoW5BQ8PNNQ3lPia57gzsQU/Khv+JvzKPku8kNZL6NMUR/qwXhMG6E+g1idqEPanomJ+VZgixEg==",
 | 
				
			||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "requires": {
 | 
					      "requires": {
 | 
				
			||||||
        "esbuild": "^0.15.9",
 | 
					        "esbuild": "^0.15.9",
 | 
				
			||||||
@@ -3028,9 +3004,9 @@
 | 
				
			|||||||
      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
 | 
					      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "yaml": {
 | 
					    "yaml": {
 | 
				
			||||||
      "version": "2.3.1",
 | 
					      "version": "1.10.2",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
 | 
				
			||||||
      "integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ=="
 | 
					      "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "name": "creddy",
 | 
					  "name": "creddy",
 | 
				
			||||||
  "version": "0.2.3",
 | 
					  "version": "0.1.0",
 | 
				
			||||||
  "scripts": {
 | 
					  "scripts": {
 | 
				
			||||||
    "dev": "vite",
 | 
					    "dev": "vite",
 | 
				
			||||||
    "build": "vite build",
 | 
					    "build": "vite build",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1 +1 @@
 | 
				
			|||||||
DATABASE_URL=sqlite://C:/Users/Joe/AppData/Roaming/creddy/creddy.dev.db
 | 
					DATABASE_URL=sqlite://creddy.db?mode=rwc
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										1286
									
								
								src-tauri/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1286
									
								
								src-tauri/Cargo.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -1,22 +1,14 @@
 | 
				
			|||||||
[package]
 | 
					[package]
 | 
				
			||||||
name = "creddy"
 | 
					name = "app"
 | 
				
			||||||
version = "0.2.3"
 | 
					version = "0.1.0"
 | 
				
			||||||
description = "A friendly AWS credentials manager"
 | 
					description = "A Tauri App"
 | 
				
			||||||
authors = ["Joseph Montanaro"]
 | 
					authors = ["you"]
 | 
				
			||||||
license = ""
 | 
					license = ""
 | 
				
			||||||
repository = ""
 | 
					repository = ""
 | 
				
			||||||
default-run = "creddy"
 | 
					default-run = "app"
 | 
				
			||||||
edition = "2021"
 | 
					edition = "2021"
 | 
				
			||||||
rust-version = "1.57"
 | 
					rust-version = "1.57"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[bin]]
 | 
					 | 
				
			||||||
name = "creddy_cli"
 | 
					 | 
				
			||||||
path = "src/bin/creddy_cli.rs"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[[bin]]
 | 
					 | 
				
			||||||
name = "creddy"
 | 
					 | 
				
			||||||
path = "src/main.rs"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 | 
					# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[build-dependencies]
 | 
					[build-dependencies]
 | 
				
			||||||
@@ -42,10 +34,6 @@ strum = "0.24"
 | 
				
			|||||||
strum_macros = "0.24"
 | 
					strum_macros = "0.24"
 | 
				
			||||||
auto-launch = "0.4.0"
 | 
					auto-launch = "0.4.0"
 | 
				
			||||||
dirs = "5.0"
 | 
					dirs = "5.0"
 | 
				
			||||||
clap = { version = "3.2.23", features = ["derive"] }
 | 
					 | 
				
			||||||
is-terminal = "0.4.7"
 | 
					 | 
				
			||||||
argon2 = { version = "0.5.0", features = ["std"] }
 | 
					 | 
				
			||||||
chacha20poly1305 = { version = "0.10.1", features = ["std"] }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
[features]
 | 
					[features]
 | 
				
			||||||
# by default Tauri runs in production mode
 | 
					# by default Tauri runs in production mode
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,22 +0,0 @@
 | 
				
			|||||||
<?xml version="1.0" encoding="utf-8"?>
 | 
					 | 
				
			||||||
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
 | 
					 | 
				
			||||||
    <Fragment>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <DirectoryRef Id="INSTALLDIR">
 | 
					 | 
				
			||||||
            <!-- Create a subdirectory for the console binary so that we can add it to PATH -->
 | 
					 | 
				
			||||||
            <Directory Id="BinDir" Name="bin">
 | 
					 | 
				
			||||||
                <Component Id="CliBinary" Guid="b6358c8e-504f-41fd-b14b-38af821dcd04">
 | 
					 | 
				
			||||||
                    <!-- Same name as the main executable, so that it can be invoked as just "creddy" -->
 | 
					 | 
				
			||||||
                    <File Id="Bin_Cli" Source="..\..\creddy_cli.exe" Name="creddy.exe" KeyPath="yes"/>
 | 
					 | 
				
			||||||
                </Component>
 | 
					 | 
				
			||||||
            </Directory>
 | 
					 | 
				
			||||||
        </DirectoryRef>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <DirectoryRef Id="TARGETDIR">
 | 
					 | 
				
			||||||
            <Component Id="AddToPath" Guid="b5fdaf7e-94f2-4aad-9144-aa3a8edfa675">
 | 
					 | 
				
			||||||
                <Environment Id="CreddyInstallDir" Action="set" Name="PATH" Part="last" Permanent="no" Value="[BinDir]" />
 | 
					 | 
				
			||||||
            </Component>
 | 
					 | 
				
			||||||
        </DirectoryRef>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    </Fragment>
 | 
					 | 
				
			||||||
</Wix>
 | 
					 | 
				
			||||||
@@ -1,92 +0,0 @@
 | 
				
			|||||||
use std::error::Error;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use once_cell::sync::OnceCell;
 | 
					 | 
				
			||||||
use sqlx::{
 | 
					 | 
				
			||||||
    SqlitePool,
 | 
					 | 
				
			||||||
    sqlite::SqlitePoolOptions,
 | 
					 | 
				
			||||||
    sqlite::SqliteConnectOptions,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
use tauri::{
 | 
					 | 
				
			||||||
    App,
 | 
					 | 
				
			||||||
    AppHandle,
 | 
					 | 
				
			||||||
    Manager,
 | 
					 | 
				
			||||||
    async_runtime as rt,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use crate::{
 | 
					 | 
				
			||||||
    config::{self, AppConfig},
 | 
					 | 
				
			||||||
    credentials::Session,
 | 
					 | 
				
			||||||
    ipc,
 | 
					 | 
				
			||||||
    server::Server,
 | 
					 | 
				
			||||||
    errors::*,
 | 
					 | 
				
			||||||
    state::AppState,
 | 
					 | 
				
			||||||
    tray,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub static APP: OnceCell<AppHandle> = OnceCell::new();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub fn run() -> tauri::Result<()> {
 | 
					 | 
				
			||||||
    tauri::Builder::default()
 | 
					 | 
				
			||||||
        .plugin(tauri_plugin_single_instance::init(|app, _argv, _cwd| {
 | 
					 | 
				
			||||||
            app.get_window("main")
 | 
					 | 
				
			||||||
                .map(|w| w.show().error_popup("Failed to show main window"));
 | 
					 | 
				
			||||||
        }))
 | 
					 | 
				
			||||||
        .system_tray(tray::create())
 | 
					 | 
				
			||||||
        .on_system_tray_event(tray::handle_event)
 | 
					 | 
				
			||||||
        .invoke_handler(tauri::generate_handler![
 | 
					 | 
				
			||||||
            ipc::unlock,
 | 
					 | 
				
			||||||
            ipc::respond,
 | 
					 | 
				
			||||||
            ipc::get_session_status,
 | 
					 | 
				
			||||||
            ipc::save_credentials,
 | 
					 | 
				
			||||||
            ipc::get_config,
 | 
					 | 
				
			||||||
            ipc::save_config,
 | 
					 | 
				
			||||||
        ])
 | 
					 | 
				
			||||||
        .setup(|app| rt::block_on(setup(app)))
 | 
					 | 
				
			||||||
        .build(tauri::generate_context!())?
 | 
					 | 
				
			||||||
        .run(|app, run_event| match run_event {
 | 
					 | 
				
			||||||
            tauri::RunEvent::WindowEvent { label, event, .. } => match event {
 | 
					 | 
				
			||||||
                tauri::WindowEvent::CloseRequested { api, .. } => {
 | 
					 | 
				
			||||||
                    let _ = app.get_window(&label).map(|w| w.hide());
 | 
					 | 
				
			||||||
                    api.prevent_close();
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                _ => ()
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            _ => ()
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Ok(())
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub async fn connect_db() -> Result<SqlitePool, SetupError> {
 | 
					 | 
				
			||||||
    let conn_opts = SqliteConnectOptions::new()
 | 
					 | 
				
			||||||
        .filename(config::get_or_create_db_path()?)
 | 
					 | 
				
			||||||
        .create_if_missing(true);
 | 
					 | 
				
			||||||
    let pool_opts = SqlitePoolOptions::new();
 | 
					 | 
				
			||||||
    let pool: SqlitePool = pool_opts.connect_with(conn_opts).await?;
 | 
					 | 
				
			||||||
    sqlx::migrate!().run(&pool).await?;
 | 
					 | 
				
			||||||
    Ok(pool)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async fn setup(app: &mut App) -> Result<(), Box<dyn Error>> {
 | 
					 | 
				
			||||||
    APP.set(app.handle()).unwrap();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let pool = connect_db().await?;
 | 
					 | 
				
			||||||
    let conf = AppConfig::load(&pool).await?;
 | 
					 | 
				
			||||||
    let session = Session::load(&pool).await?;
 | 
					 | 
				
			||||||
    let srv = Server::new(conf.listen_addr, conf.listen_port, app.handle()).await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    config::set_auto_launch(conf.start_on_login)?;
 | 
					 | 
				
			||||||
    if !conf.start_minimized {
 | 
					 | 
				
			||||||
        app.get_window("main")
 | 
					 | 
				
			||||||
            .ok_or(HandlerError::NoMainWindow)?
 | 
					 | 
				
			||||||
            .show()?;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let state = AppState::new(conf, session, srv, pool);
 | 
					 | 
				
			||||||
    app.manage(state);
 | 
					 | 
				
			||||||
    Ok(())
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,45 +0,0 @@
 | 
				
			|||||||
// Windows isn't really amenable to having a single executable work as both a CLI and GUI app,
 | 
					 | 
				
			||||||
// so we just have a second binary for CLI usage
 | 
					 | 
				
			||||||
use creddy::{
 | 
					 | 
				
			||||||
    cli,
 | 
					 | 
				
			||||||
    errors::CliError,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
use std::{
 | 
					 | 
				
			||||||
    env,
 | 
					 | 
				
			||||||
    process::{self, Command},
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
fn main() {
 | 
					 | 
				
			||||||
    let args = cli::parser().get_matches();
 | 
					 | 
				
			||||||
    if let Some(true) = args.get_one::<bool>("help") {
 | 
					 | 
				
			||||||
        cli::parser().print_help().unwrap(); // if we can't print help we can't print an error
 | 
					 | 
				
			||||||
        process::exit(0);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let res = match args.subcommand() {
 | 
					 | 
				
			||||||
        None | Some(("run", _)) => launch_gui(),
 | 
					 | 
				
			||||||
        Some(("show", m)) => cli::show(m),
 | 
					 | 
				
			||||||
        Some(("exec", m)) => cli::exec(m),
 | 
					 | 
				
			||||||
        _ => unreachable!(),
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if let Err(e) = res {
 | 
					 | 
				
			||||||
        eprintln!("Error: {e}");
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
fn launch_gui() -> Result<(), CliError>  {
 | 
					 | 
				
			||||||
    let mut path = env::current_exe()?;
 | 
					 | 
				
			||||||
    path.pop(); // bin dir
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    // binaries are colocated in dev, but not in production
 | 
					 | 
				
			||||||
    #[cfg(not(debug_assertions))]
 | 
					 | 
				
			||||||
    path.pop(); // install dir
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    path.push("creddy.exe"); // exe in main install dir (aka gui exe)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Command::new(path).spawn()?;
 | 
					 | 
				
			||||||
    Ok(())
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,146 +0,0 @@
 | 
				
			|||||||
use std::process::Command as ChildCommand;
 | 
					 | 
				
			||||||
#[cfg(unix)]
 | 
					 | 
				
			||||||
use std::os::unix::process::CommandExt;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use clap::{
 | 
					 | 
				
			||||||
    Command,
 | 
					 | 
				
			||||||
     Arg,
 | 
					 | 
				
			||||||
     ArgMatches,
 | 
					 | 
				
			||||||
     ArgAction
 | 
					 | 
				
			||||||
 };
 | 
					 | 
				
			||||||
use tokio::{
 | 
					 | 
				
			||||||
    net::TcpStream,
 | 
					 | 
				
			||||||
    io::{AsyncReadExt, AsyncWriteExt},
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use crate::app;
 | 
					 | 
				
			||||||
use crate::config::AppConfig;
 | 
					 | 
				
			||||||
use crate::credentials::{BaseCredentials, SessionCredentials};
 | 
					 | 
				
			||||||
use crate::errors::*;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub fn parser() -> Command<'static> {
 | 
					 | 
				
			||||||
    Command::new("creddy")
 | 
					 | 
				
			||||||
        .about("A friendly AWS credentials manager")
 | 
					 | 
				
			||||||
        .subcommand(
 | 
					 | 
				
			||||||
            Command::new("run")
 | 
					 | 
				
			||||||
                .about("Launch Creddy")
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .subcommand(
 | 
					 | 
				
			||||||
            Command::new("show")
 | 
					 | 
				
			||||||
                .about("Fetch and display AWS credentials")
 | 
					 | 
				
			||||||
                .arg(
 | 
					 | 
				
			||||||
                    Arg::new("base")
 | 
					 | 
				
			||||||
                        .short('b')
 | 
					 | 
				
			||||||
                        .long("base")
 | 
					 | 
				
			||||||
                        .action(ArgAction::SetTrue)
 | 
					 | 
				
			||||||
                        .help("Use base credentials instead of session credentials")
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .subcommand(
 | 
					 | 
				
			||||||
            Command::new("exec")
 | 
					 | 
				
			||||||
                .about("Inject AWS credentials into the environment of another command")
 | 
					 | 
				
			||||||
                .trailing_var_arg(true)
 | 
					 | 
				
			||||||
                .arg(
 | 
					 | 
				
			||||||
                    Arg::new("base")
 | 
					 | 
				
			||||||
                        .short('b')
 | 
					 | 
				
			||||||
                        .long("base")
 | 
					 | 
				
			||||||
                        .action(ArgAction::SetTrue)
 | 
					 | 
				
			||||||
                        .help("Use base credentials instead of session credentials")
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                .arg(
 | 
					 | 
				
			||||||
                    Arg::new("command")
 | 
					 | 
				
			||||||
                        .multiple_values(true)
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub fn show(args: &ArgMatches) -> Result<(), CliError> {
 | 
					 | 
				
			||||||
    let base = args.get_one("base").unwrap_or(&false);
 | 
					 | 
				
			||||||
    let creds = get_credentials(*base)?;
 | 
					 | 
				
			||||||
    println!("{creds}");
 | 
					 | 
				
			||||||
    Ok(())
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub fn exec(args: &ArgMatches) -> Result<(), CliError> {
 | 
					 | 
				
			||||||
    let base = *args.get_one("base").unwrap_or(&false);
 | 
					 | 
				
			||||||
    let mut cmd_line = args.get_many("command")
 | 
					 | 
				
			||||||
        .ok_or(ExecError::NoCommand)?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let cmd_name: &String = cmd_line.next().unwrap(); // Clap guarantees that there will be at least one
 | 
					 | 
				
			||||||
    let mut cmd = ChildCommand::new(cmd_name);
 | 
					 | 
				
			||||||
    cmd.args(cmd_line);
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    if base {
 | 
					 | 
				
			||||||
        let creds: BaseCredentials = serde_json::from_str(&get_credentials(base)?)
 | 
					 | 
				
			||||||
            .map_err(|_| RequestError::InvalidJson)?;
 | 
					 | 
				
			||||||
        cmd.env("AWS_ACCESS_KEY_ID", creds.access_key_id);
 | 
					 | 
				
			||||||
        cmd.env("AWS_SECRET_ACCESS_KEY", creds.secret_access_key);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    else {
 | 
					 | 
				
			||||||
        let creds: SessionCredentials = serde_json::from_str(&get_credentials(base)?)
 | 
					 | 
				
			||||||
            .map_err(|_| RequestError::InvalidJson)?;
 | 
					 | 
				
			||||||
        cmd.env("AWS_ACCESS_KEY_ID", creds.access_key_id);
 | 
					 | 
				
			||||||
        cmd.env("AWS_SECRET_ACCESS_KEY", creds.secret_access_key);
 | 
					 | 
				
			||||||
        cmd.env("AWS_SESSION_TOKEN", creds.token);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    #[cfg(unix)]
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        let e = cmd.exec(); // never returns if successful
 | 
					 | 
				
			||||||
        Err(ExecError::ExecutionFailed(e))?;
 | 
					 | 
				
			||||||
        Ok(())
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    #[cfg(windows)]
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        let mut child = cmd.spawn()
 | 
					 | 
				
			||||||
            .map_err(|e| ExecError::ExecutionFailed(e))?;
 | 
					 | 
				
			||||||
        let status = child.wait()
 | 
					 | 
				
			||||||
            .map_err(|e| ExecError::ExecutionFailed(e))?;
 | 
					 | 
				
			||||||
        std::process::exit(status.code().unwrap_or(1));
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[tokio::main]
 | 
					 | 
				
			||||||
async fn get_credentials(base: bool) -> Result<String, RequestError> {
 | 
					 | 
				
			||||||
    let pool = app::connect_db().await?;
 | 
					 | 
				
			||||||
    let config = AppConfig::load(&pool).await?;
 | 
					 | 
				
			||||||
    let path = if base {"/creddy/base-credentials"} else {"/"};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let mut stream = TcpStream::connect((config.listen_addr, config.listen_port)).await?;
 | 
					 | 
				
			||||||
    let req = format!("GET {path} HTTP/1.0\r\n\r\n");
 | 
					 | 
				
			||||||
    stream.write_all(req.as_bytes()).await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // some day we'll have a proper HTTP parser
 | 
					 | 
				
			||||||
    let mut buf = vec![0; 8192];
 | 
					 | 
				
			||||||
    stream.read_to_end(&mut buf).await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let status = buf.split(|&c| &[c] == b" ")
 | 
					 | 
				
			||||||
        .skip(1)
 | 
					 | 
				
			||||||
        .next()
 | 
					 | 
				
			||||||
        .ok_or(RequestError::MalformedHttpResponse)?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if status != b"200" {
 | 
					 | 
				
			||||||
        let s = String::from_utf8_lossy(status).to_string();
 | 
					 | 
				
			||||||
        return Err(RequestError::Failed(s));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let break_idx = buf.windows(4)
 | 
					 | 
				
			||||||
        .position(|w| w == b"\r\n\r\n")
 | 
					 | 
				
			||||||
        .ok_or(RequestError::MalformedHttpResponse)?;
 | 
					 | 
				
			||||||
    let body = &buf[(break_idx + 4)..];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let creds_str = std::str::from_utf8(body)
 | 
					 | 
				
			||||||
        .map_err(|_| RequestError::MalformedHttpResponse)?
 | 
					 | 
				
			||||||
        .to_string();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if creds_str == "Denied!" {
 | 
					 | 
				
			||||||
        return Err(RequestError::Rejected);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    Ok(creds_str)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,12 +1,9 @@
 | 
				
			|||||||
use std::path::PathBuf;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use netstat2::{AddressFamilyFlags, ProtocolFlags, ProtocolSocketInfo};
 | 
					use netstat2::{AddressFamilyFlags, ProtocolFlags, ProtocolSocketInfo};
 | 
				
			||||||
use tauri::Manager;
 | 
					use tauri::Manager;
 | 
				
			||||||
use sysinfo::{System, SystemExt, Pid, PidExt, ProcessExt};
 | 
					use sysinfo::{System, SystemExt, Pid, PidExt, ProcessExt};
 | 
				
			||||||
use serde::{Serialize, Deserialize};
 | 
					use serde::{Serialize, Deserialize};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
    app::APP,
 | 
					 | 
				
			||||||
    errors::*,
 | 
					    errors::*,
 | 
				
			||||||
    config::AppConfig,
 | 
					    config::AppConfig,
 | 
				
			||||||
    state::AppState,
 | 
					    state::AppState,
 | 
				
			||||||
@@ -16,12 +13,12 @@ use crate::{
 | 
				
			|||||||
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
 | 
					#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
 | 
				
			||||||
pub struct Client {
 | 
					pub struct Client {
 | 
				
			||||||
    pub pid: u32,
 | 
					    pub pid: u32,
 | 
				
			||||||
    pub exe: PathBuf,
 | 
					    pub exe: String,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async fn get_associated_pids(local_port: u16) -> Result<Vec<u32>, netstat2::error::Error> {
 | 
					async fn get_associated_pids(local_port: u16) -> Result<Vec<u32>, netstat2::error::Error> {
 | 
				
			||||||
    let state = APP.get().unwrap().state::<AppState>();
 | 
					    let state = crate::APP.get().unwrap().state::<AppState>();
 | 
				
			||||||
    let AppConfig {
 | 
					    let AppConfig {
 | 
				
			||||||
        listen_addr: app_listen_addr,
 | 
					        listen_addr: app_listen_addr,
 | 
				
			||||||
        listen_port: app_listen_port,
 | 
					        listen_port: app_listen_port,
 | 
				
			||||||
@@ -63,7 +60,7 @@ pub async fn get_clients(local_port: u16) -> Result<Vec<Option<Client>>, ClientI
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        let client = Client {
 | 
					        let client = Client {
 | 
				
			||||||
            pid: p,
 | 
					            pid: p,
 | 
				
			||||||
            exe: proc.exe().to_path_buf(),
 | 
					            exe: proc.exe().to_string_lossy().into_owned(),
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
        clients.push(Some(client));
 | 
					        clients.push(Some(client));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,7 +2,6 @@ use std::net::Ipv4Addr;
 | 
				
			|||||||
use std::path::PathBuf;
 | 
					use std::path::PathBuf;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use auto_launch::AutoLaunchBuilder;
 | 
					use auto_launch::AutoLaunchBuilder;
 | 
				
			||||||
use is_terminal::IsTerminal;
 | 
					 | 
				
			||||||
use serde::{Serialize, Deserialize};
 | 
					use serde::{Serialize, Deserialize};
 | 
				
			||||||
use sqlx::SqlitePool;
 | 
					use sqlx::SqlitePool;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -91,17 +90,16 @@ pub fn set_auto_launch(is_configured: bool) -> Result<(), SetupError> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub fn get_or_create_db_path() -> Result<PathBuf, DataDirError> {
 | 
					pub fn get_or_create_db_path() -> Result<PathBuf, DataDirError> {
 | 
				
			||||||
 | 
					    // debug_assertions doesn't always mean we are running in dev
 | 
				
			||||||
 | 
					    if cfg!(debug_assertions) && std::env::var("HOME").is_ok() {
 | 
				
			||||||
 | 
					        return Ok(PathBuf::from("./creddy.db"));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let mut path = dirs::data_dir()
 | 
					    let mut path = dirs::data_dir()
 | 
				
			||||||
        .ok_or(DataDirError::NotFound)?;
 | 
					        .ok_or(DataDirError::NotFound)?;
 | 
				
			||||||
    path.push("Creddy");
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    std::fs::create_dir_all(&path)?;
 | 
					    std::fs::create_dir_all(&path)?;
 | 
				
			||||||
    if cfg!(debug_assertions) && std::io::stdout().is_terminal() {
 | 
					    path.push("creddy.db");
 | 
				
			||||||
        path.push("creddy.dev.db");
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    else {
 | 
					 | 
				
			||||||
        path.push("creddy.db");
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Ok(path)
 | 
					    Ok(path)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,321 +0,0 @@
 | 
				
			|||||||
use std::fmt::{self, Formatter};
 | 
					 | 
				
			||||||
use std::time::{SystemTime, UNIX_EPOCH};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 use aws_smithy_types::date_time::{DateTime, Format};
 | 
					 | 
				
			||||||
use argon2::{
 | 
					 | 
				
			||||||
    Argon2,
 | 
					 | 
				
			||||||
    Algorithm,
 | 
					 | 
				
			||||||
    Version,
 | 
					 | 
				
			||||||
    ParamsBuilder,
 | 
					 | 
				
			||||||
    password_hash::rand_core::{RngCore, OsRng},
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
use chacha20poly1305::{
 | 
					 | 
				
			||||||
    XChaCha20Poly1305,
 | 
					 | 
				
			||||||
    XNonce,
 | 
					 | 
				
			||||||
    aead::{
 | 
					 | 
				
			||||||
        Aead,
 | 
					 | 
				
			||||||
        AeadCore,
 | 
					 | 
				
			||||||
        KeyInit,
 | 
					 | 
				
			||||||
        Error as AeadError,
 | 
					 | 
				
			||||||
        generic_array::GenericArray,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
use serde::{
 | 
					 | 
				
			||||||
    Serialize,
 | 
					 | 
				
			||||||
    Deserialize,
 | 
					 | 
				
			||||||
    Serializer,
 | 
					 | 
				
			||||||
    Deserializer,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
use serde::de::{self, Visitor};
 | 
					 | 
				
			||||||
use sqlx::SqlitePool;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use crate::errors::*;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Clone, Debug)]
 | 
					 | 
				
			||||||
pub enum Session {
 | 
					 | 
				
			||||||
    Unlocked{
 | 
					 | 
				
			||||||
        base: BaseCredentials,
 | 
					 | 
				
			||||||
        session: SessionCredentials,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    Locked(LockedCredentials),
 | 
					 | 
				
			||||||
    Empty,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Session {
 | 
					 | 
				
			||||||
    pub async fn load(pool: &SqlitePool) -> Result<Self, SetupError> {
 | 
					 | 
				
			||||||
        let res = sqlx::query!("SELECT * FROM credentials ORDER BY created_at desc")
 | 
					 | 
				
			||||||
            .fetch_optional(pool)
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
        let row = match res {
 | 
					 | 
				
			||||||
            Some(r) => r,
 | 
					 | 
				
			||||||
            None => {return Ok(Session::Empty);}
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let salt: [u8; 32] = row.salt
 | 
					 | 
				
			||||||
            .try_into()
 | 
					 | 
				
			||||||
            .map_err(|_e| SetupError::InvalidRecord)?;
 | 
					 | 
				
			||||||
        let nonce = XNonce::from_exact_iter(row.nonce.into_iter())
 | 
					 | 
				
			||||||
            .ok_or(SetupError::InvalidRecord)?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let creds = LockedCredentials {
 | 
					 | 
				
			||||||
            access_key_id: row.access_key_id,
 | 
					 | 
				
			||||||
            secret_key_enc: row.secret_key_enc,
 | 
					 | 
				
			||||||
            salt,
 | 
					 | 
				
			||||||
            nonce,
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
        Ok(Session::Locked(creds))
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub async fn renew_if_expired(&mut self) -> Result<bool, GetSessionError> {
 | 
					 | 
				
			||||||
        match self {
 | 
					 | 
				
			||||||
            Session::Unlocked{ref base, ref mut session} => {
 | 
					 | 
				
			||||||
                if !session.is_expired() {
 | 
					 | 
				
			||||||
                    return Ok(false);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                *session = SessionCredentials::from_base(base).await?;
 | 
					 | 
				
			||||||
                Ok(true)
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            Session::Locked(_) => Err(GetSessionError::CredentialsLocked),
 | 
					 | 
				
			||||||
            Session::Empty => Err(GetSessionError::CredentialsEmpty),
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Clone, Debug)]
 | 
					 | 
				
			||||||
pub struct LockedCredentials {
 | 
					 | 
				
			||||||
    pub access_key_id: String,
 | 
					 | 
				
			||||||
    pub secret_key_enc: Vec<u8>,
 | 
					 | 
				
			||||||
    pub salt: [u8; 32],
 | 
					 | 
				
			||||||
    pub nonce: XNonce,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl LockedCredentials {
 | 
					 | 
				
			||||||
    pub async fn save(&self, pool: &SqlitePool) -> Result<(), sqlx::Error> {
 | 
					 | 
				
			||||||
        sqlx::query(
 | 
					 | 
				
			||||||
            "INSERT INTO credentials (access_key_id, secret_key_enc, salt, nonce, created_at)
 | 
					 | 
				
			||||||
            VALUES (?, ?, ?, ?, strftime('%s'))"
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
            .bind(&self.access_key_id)
 | 
					 | 
				
			||||||
            .bind(&self.secret_key_enc)
 | 
					 | 
				
			||||||
            .bind(&self.salt[..])
 | 
					 | 
				
			||||||
            .bind(&self.nonce[..])
 | 
					 | 
				
			||||||
            .execute(pool)
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Ok(())
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn decrypt(&self, passphrase: &str) -> Result<BaseCredentials, UnlockError> {
 | 
					 | 
				
			||||||
        let crypto = Crypto::new(passphrase, &self.salt)
 | 
					 | 
				
			||||||
            .map_err(|e| CryptoError::Argon2(e))?;
 | 
					 | 
				
			||||||
        let decrypted = crypto.decrypt(&self.nonce, &self.secret_key_enc)
 | 
					 | 
				
			||||||
            .map_err(|e| CryptoError::Aead(e))?;
 | 
					 | 
				
			||||||
        let secret_access_key = String::from_utf8(decrypted)
 | 
					 | 
				
			||||||
            .map_err(|_| UnlockError::InvalidUtf8)?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let creds = BaseCredentials {
 | 
					 | 
				
			||||||
            access_key_id: self.access_key_id.clone(),
 | 
					 | 
				
			||||||
            secret_access_key,
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
        Ok(creds)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
 | 
					 | 
				
			||||||
#[serde(rename_all = "PascalCase")]
 | 
					 | 
				
			||||||
pub struct BaseCredentials {
 | 
					 | 
				
			||||||
    pub access_key_id: String,
 | 
					 | 
				
			||||||
    pub secret_access_key: String,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl BaseCredentials {
 | 
					 | 
				
			||||||
    pub fn encrypt(&self, passphrase: &str) -> Result<LockedCredentials, CryptoError> {
 | 
					 | 
				
			||||||
        let salt = Crypto::salt();
 | 
					 | 
				
			||||||
        let crypto = Crypto::new(passphrase, &salt)?;
 | 
					 | 
				
			||||||
        let (nonce, secret_key_enc) = crypto.encrypt(self.secret_access_key.as_bytes())?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let locked = LockedCredentials {
 | 
					 | 
				
			||||||
            access_key_id: self.access_key_id.clone(),
 | 
					 | 
				
			||||||
            secret_key_enc,
 | 
					 | 
				
			||||||
            salt,
 | 
					 | 
				
			||||||
            nonce,
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
        Ok(locked)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
 | 
					 | 
				
			||||||
#[serde(rename_all = "PascalCase")]
 | 
					 | 
				
			||||||
pub struct SessionCredentials {
 | 
					 | 
				
			||||||
    pub access_key_id: String,
 | 
					 | 
				
			||||||
    pub secret_access_key: String,
 | 
					 | 
				
			||||||
    pub token: String,
 | 
					 | 
				
			||||||
    #[serde(serialize_with = "serialize_expiration")]
 | 
					 | 
				
			||||||
    #[serde(deserialize_with = "deserialize_expiration")]
 | 
					 | 
				
			||||||
    pub expiration: DateTime,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl SessionCredentials {
 | 
					 | 
				
			||||||
    pub async fn from_base(base: &BaseCredentials) -> Result<Self, GetSessionError> {
 | 
					 | 
				
			||||||
        let req_creds = aws_sdk_sts::Credentials::new(
 | 
					 | 
				
			||||||
            &base.access_key_id,
 | 
					 | 
				
			||||||
            &base.secret_access_key,
 | 
					 | 
				
			||||||
            None, // token
 | 
					 | 
				
			||||||
            None, //expiration
 | 
					 | 
				
			||||||
            "Creddy", // "provider name" apparently
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        let config = aws_config::from_env()
 | 
					 | 
				
			||||||
            .credentials_provider(req_creds)
 | 
					 | 
				
			||||||
            .load()
 | 
					 | 
				
			||||||
            .await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let client = aws_sdk_sts::Client::new(&config);
 | 
					 | 
				
			||||||
        let resp = client.get_session_token()
 | 
					 | 
				
			||||||
            .duration_seconds(43_200)
 | 
					 | 
				
			||||||
            .send()
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let aws_session = resp.credentials().ok_or(GetSessionError::EmptyResponse)?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let access_key_id = aws_session.access_key_id()
 | 
					 | 
				
			||||||
            .ok_or(GetSessionError::EmptyResponse)?
 | 
					 | 
				
			||||||
            .to_string();
 | 
					 | 
				
			||||||
        let secret_access_key = aws_session.secret_access_key()
 | 
					 | 
				
			||||||
            .ok_or(GetSessionError::EmptyResponse)?
 | 
					 | 
				
			||||||
            .to_string();
 | 
					 | 
				
			||||||
        let token = aws_session.session_token()
 | 
					 | 
				
			||||||
            .ok_or(GetSessionError::EmptyResponse)?
 | 
					 | 
				
			||||||
            .to_string();
 | 
					 | 
				
			||||||
        let expiration = aws_session.expiration()
 | 
					 | 
				
			||||||
            .ok_or(GetSessionError::EmptyResponse)?
 | 
					 | 
				
			||||||
            .clone();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let session_creds = SessionCredentials {
 | 
					 | 
				
			||||||
            access_key_id,
 | 
					 | 
				
			||||||
            secret_access_key,
 | 
					 | 
				
			||||||
            token,
 | 
					 | 
				
			||||||
            expiration,
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        #[cfg(debug_assertions)]
 | 
					 | 
				
			||||||
        println!("Got new session:\n{}", serde_json::to_string(&session_creds).unwrap());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Ok(session_creds)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn is_expired(&self) -> bool {
 | 
					 | 
				
			||||||
        let current_ts = SystemTime::now()
 | 
					 | 
				
			||||||
            .duration_since(UNIX_EPOCH)
 | 
					 | 
				
			||||||
            .unwrap() // doesn't panic because UNIX_EPOCH won't be later than now()
 | 
					 | 
				
			||||||
            .as_secs();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let expire_ts = self.expiration.secs();
 | 
					 | 
				
			||||||
        let remaining = expire_ts - (current_ts as i64);
 | 
					 | 
				
			||||||
        remaining < 60
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
fn serialize_expiration<S>(exp: &DateTime, serializer: S) -> Result<S::Ok, S::Error>
 | 
					 | 
				
			||||||
where S: Serializer
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    // this only fails if the d/t is out of range, which it can't be for this format
 | 
					 | 
				
			||||||
    let time_str = exp.fmt(Format::DateTime).unwrap();
 | 
					 | 
				
			||||||
    serializer.serialize_str(&time_str)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
struct DateTimeVisitor;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl<'de> Visitor<'de> for DateTimeVisitor {
 | 
					 | 
				
			||||||
    type Value = DateTime;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
 | 
					 | 
				
			||||||
        write!(formatter, "an RFC 3339 UTC string, e.g. \"2014-01-05T10:17:34Z\"")
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    fn visit_str<E: de::Error>(self, v: &str) -> Result<DateTime, E> {
 | 
					 | 
				
			||||||
        DateTime::from_str(v, Format::DateTime)
 | 
					 | 
				
			||||||
            .map_err(|_| E::custom(format!("Invalid date/time: {v}")))
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
fn deserialize_expiration<'de, D>(deserializer: D) -> Result<DateTime, D::Error>
 | 
					 | 
				
			||||||
where D: Deserializer<'de>
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    deserializer.deserialize_str(DateTimeVisitor)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
struct Crypto {
 | 
					 | 
				
			||||||
    cipher: XChaCha20Poly1305,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Crypto {
 | 
					 | 
				
			||||||
    /// Argon2 params rationale:
 | 
					 | 
				
			||||||
    ///
 | 
					 | 
				
			||||||
    /// m_cost is measured in KiB, so 128 * 1024 gives us 128MiB.
 | 
					 | 
				
			||||||
    /// This should roughly double the memory usage of the application
 | 
					 | 
				
			||||||
    /// while deriving the key.
 | 
					 | 
				
			||||||
    ///
 | 
					 | 
				
			||||||
    /// p_cost is irrelevant since (at present) there isn't any parallelism
 | 
					 | 
				
			||||||
    /// implemented, so we leave it at 1.
 | 
					 | 
				
			||||||
    ///
 | 
					 | 
				
			||||||
    /// With the above m_cost, t_cost = 8 results in about 800ms to derive
 | 
					 | 
				
			||||||
    /// a key on my (somewhat older) CPU. This is probably overkill, but
 | 
					 | 
				
			||||||
    /// given that it should only have to happen ~once a day for most 
 | 
					 | 
				
			||||||
    /// usage, it should be acceptable.
 | 
					 | 
				
			||||||
    #[cfg(not(debug_assertions))]
 | 
					 | 
				
			||||||
    const MEM_COST: u32 = 128 * 1024;
 | 
					 | 
				
			||||||
    #[cfg(not(debug_assertions))]
 | 
					 | 
				
			||||||
    const TIME_COST: u32 = 8;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /// But since this takes a million years without optimizations,
 | 
					 | 
				
			||||||
    /// we turn it way down in debug builds.
 | 
					 | 
				
			||||||
    #[cfg(debug_assertions)]
 | 
					 | 
				
			||||||
    const MEM_COST: u32 = 48 * 1024;
 | 
					 | 
				
			||||||
    #[cfg(debug_assertions)]
 | 
					 | 
				
			||||||
    const TIME_COST: u32 = 1;
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    fn new(passphrase: &str, salt: &[u8]) -> argon2::Result<Crypto> {
 | 
					 | 
				
			||||||
        let params = ParamsBuilder::new()
 | 
					 | 
				
			||||||
            .m_cost(Self::MEM_COST)
 | 
					 | 
				
			||||||
            .p_cost(1)
 | 
					 | 
				
			||||||
            .t_cost(Self::TIME_COST)
 | 
					 | 
				
			||||||
            .build()
 | 
					 | 
				
			||||||
            .unwrap(); // only errors if the given params are invalid
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let hasher = Argon2::new(
 | 
					 | 
				
			||||||
            Algorithm::Argon2id,
 | 
					 | 
				
			||||||
            Version::V0x13,
 | 
					 | 
				
			||||||
            params,
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let mut key = [0; 32];
 | 
					 | 
				
			||||||
        hasher.hash_password_into(passphrase.as_bytes(), &salt, &mut key)?;
 | 
					 | 
				
			||||||
        let cipher = XChaCha20Poly1305::new(GenericArray::from_slice(&key));
 | 
					 | 
				
			||||||
        Ok(Crypto { cipher })
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    fn salt() -> [u8; 32] {
 | 
					 | 
				
			||||||
        let mut salt = [0; 32];
 | 
					 | 
				
			||||||
        OsRng.fill_bytes(&mut salt);
 | 
					 | 
				
			||||||
        salt
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    fn encrypt(&self, data: &[u8]) -> Result<(XNonce, Vec<u8>), AeadError> {
 | 
					 | 
				
			||||||
        let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
 | 
					 | 
				
			||||||
        let ciphertext = self.cipher.encrypt(&nonce, data)?;
 | 
					 | 
				
			||||||
        Ok((nonce, ciphertext))
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    fn decrypt(&self, nonce: &XNonce, data: &[u8]) -> Result<Vec<u8>, AeadError> {
 | 
					 | 
				
			||||||
        self.cipher.decrypt(nonce, data)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -116,13 +116,13 @@ pub enum SendResponseError {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// errors encountered while handling an HTTP request
 | 
					// errors encountered while handling an HTTP request
 | 
				
			||||||
#[derive(Debug, ThisError, AsRefStr)]
 | 
					#[derive(Debug, ThisError, AsRefStr)]
 | 
				
			||||||
pub enum HandlerError {
 | 
					pub enum RequestError {
 | 
				
			||||||
    #[error("Error writing to stream: {0}")]
 | 
					    #[error("Error writing to stream: {0}")]
 | 
				
			||||||
    StreamIOError(#[from] std::io::Error),
 | 
					    StreamIOError(#[from] std::io::Error),
 | 
				
			||||||
    // #[error("Received invalid UTF-8 in request")]
 | 
					    // #[error("Received invalid UTF-8 in request")]
 | 
				
			||||||
    // InvalidUtf8,
 | 
					    // InvalidUtf8,
 | 
				
			||||||
    #[error("HTTP request malformed")]
 | 
					    #[error("HTTP request malformed")]
 | 
				
			||||||
    BadRequest(Vec<u8>),
 | 
					    BadRequest,
 | 
				
			||||||
    #[error("HTTP request too large")]
 | 
					    #[error("HTTP request too large")]
 | 
				
			||||||
    RequestTooLarge,
 | 
					    RequestTooLarge,
 | 
				
			||||||
    #[error("Error accessing credentials: {0}")]
 | 
					    #[error("Error accessing credentials: {0}")]
 | 
				
			||||||
@@ -164,8 +164,8 @@ pub enum UnlockError {
 | 
				
			|||||||
    NotLocked,
 | 
					    NotLocked,
 | 
				
			||||||
    #[error("No saved credentials were found")]
 | 
					    #[error("No saved credentials were found")]
 | 
				
			||||||
    NoCredentials,
 | 
					    NoCredentials,
 | 
				
			||||||
    #[error(transparent)]
 | 
					    #[error("Invalid passphrase")]
 | 
				
			||||||
    Crypto(#[from] CryptoError),
 | 
					    BadPassphrase,
 | 
				
			||||||
    #[error("Data was found to be corrupt after decryption")]
 | 
					    #[error("Data was found to be corrupt after decryption")]
 | 
				
			||||||
    InvalidUtf8, // Somehow we got invalid utf-8 even though decryption succeeded
 | 
					    InvalidUtf8, // Somehow we got invalid utf-8 even though decryption succeeded
 | 
				
			||||||
    #[error("Database error: {0}")]
 | 
					    #[error("Database error: {0}")]
 | 
				
			||||||
@@ -175,15 +175,6 @@ pub enum UnlockError {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Debug, ThisError, AsRefStr)]
 | 
					 | 
				
			||||||
pub enum CryptoError {
 | 
					 | 
				
			||||||
    #[error(transparent)]
 | 
					 | 
				
			||||||
    Argon2(#[from] argon2::Error),
 | 
					 | 
				
			||||||
    #[error("Invalid passphrase")] // I think this is the only way decryption fails
 | 
					 | 
				
			||||||
    Aead(#[from] chacha20poly1305::aead::Error),
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Errors encountered while trying to figure out who's on the other end of a request
 | 
					// Errors encountered while trying to figure out who's on the other end of a request
 | 
				
			||||||
#[derive(Debug, ThisError, AsRefStr)]
 | 
					#[derive(Debug, ThisError, AsRefStr)]
 | 
				
			||||||
pub enum ClientInfoError {
 | 
					pub enum ClientInfoError {
 | 
				
			||||||
@@ -194,45 +185,6 @@ pub enum ClientInfoError {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Errors encountered while requesting credentials via CLI (creddy show, creddy exec)
 | 
					 | 
				
			||||||
#[derive(Debug, ThisError, AsRefStr)]
 | 
					 | 
				
			||||||
pub enum RequestError {
 | 
					 | 
				
			||||||
    #[error("Credentials request failed: HTTP {0}")]
 | 
					 | 
				
			||||||
    Failed(String),
 | 
					 | 
				
			||||||
    #[error("Credentials request was rejected")]
 | 
					 | 
				
			||||||
    Rejected,
 | 
					 | 
				
			||||||
    #[error("Couldn't interpret the server's response")]
 | 
					 | 
				
			||||||
    MalformedHttpResponse,
 | 
					 | 
				
			||||||
    #[error("The server did not respond with valid JSON")]
 | 
					 | 
				
			||||||
    InvalidJson,
 | 
					 | 
				
			||||||
    #[error("Error reading/writing stream: {0}")]
 | 
					 | 
				
			||||||
    StreamIOError(#[from] std::io::Error),
 | 
					 | 
				
			||||||
    #[error("Error loading configuration data: {0}")]
 | 
					 | 
				
			||||||
    Setup(#[from] SetupError),
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Errors encountered while running a subprocess (via creddy exec)
 | 
					 | 
				
			||||||
#[derive(Debug, ThisError, AsRefStr)]
 | 
					 | 
				
			||||||
pub enum ExecError {
 | 
					 | 
				
			||||||
    #[error("Please specify a command")]
 | 
					 | 
				
			||||||
    NoCommand,
 | 
					 | 
				
			||||||
    #[error("Failed to execute command: {0}")]
 | 
					 | 
				
			||||||
    ExecutionFailed(#[from] std::io::Error)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Debug, ThisError, AsRefStr)]
 | 
					 | 
				
			||||||
pub enum CliError {
 | 
					 | 
				
			||||||
    #[error(transparent)]
 | 
					 | 
				
			||||||
    Request(#[from] RequestError),
 | 
					 | 
				
			||||||
    #[error(transparent)]
 | 
					 | 
				
			||||||
    Exec(#[from] ExecError),
 | 
					 | 
				
			||||||
    #[error(transparent)]
 | 
					 | 
				
			||||||
    Io(#[from] std::io::Error),
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// =========================
 | 
					// =========================
 | 
				
			||||||
// Serialize implementations
 | 
					// Serialize implementations
 | 
				
			||||||
// =========================
 | 
					// =========================
 | 
				
			||||||
@@ -258,15 +210,15 @@ impl_serialize_basic!(GetCredentialsError);
 | 
				
			|||||||
impl_serialize_basic!(ClientInfoError);
 | 
					impl_serialize_basic!(ClientInfoError);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl Serialize for HandlerError {
 | 
					impl Serialize for RequestError {
 | 
				
			||||||
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
 | 
					    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
 | 
				
			||||||
        let mut map = serializer.serialize_map(None)?;
 | 
					        let mut map = serializer.serialize_map(None)?;
 | 
				
			||||||
        map.serialize_entry("code", self.as_ref())?;
 | 
					        map.serialize_entry("code", self.as_ref())?;
 | 
				
			||||||
        map.serialize_entry("msg", &format!("{self}"))?;
 | 
					        map.serialize_entry("msg", &format!("{self}"))?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        match self {
 | 
					        match self {
 | 
				
			||||||
            HandlerError::NoCredentials(src) => map.serialize_entry("source", &src)?,
 | 
					            RequestError::NoCredentials(src) => map.serialize_entry("source", &src)?,
 | 
				
			||||||
            HandlerError::ClientInfo(src) => map.serialize_entry("source", &src)?,
 | 
					            RequestError::ClientInfo(src) => map.serialize_entry("source", &src)?,
 | 
				
			||||||
            _ => serialize_upstream_err(self, &mut map)?,
 | 
					            _ => serialize_upstream_err(self, &mut map)?,
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,18 +1,16 @@
 | 
				
			|||||||
use serde::{Serialize, Deserialize};
 | 
					use serde::{Serialize, Deserialize};
 | 
				
			||||||
use tauri::State;
 | 
					use tauri::State;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::config::AppConfig;
 | 
					 | 
				
			||||||
use crate::credentials::{Session,BaseCredentials};
 | 
					 | 
				
			||||||
use crate::errors::*;
 | 
					use crate::errors::*;
 | 
				
			||||||
 | 
					use crate::config::AppConfig;
 | 
				
			||||||
use crate::clientinfo::Client;
 | 
					use crate::clientinfo::Client;
 | 
				
			||||||
use crate::state::AppState;
 | 
					use crate::state::{AppState, Session, BaseCredentials};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
 | 
					#[derive(Clone, Debug, Serialize, Deserialize)]
 | 
				
			||||||
pub struct Request {
 | 
					pub struct Request {
 | 
				
			||||||
    pub id: u64,
 | 
					    pub id: u64,
 | 
				
			||||||
    pub clients: Vec<Option<Client>>,
 | 
					    pub clients: Vec<Option<Client>>,
 | 
				
			||||||
    pub base: bool,
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -60,7 +58,7 @@ pub async fn save_credentials(
 | 
				
			|||||||
    passphrase: String,
 | 
					    passphrase: String,
 | 
				
			||||||
    app_state: State<'_, AppState>
 | 
					    app_state: State<'_, AppState>
 | 
				
			||||||
) -> Result<(), UnlockError> {
 | 
					) -> Result<(), UnlockError> {
 | 
				
			||||||
    app_state.new_creds(credentials, &passphrase).await
 | 
					    app_state.save_creds(credentials, &passphrase).await
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,10 +0,0 @@
 | 
				
			|||||||
pub mod app;
 | 
					 | 
				
			||||||
pub mod cli;
 | 
					 | 
				
			||||||
mod config;
 | 
					 | 
				
			||||||
mod credentials;
 | 
					 | 
				
			||||||
pub mod errors;
 | 
					 | 
				
			||||||
mod clientinfo;
 | 
					 | 
				
			||||||
mod ipc;
 | 
					 | 
				
			||||||
mod state;
 | 
					 | 
				
			||||||
mod server;
 | 
					 | 
				
			||||||
mod tray;
 | 
					 | 
				
			||||||
@@ -2,26 +2,98 @@
 | 
				
			|||||||
    all(not(debug_assertions), target_os = "windows"),
 | 
					    all(not(debug_assertions), target_os = "windows"),
 | 
				
			||||||
    windows_subsystem = "windows"
 | 
					    windows_subsystem = "windows"
 | 
				
			||||||
)]
 | 
					)]
 | 
				
			||||||
 | 
					use std::error::Error;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use creddy::{
 | 
					use once_cell::sync::OnceCell;
 | 
				
			||||||
    app,
 | 
					use sqlx::{
 | 
				
			||||||
    cli,
 | 
					    SqlitePool,
 | 
				
			||||||
    errors::ErrorPopup,
 | 
					    sqlite::SqlitePoolOptions,
 | 
				
			||||||
 | 
					    sqlite::SqliteConnectOptions,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					use tauri::{
 | 
				
			||||||
 | 
					    App,
 | 
				
			||||||
 | 
					    AppHandle,
 | 
				
			||||||
 | 
					    Manager,
 | 
				
			||||||
 | 
					    async_runtime as rt,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					mod config;
 | 
				
			||||||
 | 
					mod errors;
 | 
				
			||||||
 | 
					mod clientinfo;
 | 
				
			||||||
 | 
					mod ipc;
 | 
				
			||||||
 | 
					mod state;
 | 
				
			||||||
 | 
					mod server;
 | 
				
			||||||
 | 
					mod tray;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use config::AppConfig;
 | 
				
			||||||
 | 
					use server::Server;
 | 
				
			||||||
 | 
					use errors::*;
 | 
				
			||||||
 | 
					use state::AppState;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub static APP: OnceCell<AppHandle> = OnceCell::new();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn setup(app: &mut App) -> Result<(), Box<dyn Error>> {
 | 
				
			||||||
 | 
					    APP.set(app.handle()).unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let conn_opts = SqliteConnectOptions::new()
 | 
				
			||||||
 | 
					        .filename(config::get_or_create_db_path()?)
 | 
				
			||||||
 | 
					        .create_if_missing(true);
 | 
				
			||||||
 | 
					    let pool_opts = SqlitePoolOptions::new();
 | 
				
			||||||
 | 
					    let pool: SqlitePool = pool_opts.connect_with(conn_opts).await?;
 | 
				
			||||||
 | 
					    sqlx::migrate!().run(&pool).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let conf = AppConfig::load(&pool).await?;
 | 
				
			||||||
 | 
					    let session = AppState::load_creds(&pool).await?;
 | 
				
			||||||
 | 
					    let srv = Server::new(conf.listen_addr, conf.listen_port, app.handle()).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    config::set_auto_launch(conf.start_on_login)?;
 | 
				
			||||||
 | 
					    if !conf.start_minimized {
 | 
				
			||||||
 | 
					        app.get_window("main")
 | 
				
			||||||
 | 
					            .ok_or(RequestError::NoMainWindow)?
 | 
				
			||||||
 | 
					            .show()?;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let state = AppState::new(conf, session, srv, pool);
 | 
				
			||||||
 | 
					    app.manage(state);
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn run() -> tauri::Result<()> {
 | 
				
			||||||
 | 
					    tauri::Builder::default()
 | 
				
			||||||
 | 
					        .plugin(tauri_plugin_single_instance::init(|app, _argv, _cwd| {
 | 
				
			||||||
 | 
					            app.get_window("main")
 | 
				
			||||||
 | 
					                .map(|w| w.show().error_popup("Failed to show main window"));
 | 
				
			||||||
 | 
					        }))
 | 
				
			||||||
 | 
					        .system_tray(tray::create())
 | 
				
			||||||
 | 
					        .on_system_tray_event(tray::handle_event)
 | 
				
			||||||
 | 
					        .invoke_handler(tauri::generate_handler![
 | 
				
			||||||
 | 
					            ipc::unlock,
 | 
				
			||||||
 | 
					            ipc::respond,
 | 
				
			||||||
 | 
					            ipc::get_session_status,
 | 
				
			||||||
 | 
					            ipc::save_credentials,
 | 
				
			||||||
 | 
					            ipc::get_config,
 | 
				
			||||||
 | 
					            ipc::save_config,
 | 
				
			||||||
 | 
					        ])
 | 
				
			||||||
 | 
					        .setup(|app| rt::block_on(setup(app)))
 | 
				
			||||||
 | 
					        .build(tauri::generate_context!())?
 | 
				
			||||||
 | 
					        .run(|app, run_event| match run_event {
 | 
				
			||||||
 | 
					            tauri::RunEvent::WindowEvent { label, event, .. } => match event {
 | 
				
			||||||
 | 
					                tauri::WindowEvent::CloseRequested { api, .. } => {
 | 
				
			||||||
 | 
					                    let _ = app.get_window(&label).map(|w| w.hide());
 | 
				
			||||||
 | 
					                    api.prevent_close();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                _ => ()
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            _ => ()
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fn main() {
 | 
					fn main() {
 | 
				
			||||||
    let res = match cli::parser().get_matches().subcommand() {
 | 
					    run().error_popup("Creddy failed to start");
 | 
				
			||||||
        None | Some(("run", _)) => {
 | 
					 | 
				
			||||||
            app::run().error_popup("Creddy failed to start");
 | 
					 | 
				
			||||||
            Ok(())
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        Some(("show", m)) => cli::show(m),
 | 
					 | 
				
			||||||
        Some(("exec", m)) => cli::exec(m),
 | 
					 | 
				
			||||||
        _ => unreachable!(),
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if let Err(e) = res {
 | 
					 | 
				
			||||||
        eprintln!("Error: {e}");
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -51,37 +51,25 @@ impl Handler {
 | 
				
			|||||||
        state.unregister_request(self.request_id).await;
 | 
					        state.unregister_request(self.request_id).await;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async fn try_handle(&mut self) -> Result<(), HandlerError> {
 | 
					    async fn try_handle(&mut self) -> Result<(), RequestError> {
 | 
				
			||||||
        let req_path = self.recv_request().await?;
 | 
					        let _ = self.recv_request().await?;
 | 
				
			||||||
        let clients = self.get_clients().await?;
 | 
					        let clients = self.get_clients().await?;
 | 
				
			||||||
        if self.includes_banned(&clients).await {
 | 
					        if self.includes_banned(&clients).await {
 | 
				
			||||||
            self.stream.write(b"HTTP/1.0 403 Access Denied\r\n\r\n").await?;
 | 
					            self.stream.write(b"HTTP/1.0 403 Access Denied\r\n\r\n").await?;
 | 
				
			||||||
            return Ok(())
 | 
					            return Ok(())
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        let base = req_path == b"/creddy/base-credentials";
 | 
					
 | 
				
			||||||
        
 | 
					        let req = Request {id: self.request_id, clients};
 | 
				
			||||||
        let req = Request {id: self.request_id, clients, base};
 | 
					 | 
				
			||||||
        self.app.emit_all("credentials-request", &req)?;
 | 
					        self.app.emit_all("credentials-request", &req)?;
 | 
				
			||||||
        let starting_visibility = self.show_window()?;
 | 
					        let starting_visibility = self.show_window()?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        match self.wait_for_response().await? {
 | 
					        match self.wait_for_response().await? {
 | 
				
			||||||
            Approval::Approved => {
 | 
					            Approval::Approved => self.send_credentials().await?,
 | 
				
			||||||
                let state = self.app.state::<AppState>();
 | 
					 | 
				
			||||||
                let creds = if base {
 | 
					 | 
				
			||||||
                    state.serialize_base_creds().await?
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                else {
 | 
					 | 
				
			||||||
                    state.serialize_session_creds().await?
 | 
					 | 
				
			||||||
                };
 | 
					 | 
				
			||||||
                self.send_body(creds.as_bytes()).await?;
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            Approval::Denied => {
 | 
					            Approval::Denied => {
 | 
				
			||||||
                let state = self.app.state::<AppState>();
 | 
					                let state = self.app.state::<AppState>();
 | 
				
			||||||
                for client in req.clients {
 | 
					                for client in req.clients {
 | 
				
			||||||
                    state.add_ban(client).await;
 | 
					                    state.add_ban(client).await;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                self.send_body(b"Denied!").await?;
 | 
					 | 
				
			||||||
                self.stream.shutdown().await?;
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -95,36 +83,35 @@ impl Handler {
 | 
				
			|||||||
        sleep(delay).await;
 | 
					        sleep(delay).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if !starting_visibility && state.req_count().await == 0 {
 | 
					        if !starting_visibility && state.req_count().await == 0 {
 | 
				
			||||||
            let window = self.app.get_window("main").ok_or(HandlerError::NoMainWindow)?;
 | 
					            let window = self.app.get_window("main").ok_or(RequestError::NoMainWindow)?;
 | 
				
			||||||
            window.hide()?;
 | 
					            window.hide()?;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Ok(())
 | 
					        Ok(())
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async fn recv_request(&mut self) -> Result<Vec<u8>, HandlerError> {
 | 
					    async fn recv_request(&mut self) -> Result<Vec<u8>, RequestError> {
 | 
				
			||||||
        let mut buf = vec![0; 8192]; // it's what tokio's BufReader uses
 | 
					        let mut buf = vec![0; 8192]; // it's what tokio's BufReader uses
 | 
				
			||||||
        let mut n = 0;
 | 
					        let mut n = 0;
 | 
				
			||||||
        loop {
 | 
					        loop {
 | 
				
			||||||
            n += self.stream.read(&mut buf[n..]).await?;
 | 
					            n += self.stream.read(&mut buf[n..]).await?;
 | 
				
			||||||
            if n >= 4 && &buf[(n - 4)..n] == b"\r\n\r\n" {break;}
 | 
					            if n >= 4 && &buf[(n - 4)..n] == b"\r\n\r\n" {break;}
 | 
				
			||||||
            if n == buf.len() {return Err(HandlerError::RequestTooLarge);}
 | 
					            if n == buf.len() {return Err(RequestError::RequestTooLarge);}
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if cfg!(debug_assertions) {
 | 
				
			||||||
 | 
					            println!("{}", std::str::from_utf8(&buf).unwrap());
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let path = buf.split(|&c| &[c] == b" ")
 | 
					        let path = buf.split(|&c| &[c] == b" ")
 | 
				
			||||||
            .skip(1)
 | 
					            .skip(1)
 | 
				
			||||||
            .next()
 | 
					            .next()
 | 
				
			||||||
            .ok_or(HandlerError::BadRequest(buf.clone()))?;
 | 
					            .ok_or(RequestError::BadRequest(buf))?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        #[cfg(debug_assertions)] {
 | 
					        Ok(buf)
 | 
				
			||||||
            println!("Path: {}", std::str::from_utf8(&path).unwrap());
 | 
					 | 
				
			||||||
            println!("{}", std::str::from_utf8(&buf).unwrap());
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Ok(path.into())
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async fn get_clients(&self) -> Result<Vec<Option<Client>>, HandlerError> {
 | 
					    async fn get_clients(&self) -> Result<Vec<Option<Client>>, RequestError> {
 | 
				
			||||||
        let peer_addr = match self.stream.peer_addr()? {
 | 
					        let peer_addr = match self.stream.peer_addr()? {
 | 
				
			||||||
            SocketAddr::V4(addr) => addr,
 | 
					            SocketAddr::V4(addr) => addr,
 | 
				
			||||||
            _ => unreachable!(), // we only listen on IPv4
 | 
					            _ => unreachable!(), // we only listen on IPv4
 | 
				
			||||||
@@ -143,8 +130,8 @@ impl Handler {
 | 
				
			|||||||
        false
 | 
					        false
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    fn show_window(&self) -> Result<bool, HandlerError> {
 | 
					    fn show_window(&self) -> Result<bool, RequestError> {
 | 
				
			||||||
        let window = self.app.get_window("main").ok_or(HandlerError::NoMainWindow)?;
 | 
					        let window = self.app.get_window("main").ok_or(RequestError::NoMainWindow)?;
 | 
				
			||||||
        let starting_visibility = window.is_visible()?;
 | 
					        let starting_visibility = window.is_visible()?;
 | 
				
			||||||
        if !starting_visibility {
 | 
					        if !starting_visibility {
 | 
				
			||||||
            window.unminimize()?;
 | 
					            window.unminimize()?;
 | 
				
			||||||
@@ -154,7 +141,7 @@ impl Handler {
 | 
				
			|||||||
        Ok(starting_visibility)
 | 
					        Ok(starting_visibility)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async fn wait_for_response(&mut self) -> Result<Approval, HandlerError> {
 | 
					    async fn wait_for_response(&mut self) -> Result<Approval, RequestError> {
 | 
				
			||||||
        self.stream.write(b"HTTP/1.0 200 OK\r\n").await?;
 | 
					        self.stream.write(b"HTTP/1.0 200 OK\r\n").await?;
 | 
				
			||||||
        self.stream.write(b"Content-Type: application/json\r\n").await?;
 | 
					        self.stream.write(b"Content-Type: application/json\r\n").await?;
 | 
				
			||||||
        self.stream.write(b"X-Creddy-delaying-tactic: ").await?;
 | 
					        self.stream.write(b"X-Creddy-delaying-tactic: ").await?;
 | 
				
			||||||
@@ -177,12 +164,15 @@ impl Handler {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async fn send_body(&mut self, body: &[u8]) -> Result<(), HandlerError> {
 | 
					    async fn send_credentials(&mut self) -> Result<(), RequestError> {
 | 
				
			||||||
 | 
					        let state = self.app.state::<AppState>();
 | 
				
			||||||
 | 
					        let creds = state.serialize_session_creds().await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.stream.write(b"\r\nContent-Length: ").await?;
 | 
					        self.stream.write(b"\r\nContent-Length: ").await?;
 | 
				
			||||||
        self.stream.write(body.len().to_string().as_bytes()).await?;
 | 
					        self.stream.write(creds.as_bytes().len().to_string().as_bytes()).await?;
 | 
				
			||||||
 | 
					        self.stream.write(b"\r\n\r\n").await?;
 | 
				
			||||||
 | 
					        self.stream.write(creds.as_bytes()).await?;
 | 
				
			||||||
        self.stream.write(b"\r\n\r\n").await?;
 | 
					        self.stream.write(b"\r\n\r\n").await?;
 | 
				
			||||||
        self.stream.write(body).await?;
 | 
					 | 
				
			||||||
        self.stream.shutdown().await?;
 | 
					 | 
				
			||||||
        Ok(())
 | 
					        Ok(())
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,28 +1,90 @@
 | 
				
			|||||||
use std::collections::{HashMap, HashSet};
 | 
					use std::collections::{HashMap, HashSet};
 | 
				
			||||||
use std::time::Duration;
 | 
					use std::time::{
 | 
				
			||||||
 | 
					    Duration,
 | 
				
			||||||
 | 
					    SystemTime,
 | 
				
			||||||
 | 
					    UNIX_EPOCH
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use aws_smithy_types::date_time::{
 | 
				
			||||||
 | 
					    DateTime as AwsDateTime,
 | 
				
			||||||
 | 
					    Format as AwsDateTimeFormat,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use serde::{Serialize, Deserialize};
 | 
				
			||||||
use tokio::{
 | 
					use tokio::{
 | 
				
			||||||
    sync::oneshot::Sender,
 | 
					    sync::oneshot::Sender,
 | 
				
			||||||
    sync::RwLock,
 | 
					    sync::RwLock,
 | 
				
			||||||
    time::sleep,
 | 
					    time::sleep,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
use sqlx::SqlitePool;
 | 
					use sqlx::SqlitePool;
 | 
				
			||||||
 | 
					use sodiumoxide::crypto::{
 | 
				
			||||||
 | 
					        pwhash,
 | 
				
			||||||
 | 
					        pwhash::Salt, 
 | 
				
			||||||
 | 
					        secretbox, 
 | 
				
			||||||
 | 
					        secretbox::{Nonce, Key}
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
use tauri::async_runtime as runtime;
 | 
					use tauri::async_runtime as runtime;
 | 
				
			||||||
use tauri::Manager;
 | 
					use tauri::Manager;
 | 
				
			||||||
 | 
					use serde::Serializer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::app::APP;
 | 
					 | 
				
			||||||
use crate::credentials::{
 | 
					 | 
				
			||||||
    Session,
 | 
					 | 
				
			||||||
    BaseCredentials,
 | 
					 | 
				
			||||||
    SessionCredentials,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
use crate::{config, config::AppConfig};
 | 
					use crate::{config, config::AppConfig};
 | 
				
			||||||
use crate::ipc::{self, Approval};
 | 
					use crate::ipc;
 | 
				
			||||||
use crate::clientinfo::Client;
 | 
					use crate::clientinfo::Client;
 | 
				
			||||||
use crate::errors::*;
 | 
					use crate::errors::*;
 | 
				
			||||||
use crate::server::Server;
 | 
					use crate::server::Server;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Clone, Debug, Serialize, Deserialize)]
 | 
				
			||||||
 | 
					#[serde(rename_all = "PascalCase")]
 | 
				
			||||||
 | 
					pub struct BaseCredentials {
 | 
				
			||||||
 | 
					    access_key_id: String,
 | 
				
			||||||
 | 
					    secret_access_key: String,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Clone, Debug, Serialize)]
 | 
				
			||||||
 | 
					#[serde(rename_all = "PascalCase")]
 | 
				
			||||||
 | 
					pub struct SessionCredentials {
 | 
				
			||||||
 | 
					    access_key_id: String,
 | 
				
			||||||
 | 
					    secret_access_key: String,
 | 
				
			||||||
 | 
					    token: String,
 | 
				
			||||||
 | 
					    #[serde(serialize_with = "serialize_expiration")]
 | 
				
			||||||
 | 
					    expiration: AwsDateTime,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl SessionCredentials {
 | 
				
			||||||
 | 
					    fn is_expired(&self) -> bool {
 | 
				
			||||||
 | 
					        let current_ts = SystemTime::now()
 | 
				
			||||||
 | 
					            .duration_since(UNIX_EPOCH)
 | 
				
			||||||
 | 
					            .unwrap() // doesn't panic because UNIX_EPOCH won't be later than now()
 | 
				
			||||||
 | 
					            .as_secs();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let expire_ts = self.expiration.secs();
 | 
				
			||||||
 | 
					        let remaining = expire_ts - (current_ts as i64);
 | 
				
			||||||
 | 
					        remaining < 60
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Clone, Debug)]
 | 
				
			||||||
 | 
					pub struct LockedCredentials {
 | 
				
			||||||
 | 
					    access_key_id: String,
 | 
				
			||||||
 | 
					    secret_key_enc: Vec<u8>,
 | 
				
			||||||
 | 
					    salt: Salt,
 | 
				
			||||||
 | 
					    nonce: Nonce,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Clone, Debug)]
 | 
				
			||||||
 | 
					pub enum Session {
 | 
				
			||||||
 | 
					    Unlocked{
 | 
				
			||||||
 | 
					        base: BaseCredentials,
 | 
				
			||||||
 | 
					        session: SessionCredentials,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    Locked(LockedCredentials),
 | 
				
			||||||
 | 
					    Empty,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Debug)]
 | 
					#[derive(Debug)]
 | 
				
			||||||
pub struct AppState {
 | 
					pub struct AppState {
 | 
				
			||||||
    pub config: RwLock<AppConfig>,
 | 
					    pub config: RwLock<AppConfig>,
 | 
				
			||||||
@@ -47,11 +109,57 @@ impl AppState {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub async fn new_creds(&self, base_creds: BaseCredentials, passphrase: &str) -> Result<(), UnlockError> {
 | 
					    pub async fn load_creds(pool: &SqlitePool) -> Result<Session, SetupError> {
 | 
				
			||||||
        let locked = base_creds.encrypt(passphrase)?;
 | 
					        let res = sqlx::query!("SELECT * FROM credentials ORDER BY created_at desc")
 | 
				
			||||||
 | 
					            .fetch_optional(pool)
 | 
				
			||||||
 | 
					            .await?;
 | 
				
			||||||
 | 
					        let row = match res {
 | 
				
			||||||
 | 
					            Some(r) => r,
 | 
				
			||||||
 | 
					            None => {return Ok(Session::Empty);}
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let salt_buf: [u8; 32] = row.salt
 | 
				
			||||||
 | 
					            .try_into()
 | 
				
			||||||
 | 
					            .map_err(|_e| SetupError::InvalidRecord)?;
 | 
				
			||||||
 | 
					        let nonce_buf: [u8; 24] = row.nonce
 | 
				
			||||||
 | 
					            .try_into()
 | 
				
			||||||
 | 
					            .map_err(|_e| SetupError::InvalidRecord)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let creds = LockedCredentials {
 | 
				
			||||||
 | 
					            access_key_id: row.access_key_id,
 | 
				
			||||||
 | 
					            secret_key_enc: row.secret_key_enc,
 | 
				
			||||||
 | 
					            salt: Salt(salt_buf),
 | 
				
			||||||
 | 
					            nonce: Nonce(nonce_buf),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        Ok(Session::Locked(creds))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn save_creds(&self, creds: BaseCredentials, passphrase: &str) -> Result<(), UnlockError> {
 | 
				
			||||||
 | 
					        let BaseCredentials {access_key_id, secret_access_key} = creds;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // do this first so that if it fails we don't save bad credentials
 | 
					        // do this first so that if it fails we don't save bad credentials
 | 
				
			||||||
        self.new_session(base_creds).await?;
 | 
					        self.new_session(&access_key_id, &secret_access_key).await?;
 | 
				
			||||||
        locked.save(&self.pool).await?;
 | 
					
 | 
				
			||||||
 | 
					        let salt = pwhash::gen_salt();
 | 
				
			||||||
 | 
					        let mut key_buf = [0; secretbox::KEYBYTES];
 | 
				
			||||||
 | 
					        pwhash::derive_key_interactive(&mut key_buf, passphrase.as_bytes(), &salt).unwrap();
 | 
				
			||||||
 | 
					        let key = Key(key_buf);
 | 
				
			||||||
 | 
					        // not sure we need both salt AND nonce given that we generate a
 | 
				
			||||||
 | 
					        // fresh salt every time we encrypt, but better safe than sorry
 | 
				
			||||||
 | 
					        let nonce = secretbox::gen_nonce();
 | 
				
			||||||
 | 
					        let secret_key_enc = secretbox::seal(secret_access_key.as_bytes(), &nonce, &key);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        sqlx::query(
 | 
				
			||||||
 | 
					            "INSERT INTO credentials (access_key_id, secret_key_enc, salt, nonce, created_at)
 | 
				
			||||||
 | 
					            VALUES (?, ?, ?, ?, strftime('%s'))"
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					            .bind(&access_key_id)
 | 
				
			||||||
 | 
					            .bind(&secret_key_enc)
 | 
				
			||||||
 | 
					            .bind(&salt.0[0..])
 | 
				
			||||||
 | 
					            .bind(&nonce.0[0..])
 | 
				
			||||||
 | 
					            .execute(&self.pool)
 | 
				
			||||||
 | 
					            .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Ok(())
 | 
					        Ok(())
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -97,10 +205,7 @@ impl AppState {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub async fn send_response(&self, response: ipc::RequestResponse) -> Result<(), SendResponseError> {
 | 
					    pub async fn send_response(&self, response: ipc::RequestResponse) -> Result<(), SendResponseError> {
 | 
				
			||||||
        if let Approval::Approved = response.approval {
 | 
					        self.renew_session_if_expired().await?;
 | 
				
			||||||
            let mut session = self.session.write().await;
 | 
					 | 
				
			||||||
            session.renew_if_expired().await?;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let mut open_requests = self.open_requests.write().await;
 | 
					        let mut open_requests = self.open_requests.write().await;
 | 
				
			||||||
        let chan = open_requests
 | 
					        let chan = open_requests
 | 
				
			||||||
@@ -118,7 +223,7 @@ impl AppState {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        runtime::spawn(async move {
 | 
					        runtime::spawn(async move {
 | 
				
			||||||
            sleep(Duration::from_secs(5)).await;
 | 
					            sleep(Duration::from_secs(5)).await;
 | 
				
			||||||
            let app = APP.get().unwrap();
 | 
					            let app = crate::APP.get().unwrap();
 | 
				
			||||||
            let state = app.state::<AppState>();
 | 
					            let state = app.state::<AppState>();
 | 
				
			||||||
            let mut bans = state.bans.write().await;
 | 
					            let mut bans = state.bans.write().await;
 | 
				
			||||||
            bans.remove(&client);
 | 
					            bans.remove(&client);
 | 
				
			||||||
@@ -130,25 +235,46 @@ impl AppState {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub async fn unlock(&self, passphrase: &str) -> Result<(), UnlockError> {
 | 
					    pub async fn unlock(&self, passphrase: &str) -> Result<(), UnlockError> {
 | 
				
			||||||
        let base_creds = match *self.session.read().await {
 | 
					        let mut session = self.session.write().await;
 | 
				
			||||||
 | 
					        let LockedCredentials {
 | 
				
			||||||
 | 
					            access_key_id,
 | 
				
			||||||
 | 
					            secret_key_enc,
 | 
				
			||||||
 | 
					            salt,
 | 
				
			||||||
 | 
					            nonce
 | 
				
			||||||
 | 
					        } = match *session {
 | 
				
			||||||
            Session::Empty => {return Err(UnlockError::NoCredentials);},
 | 
					            Session::Empty => {return Err(UnlockError::NoCredentials);},
 | 
				
			||||||
            Session::Unlocked{..} => {return Err(UnlockError::NotLocked);},
 | 
					            Session::Unlocked{..} => {return Err(UnlockError::NotLocked);},
 | 
				
			||||||
            Session::Locked(ref locked) => locked.decrypt(passphrase)?,
 | 
					            Session::Locked(ref c) => c,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let mut key_buf = [0; secretbox::KEYBYTES];
 | 
				
			||||||
 | 
					        // pretty sure this only fails if we're out of memory
 | 
				
			||||||
 | 
					        pwhash::derive_key_interactive(&mut key_buf, passphrase.as_bytes(), salt).unwrap();
 | 
				
			||||||
 | 
					        let decrypted = secretbox::open(secret_key_enc, nonce, &Key(key_buf))
 | 
				
			||||||
 | 
					            .map_err(|_e| UnlockError::BadPassphrase)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let secret_access_key = String::from_utf8(decrypted).map_err(|_e| UnlockError::InvalidUtf8)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let session_creds = self.new_session(access_key_id, &secret_access_key).await?;
 | 
				
			||||||
 | 
					        *session = Session::Unlocked {
 | 
				
			||||||
 | 
					            base: BaseCredentials {
 | 
				
			||||||
 | 
					                access_key_id: access_key_id.clone(),
 | 
				
			||||||
 | 
					                secret_access_key,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            session: session_creds
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
        // Read lock is dropped here, so this doesn't deadlock
 | 
					 | 
				
			||||||
        self.new_session(base_creds).await?;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Ok(())
 | 
					        Ok(())
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub async fn serialize_base_creds(&self) -> Result<String, GetCredentialsError> {
 | 
					    // pub async fn serialize_base_creds(&self) -> Result<String, GetCredentialsError> {
 | 
				
			||||||
        let session = self.session.read().await;
 | 
					    //     let session = self.session.read().await;
 | 
				
			||||||
        match *session {
 | 
					    //     match *session {
 | 
				
			||||||
            Session::Unlocked{ref base, ..} => Ok(serde_json::to_string(base).unwrap()),
 | 
					    //         Session::Unlocked{ref base, ..} => Ok(serde_json::to_string(base).unwrap()),
 | 
				
			||||||
            Session::Locked(_) => Err(GetCredentialsError::Locked),
 | 
					    //         Session::Locked(_) => Err(GetCredentialsError::Locked),
 | 
				
			||||||
            Session::Empty => Err(GetCredentialsError::Empty),
 | 
					    //         Session::Empty => Err(GetCredentialsError::Empty),
 | 
				
			||||||
        }
 | 
					    //     }
 | 
				
			||||||
    }
 | 
					    // }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub async fn serialize_session_creds(&self) -> Result<String, GetCredentialsError> {
 | 
					    pub async fn serialize_session_creds(&self) -> Result<String, GetCredentialsError> {
 | 
				
			||||||
        let session = self.session.read().await;
 | 
					        let session = self.session.read().await;
 | 
				
			||||||
@@ -159,10 +285,77 @@ impl AppState {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async fn new_session(&self, base: BaseCredentials) -> Result<(), GetSessionError> {
 | 
					    async fn new_session(&self, key_id: &str, secret_key: &str) -> Result<SessionCredentials, GetSessionError> {
 | 
				
			||||||
        let session = SessionCredentials::from_base(&base).await?;
 | 
					        let creds = aws_sdk_sts::Credentials::new(
 | 
				
			||||||
        let mut app_session = self.session.write().await;
 | 
					            key_id,
 | 
				
			||||||
        *app_session = Session::Unlocked {base, session};
 | 
					            secret_key,
 | 
				
			||||||
        Ok(())
 | 
					            None, // token
 | 
				
			||||||
 | 
					            None, // expiration
 | 
				
			||||||
 | 
					            "creddy", // "provider name" apparently
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        let config = aws_config::from_env()
 | 
				
			||||||
 | 
					            .credentials_provider(creds)
 | 
				
			||||||
 | 
					            .load()
 | 
				
			||||||
 | 
					            .await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let client = aws_sdk_sts::Client::new(&config);
 | 
				
			||||||
 | 
					        let resp = client.get_session_token()
 | 
				
			||||||
 | 
					            .duration_seconds(43_200)
 | 
				
			||||||
 | 
					            .send()
 | 
				
			||||||
 | 
					            .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let aws_session = resp.credentials().ok_or(GetSessionError::EmptyResponse)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let access_key_id = aws_session.access_key_id()
 | 
				
			||||||
 | 
					            .ok_or(GetSessionError::EmptyResponse)?
 | 
				
			||||||
 | 
					            .to_string();
 | 
				
			||||||
 | 
					        let secret_access_key = aws_session.secret_access_key()
 | 
				
			||||||
 | 
					            .ok_or(GetSessionError::EmptyResponse)?
 | 
				
			||||||
 | 
					            .to_string();
 | 
				
			||||||
 | 
					        let token = aws_session.session_token()
 | 
				
			||||||
 | 
					            .ok_or(GetSessionError::EmptyResponse)?
 | 
				
			||||||
 | 
					            .to_string();
 | 
				
			||||||
 | 
					        let expiration = aws_session.expiration()
 | 
				
			||||||
 | 
					            .ok_or(GetSessionError::EmptyResponse)?
 | 
				
			||||||
 | 
					            .clone();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let session_creds = SessionCredentials {
 | 
				
			||||||
 | 
					                access_key_id,
 | 
				
			||||||
 | 
					                secret_access_key,
 | 
				
			||||||
 | 
					                token,
 | 
				
			||||||
 | 
					                expiration,
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        #[cfg(debug_assertions)]
 | 
				
			||||||
 | 
					        println!("Got new session:\n{}", serde_json::to_string(&session_creds).unwrap());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(session_creds)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn renew_session_if_expired(&self) -> Result<bool, GetSessionError> {
 | 
				
			||||||
 | 
					        match *self.session.write().await {
 | 
				
			||||||
 | 
					            Session::Unlocked{ref base, ref mut session} => {
 | 
				
			||||||
 | 
					                if !session.is_expired() {
 | 
				
			||||||
 | 
					                    return Ok(false);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                let new_session = self.new_session(
 | 
				
			||||||
 | 
					                    &base.access_key_id,
 | 
				
			||||||
 | 
					                    &base.secret_access_key
 | 
				
			||||||
 | 
					                ).await?;
 | 
				
			||||||
 | 
					                *session = new_session;
 | 
				
			||||||
 | 
					                Ok(true)
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            Session::Locked(_) => Err(GetSessionError::CredentialsLocked),
 | 
				
			||||||
 | 
					            Session::Empty => Err(GetSessionError::CredentialsEmpty),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn serialize_expiration<S>(exp: &AwsDateTime, serializer: S) -> Result<S::Ok, S::Error>
 | 
				
			||||||
 | 
					where S: Serializer
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    // this only fails if the d/t is out of range, which it can't be for this format
 | 
				
			||||||
 | 
					    let time_str = exp.fmt(AwsDateTimeFormat::DateTime).unwrap();
 | 
				
			||||||
 | 
					    serializer.serialize_str(&time_str)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,7 +8,7 @@
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
  "package": {
 | 
					  "package": {
 | 
				
			||||||
    "productName": "creddy",
 | 
					    "productName": "creddy",
 | 
				
			||||||
    "version": "0.2.3"
 | 
					    "version": "0.1.0"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "tauri": {
 | 
					  "tauri": {
 | 
				
			||||||
    "allowlist": {
 | 
					    "allowlist": {
 | 
				
			||||||
@@ -44,11 +44,7 @@
 | 
				
			|||||||
      "windows": {
 | 
					      "windows": {
 | 
				
			||||||
        "certificateThumbprint": null,
 | 
					        "certificateThumbprint": null,
 | 
				
			||||||
        "digestAlgorithm": "sha256",
 | 
					        "digestAlgorithm": "sha256",
 | 
				
			||||||
        "timestampUrl": "",
 | 
					        "timestampUrl": ""
 | 
				
			||||||
        "wix": {
 | 
					 | 
				
			||||||
          "fragmentPaths": ["conf/cli.wxs"],
 | 
					 | 
				
			||||||
          "componentRefs": ["CliBinary", "AddToPath"]
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "security": {
 | 
					    "security": {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,113 +0,0 @@
 | 
				
			|||||||
<script>
 | 
					 | 
				
			||||||
    export let color = 'base-content';
 | 
					 | 
				
			||||||
    export let thickness = '2px';
 | 
					 | 
				
			||||||
    let classes = '';
 | 
					 | 
				
			||||||
    export { classes as class };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const colorVars = {
 | 
					 | 
				
			||||||
        'primary': 'p',
 | 
					 | 
				
			||||||
        'primary-focus': 'pf',
 | 
					 | 
				
			||||||
        'primary-content': 'pc',
 | 
					 | 
				
			||||||
        'secondary': 's',
 | 
					 | 
				
			||||||
        'secondary-focus': 'sf',
 | 
					 | 
				
			||||||
        'secondary-content': 'sc',
 | 
					 | 
				
			||||||
        'accent': 'a',
 | 
					 | 
				
			||||||
        'accent-focus': 'af',
 | 
					 | 
				
			||||||
        'accent-content': 'ac',
 | 
					 | 
				
			||||||
        'neutral': 'n',
 | 
					 | 
				
			||||||
        'neutral-focus': 'nf',
 | 
					 | 
				
			||||||
        'neutral-content': 'nc',
 | 
					 | 
				
			||||||
        'base-100': 'b1',
 | 
					 | 
				
			||||||
        'base-200': 'b2',
 | 
					 | 
				
			||||||
        'base-300': 'b3',
 | 
					 | 
				
			||||||
        'base-content': 'bc',
 | 
					 | 
				
			||||||
        'info': 'in',
 | 
					 | 
				
			||||||
        'info-content': 'inc',
 | 
					 | 
				
			||||||
        'success': 'su',
 | 
					 | 
				
			||||||
        'success-content': 'suc',
 | 
					 | 
				
			||||||
        'warning': 'wa',
 | 
					 | 
				
			||||||
        'warning-content': 'wac',
 | 
					 | 
				
			||||||
        'error': 'er',
 | 
					 | 
				
			||||||
        'error-content': 'erc',
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let arcStyle = `border-width: ${thickness};`;
 | 
					 | 
				
			||||||
    arcStyle += `border-color: hsl(var(--${colorVars[color]})) transparent transparent transparent;`;
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<style>
 | 
					 | 
				
			||||||
    #spinner {
 | 
					 | 
				
			||||||
        position: relative;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        animation: spin;
 | 
					 | 
				
			||||||
        animation-duration: 1.5s;
 | 
					 | 
				
			||||||
        animation-iteration-count: infinite;
 | 
					 | 
				
			||||||
        animation-timing-function: linear;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @keyframes spin {
 | 
					 | 
				
			||||||
        50% { transform: rotate(225deg); }
 | 
					 | 
				
			||||||
        100% { transform: rotate(360deg); }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .arc {
 | 
					 | 
				
			||||||
        position: absolute;
 | 
					 | 
				
			||||||
        top: 0;
 | 
					 | 
				
			||||||
        left: 0;
 | 
					 | 
				
			||||||
        border-radius: 9999px;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .arc-top {
 | 
					 | 
				
			||||||
        transform: rotate(-45deg);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .arc-right {
 | 
					 | 
				
			||||||
        animation: spin-right;
 | 
					 | 
				
			||||||
        animation-duration: 3s;
 | 
					 | 
				
			||||||
        animation-iteration-count: infinite;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .arc-bottom {
 | 
					 | 
				
			||||||
        animation: spin-bottom;
 | 
					 | 
				
			||||||
        animation-duration: 3s;
 | 
					 | 
				
			||||||
        animation-iteration-count: infinite;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .arc-left {
 | 
					 | 
				
			||||||
        animation: spin-left;
 | 
					 | 
				
			||||||
        animation-duration: 3s;
 | 
					 | 
				
			||||||
        animation-iteration-count: infinite;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @keyframes spin-top {
 | 
					 | 
				
			||||||
        0% { transform: rotate(-45deg); }
 | 
					 | 
				
			||||||
        50% { transform: rotate(315deg); }
 | 
					 | 
				
			||||||
        100% { transform: rotate(-45deg); }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @keyframes spin-right {
 | 
					 | 
				
			||||||
        0% { transform: rotate(45deg); }
 | 
					 | 
				
			||||||
        50% { transform: rotate(315deg); }
 | 
					 | 
				
			||||||
        100% { transform: rotate(405deg); }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @keyframes spin-bottom {
 | 
					 | 
				
			||||||
        0% { transform: rotate(135deg); }
 | 
					 | 
				
			||||||
        50% { transform: rotate(315deg); }
 | 
					 | 
				
			||||||
        100% { transform: rotate(495deg); }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @keyframes spin-left {
 | 
					 | 
				
			||||||
        0% { transform: rotate(225deg); }
 | 
					 | 
				
			||||||
        50% { transform: rotate(315deg); }
 | 
					 | 
				
			||||||
        100% { transform: rotate(585deg); }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
</style>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<div id="spinner" class="w-6 h-6 {classes}">
 | 
					 | 
				
			||||||
    <div class="arc arc-top w-full h-full" style={arcStyle}></div>
 | 
					 | 
				
			||||||
    <div class="arc arc-right w-full h-full" style={arcStyle}></div>
 | 
					 | 
				
			||||||
    <div class="arc arc-bottom w-full h-full" style={arcStyle}></div>
 | 
					 | 
				
			||||||
    <div class="arc arc-left w-full h-full" style={arcStyle}></div>
 | 
					 | 
				
			||||||
</div>
 | 
					 | 
				
			||||||
@@ -68,7 +68,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<!-- Don't render at all if we're just going to immediately proceed to the next screen -->
 | 
					<!-- Don't render at all if we're just going to immediately proceed to the next screen -->
 | 
				
			||||||
{#if error || !$appState.currentRequest.approval}
 | 
					{#if !$appState.currentRequest.approval}
 | 
				
			||||||
    <div class="flex flex-col space-y-4 p-4 m-auto max-w-xl h-screen items-center justify-center">
 | 
					    <div class="flex flex-col space-y-4 p-4 m-auto max-w-xl h-screen items-center justify-center">
 | 
				
			||||||
        {#if error}
 | 
					        {#if error}
 | 
				
			||||||
            <ErrorAlert bind:this={alert}>
 | 
					            <ErrorAlert bind:this={alert}>
 | 
				
			||||||
@@ -80,18 +80,6 @@
 | 
				
			|||||||
            </ErrorAlert>
 | 
					            </ErrorAlert>
 | 
				
			||||||
        {/if}
 | 
					        {/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        {#if $appState.currentRequest.base}
 | 
					 | 
				
			||||||
            <div class="alert alert-warning shadow-lg">
 | 
					 | 
				
			||||||
                <div>
 | 
					 | 
				
			||||||
                    <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current flex-shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
 | 
					 | 
				
			||||||
                    <span>
 | 
					 | 
				
			||||||
                        WARNING: This application is requesting your base (long-lived) AWS credentials. 
 | 
					 | 
				
			||||||
                        These credentials are less secure than session credentials, since they don't expire automatically.
 | 
					 | 
				
			||||||
                    </span>
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
        {/if}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <div class="space-y-1 mb-4">
 | 
					        <div class="space-y-1 mb-4">
 | 
				
			||||||
            <h2 class="text-xl font-bold">{appName ? `"${appName}"` : 'An appplication'} would like to access your AWS credentials.</h2>
 | 
					            <h2 class="text-xl font-bold">{appName ? `"${appName}"` : 'An appplication'} would like to access your AWS credentials.</h2>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,7 +7,6 @@
 | 
				
			|||||||
    import { navigate } from '../lib/routing.js';
 | 
					    import { navigate } from '../lib/routing.js';
 | 
				
			||||||
    import Link from '../ui/Link.svelte';
 | 
					    import Link from '../ui/Link.svelte';
 | 
				
			||||||
    import ErrorAlert from '../ui/ErrorAlert.svelte';
 | 
					    import ErrorAlert from '../ui/ErrorAlert.svelte';
 | 
				
			||||||
    import Spinner from '../ui/Spinner.svelte';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let errorMsg = null;
 | 
					    let errorMsg = null;
 | 
				
			||||||
@@ -20,7 +19,6 @@
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let saving = false;
 | 
					 | 
				
			||||||
    async function save() {
 | 
					    async function save() {
 | 
				
			||||||
        if (passphrase !== confirmPassphrase) {
 | 
					        if (passphrase !== confirmPassphrase) {
 | 
				
			||||||
            alert.shake();
 | 
					            alert.shake();
 | 
				
			||||||
@@ -29,7 +27,6 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        let credentials = {AccessKeyId, SecretAccessKey};
 | 
					        let credentials = {AccessKeyId, SecretAccessKey};
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            saving = true;
 | 
					 | 
				
			||||||
            await invoke('save_credentials', {credentials, passphrase});
 | 
					            await invoke('save_credentials', {credentials, passphrase});
 | 
				
			||||||
            if ($appState.currentRequest) {
 | 
					            if ($appState.currentRequest) {
 | 
				
			||||||
                navigate('Approve');
 | 
					                navigate('Approve');
 | 
				
			||||||
@@ -50,8 +47,6 @@
 | 
				
			|||||||
            if (alert) {
 | 
					            if (alert) {
 | 
				
			||||||
                alert.shake();
 | 
					                alert.shake();
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					 | 
				
			||||||
            saving = false;
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
@@ -70,13 +65,7 @@
 | 
				
			|||||||
    <input type="password" placeholder="Passphrase" bind:value="{passphrase}" class="input input-bordered" />
 | 
					    <input type="password" placeholder="Passphrase" bind:value="{passphrase}" class="input input-bordered" />
 | 
				
			||||||
    <input type="password" placeholder="Re-enter passphrase" bind:value={confirmPassphrase} class="input input-bordered" on:change={confirm} />
 | 
					    <input type="password" placeholder="Re-enter passphrase" bind:value={confirmPassphrase} class="input input-bordered" on:change={confirm} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <button type="submit" class="btn btn-primary">
 | 
					    <input type="submit" class="btn btn-primary" />
 | 
				
			||||||
        {#if saving}
 | 
					 | 
				
			||||||
            <Spinner class="w-5 h-5" color="primary-content" thickness="2px"/>
 | 
					 | 
				
			||||||
        {:else}
 | 
					 | 
				
			||||||
            Submit
 | 
					 | 
				
			||||||
        {/if}
 | 
					 | 
				
			||||||
    </button>
 | 
					 | 
				
			||||||
    <Link target="Home" hotkey="Escape">
 | 
					    <Link target="Home" hotkey="Escape">
 | 
				
			||||||
        <button class="btn btn-sm btn-outline w-full">Cancel</button>
 | 
					        <button class="btn btn-sm btn-outline w-full">Cancel</button>
 | 
				
			||||||
    </Link>
 | 
					    </Link>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,14 +7,12 @@
 | 
				
			|||||||
    import { getRootCause } from '../lib/errors.js';
 | 
					    import { getRootCause } from '../lib/errors.js';
 | 
				
			||||||
    import ErrorAlert from '../ui/ErrorAlert.svelte';
 | 
					    import ErrorAlert from '../ui/ErrorAlert.svelte';
 | 
				
			||||||
    import Link from '../ui/Link.svelte';
 | 
					    import Link from '../ui/Link.svelte';
 | 
				
			||||||
    import Spinner from '../ui/Spinner.svelte';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let errorMsg = null;
 | 
					    let errorMsg = null;
 | 
				
			||||||
    let alert;
 | 
					    let alert;
 | 
				
			||||||
    let passphrase = '';
 | 
					    let passphrase = '';
 | 
				
			||||||
    let loadTime = 0;
 | 
					    let loadTime = 0;
 | 
				
			||||||
    let saving = false;
 | 
					 | 
				
			||||||
    async function unlock() {
 | 
					    async function unlock() {
 | 
				
			||||||
        // The hotkey for navigating here from homepage is Enter, which also
 | 
					        // The hotkey for navigating here from homepage is Enter, which also
 | 
				
			||||||
        // happens to trigger the form submit event
 | 
					        // happens to trigger the form submit event
 | 
				
			||||||
@@ -23,7 +21,6 @@
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            saving = true;
 | 
					 | 
				
			||||||
            let r = await invoke('unlock', {passphrase});
 | 
					            let r = await invoke('unlock', {passphrase});
 | 
				
			||||||
            $appState.credentialStatus = 'unlocked';
 | 
					            $appState.credentialStatus = 'unlocked';
 | 
				
			||||||
            if ($appState.currentRequest) {
 | 
					            if ($appState.currentRequest) {
 | 
				
			||||||
@@ -34,6 +31,7 @@
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        catch (e) {
 | 
					        catch (e) {
 | 
				
			||||||
 | 
					            window.error = e;
 | 
				
			||||||
            if (e.code === 'GetSession') {
 | 
					            if (e.code === 'GetSession') {
 | 
				
			||||||
                let root = getRootCause(e);
 | 
					                let root = getRootCause(e);
 | 
				
			||||||
                errorMsg = `Error response from AWS (${root.code}): ${root.msg}`;
 | 
					                errorMsg = `Error response from AWS (${root.code}): ${root.msg}`;
 | 
				
			||||||
@@ -42,12 +40,9 @@
 | 
				
			|||||||
                errorMsg = e.msg;
 | 
					                errorMsg = e.msg;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            // if the alert already existed, shake it
 | 
					 | 
				
			||||||
            if (alert) {
 | 
					            if (alert) {
 | 
				
			||||||
                alert.shake();
 | 
					                alert.shake();
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					 | 
				
			||||||
            saving = false;
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -67,14 +62,7 @@
 | 
				
			|||||||
    <!-- svelte-ignore a11y-autofocus -->
 | 
					    <!-- svelte-ignore a11y-autofocus -->
 | 
				
			||||||
    <input autofocus name="password" type="password" placeholder="correct horse battery staple" bind:value="{passphrase}" class="input input-bordered" />
 | 
					    <input autofocus name="password" type="password" placeholder="correct horse battery staple" bind:value="{passphrase}" class="input input-bordered" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <button type="submit" class="btn btn-primary">
 | 
					    <input type="submit" class="btn btn-primary" />
 | 
				
			||||||
        {#if saving}
 | 
					 | 
				
			||||||
            <Spinner class="w-5 h-5" color="primary-content" thickness="2px"/>
 | 
					 | 
				
			||||||
        {:else}
 | 
					 | 
				
			||||||
            Submit
 | 
					 | 
				
			||||||
        {/if}
 | 
					 | 
				
			||||||
    </button>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <Link target="Home" hotkey="Escape">
 | 
					    <Link target="Home" hotkey="Escape">
 | 
				
			||||||
        <button class="btn btn-outline btn-sm w-full">Cancel</button>
 | 
					        <button class="btn btn-outline btn-sm w-full">Cancel</button>
 | 
				
			||||||
    </Link>
 | 
					    </Link>
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user