Compare commits
10 Commits
0491cb5790
...
f311fde74e
Author | SHA1 | Date | |
---|---|---|---|
f311fde74e | |||
acc5c71bfa | |||
504c0b4156 | |||
bf0a2ca72d | |||
bb980c5eef | |||
ce7d75f15a | |||
37b44ddb2e | |||
8c668e51a6 | |||
9928996fab | |||
d0a2532c27 |
4
.gitignore
vendored
4
.gitignore
vendored
@ -5,7 +5,3 @@ src-tauri/target/
|
|||||||
# .env is system-specific
|
# .env is system-specific
|
||||||
.env
|
.env
|
||||||
.vscode
|
.vscode
|
||||||
|
|
||||||
# just in case
|
|
||||||
credentials*
|
|
||||||
!credentials.rs
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en" data-theme="dark">
|
<html lang="en" data-theme="creddy">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
222
package-lock.json
generated
222
package-lock.json
generated
@ -1,17 +1,17 @@
|
|||||||
{
|
{
|
||||||
"name": "creddy",
|
"name": "creddy",
|
||||||
"version": "0.4.7",
|
"version": "0.4.9",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "creddy",
|
"name": "creddy",
|
||||||
"version": "0.4.7",
|
"version": "0.4.9",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2.0.0-beta.13",
|
"@tauri-apps/api": "^2.0.0-beta.13",
|
||||||
"@tauri-apps/plugin-dialog": "^2.0.0-beta.5",
|
"@tauri-apps/plugin-dialog": "^2.0.0-beta.5",
|
||||||
"@tauri-apps/plugin-os": "^2.0.0-beta.5",
|
"@tauri-apps/plugin-os": "^2.0.0-beta.5",
|
||||||
"daisyui": "^2.51.5"
|
"daisyui": "^4.12.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/vite-plugin-svelte": "^1.0.1",
|
"@sveltejs/vite-plugin-svelte": "^1.0.1",
|
||||||
@ -27,6 +27,7 @@
|
|||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
|
||||||
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
|
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
},
|
},
|
||||||
@ -70,6 +71,7 @@
|
|||||||
"version": "8.0.2",
|
"version": "8.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||||
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
|
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"string-width": "^5.1.2",
|
"string-width": "^5.1.2",
|
||||||
"string-width-cjs": "npm:string-width@^4.2.0",
|
"string-width-cjs": "npm:string-width@^4.2.0",
|
||||||
@ -86,6 +88,7 @@
|
|||||||
"version": "0.3.5",
|
"version": "0.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
|
||||||
"integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
|
"integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/set-array": "^1.2.1",
|
"@jridgewell/set-array": "^1.2.1",
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.10",
|
"@jridgewell/sourcemap-codec": "^1.4.10",
|
||||||
@ -99,6 +102,7 @@
|
|||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
@ -107,6 +111,7 @@
|
|||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
|
||||||
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
|
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
@ -114,12 +119,14 @@
|
|||||||
"node_modules/@jridgewell/sourcemap-codec": {
|
"node_modules/@jridgewell/sourcemap-codec": {
|
||||||
"version": "1.4.15",
|
"version": "1.4.15",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
|
||||||
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
|
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/trace-mapping": {
|
"node_modules/@jridgewell/trace-mapping": {
|
||||||
"version": "0.3.25",
|
"version": "0.3.25",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
|
||||||
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
|
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/resolve-uri": "^3.1.0",
|
"@jridgewell/resolve-uri": "^3.1.0",
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
@ -129,6 +136,7 @@
|
|||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
|
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nodelib/fs.stat": "2.0.5",
|
"@nodelib/fs.stat": "2.0.5",
|
||||||
"run-parallel": "^1.1.9"
|
"run-parallel": "^1.1.9"
|
||||||
@ -141,6 +149,7 @@
|
|||||||
"version": "2.0.5",
|
"version": "2.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
|
||||||
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
|
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
@ -149,6 +158,7 @@
|
|||||||
"version": "1.2.8",
|
"version": "1.2.8",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
|
||||||
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
|
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nodelib/fs.scandir": "2.1.5",
|
"@nodelib/fs.scandir": "2.1.5",
|
||||||
"fastq": "^1.6.0"
|
"fastq": "^1.6.0"
|
||||||
@ -161,6 +171,7 @@
|
|||||||
"version": "0.11.0",
|
"version": "0.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||||
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
|
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
@ -409,6 +420,7 @@
|
|||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
|
||||||
"integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
|
"integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@ -420,6 +432,7 @@
|
|||||||
"version": "6.2.1",
|
"version": "6.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
|
||||||
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
|
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@ -430,12 +443,14 @@
|
|||||||
"node_modules/any-promise": {
|
"node_modules/any-promise": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
|
||||||
"integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="
|
"integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/anymatch": {
|
"node_modules/anymatch": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||||
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"normalize-path": "^3.0.0",
|
"normalize-path": "^3.0.0",
|
||||||
"picomatch": "^2.0.4"
|
"picomatch": "^2.0.4"
|
||||||
@ -447,12 +462,14 @@
|
|||||||
"node_modules/arg": {
|
"node_modules/arg": {
|
||||||
"version": "5.0.2",
|
"version": "5.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
|
||||||
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="
|
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/autoprefixer": {
|
"node_modules/autoprefixer": {
|
||||||
"version": "10.4.19",
|
"version": "10.4.19",
|
||||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz",
|
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz",
|
||||||
"integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==",
|
"integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==",
|
||||||
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@ -488,12 +505,14 @@
|
|||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/binary-extensions": {
|
"node_modules/binary-extensions": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||||
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
|
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
},
|
},
|
||||||
@ -505,6 +524,7 @@
|
|||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0"
|
"balanced-match": "^1.0.0"
|
||||||
}
|
}
|
||||||
@ -513,6 +533,7 @@
|
|||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fill-range": "^7.1.1"
|
"fill-range": "^7.1.1"
|
||||||
},
|
},
|
||||||
@ -524,6 +545,7 @@
|
|||||||
"version": "4.23.0",
|
"version": "4.23.0",
|
||||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz",
|
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz",
|
||||||
"integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==",
|
"integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==",
|
||||||
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@ -563,6 +585,7 @@
|
|||||||
"version": "1.0.30001625",
|
"version": "1.0.30001625",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001625.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001625.tgz",
|
||||||
"integrity": "sha512-4KE9N2gcRH+HQhpeiRZXd+1niLB/XNLAhSy4z7fI8EzcbcPoAqjNInxVHTiTwWfTIV4w096XG8OtCOCQQKPv3w==",
|
"integrity": "sha512-4KE9N2gcRH+HQhpeiRZXd+1niLB/XNLAhSy4z7fI8EzcbcPoAqjNInxVHTiTwWfTIV4w096XG8OtCOCQQKPv3w==",
|
||||||
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@ -582,6 +605,7 @@
|
|||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||||
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"anymatch": "~3.1.2",
|
"anymatch": "~3.1.2",
|
||||||
"braces": "~3.0.2",
|
"braces": "~3.0.2",
|
||||||
@ -605,6 +629,7 @@
|
|||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"is-glob": "^4.0.1"
|
"is-glob": "^4.0.1"
|
||||||
},
|
},
|
||||||
@ -612,22 +637,11 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/color": {
|
|
||||||
"version": "4.2.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
|
|
||||||
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
|
|
||||||
"dependencies": {
|
|
||||||
"color-convert": "^2.0.1",
|
|
||||||
"color-string": "^1.9.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12.5.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"color-name": "~1.1.4"
|
"color-name": "~1.1.4"
|
||||||
},
|
},
|
||||||
@ -638,21 +652,14 @@
|
|||||||
"node_modules/color-name": {
|
"node_modules/color-name": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||||
},
|
"dev": true
|
||||||
"node_modules/color-string": {
|
|
||||||
"version": "1.9.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
|
|
||||||
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
|
|
||||||
"dependencies": {
|
|
||||||
"color-name": "^1.0.0",
|
|
||||||
"simple-swizzle": "^0.2.2"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"node_modules/commander": {
|
"node_modules/commander": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
||||||
"integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
|
"integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
@ -661,6 +668,7 @@
|
|||||||
"version": "7.0.3",
|
"version": "7.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||||
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
|
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"path-key": "^3.1.0",
|
"path-key": "^3.1.0",
|
||||||
"shebang-command": "^2.0.0",
|
"shebang-command": "^2.0.0",
|
||||||
@ -690,23 +698,30 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/culori": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/culori/-/culori-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-pHJg+jbuFsCjz9iclQBqyL3B2HLCBF71BwVNujUYEvCeQMvV97R59MNK3R2+jgJ3a1fcZgI9B3vYgz8lzr/BFQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/daisyui": {
|
"node_modules/daisyui": {
|
||||||
"version": "2.52.0",
|
"version": "4.12.8",
|
||||||
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-2.52.0.tgz",
|
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-4.12.8.tgz",
|
||||||
"integrity": "sha512-LQTA5/IVXAJHBMFoeaEMfd7/akAFPPcdQPR3O9fzzcFiczneJFM73CFPnScmW2sOgn/D83cvkP854ep2T9OfTg==",
|
"integrity": "sha512-FDdh0z9BsWMI0VeUSwZy6rwp9frEuUgd83SCPOaCYV3iULPzcgTEQT3IlcAbMCrsriu2ziDYZfGOUwPYHkHrfw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"color": "^4.2",
|
"css-selector-tokenizer": "^0.8",
|
||||||
"css-selector-tokenizer": "^0.8.0",
|
"culori": "^3",
|
||||||
"postcss-js": "^4.0.0",
|
"picocolors": "^1",
|
||||||
"tailwindcss": "^3"
|
"postcss-js": "^4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.9.0"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
"url": "https://opencollective.com/daisyui"
|
"url": "https://opencollective.com/daisyui"
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"autoprefixer": "^10.0.2",
|
|
||||||
"postcss": "^8.1.6"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
@ -738,27 +753,32 @@
|
|||||||
"node_modules/didyoumean": {
|
"node_modules/didyoumean": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
||||||
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="
|
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/dlv": {
|
"node_modules/dlv": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
|
||||||
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="
|
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/eastasianwidth": {
|
"node_modules/eastasianwidth": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||||
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
|
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.4.787",
|
"version": "1.4.787",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.787.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.787.tgz",
|
||||||
"integrity": "sha512-d0EFmtLPjctczO3LogReyM2pbBiiZbnsKnGF+cdZhsYzHm/A0GV7W94kqzLD8SN4O3f3iHlgLUChqghgyznvCQ=="
|
"integrity": "sha512-d0EFmtLPjctczO3LogReyM2pbBiiZbnsKnGF+cdZhsYzHm/A0GV7W94kqzLD8SN4O3f3iHlgLUChqghgyznvCQ==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/emoji-regex": {
|
"node_modules/emoji-regex": {
|
||||||
"version": "9.2.2",
|
"version": "9.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
||||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
|
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.15.18",
|
"version": "0.15.18",
|
||||||
@ -1121,6 +1141,7 @@
|
|||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
|
||||||
"integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==",
|
"integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
@ -1129,6 +1150,7 @@
|
|||||||
"version": "3.3.2",
|
"version": "3.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
|
||||||
"integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
|
"integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
|
||||||
|
"dev": true,
|
||||||
"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",
|
||||||
@ -1144,6 +1166,7 @@
|
|||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"is-glob": "^4.0.1"
|
"is-glob": "^4.0.1"
|
||||||
},
|
},
|
||||||
@ -1160,6 +1183,7 @@
|
|||||||
"version": "1.17.1",
|
"version": "1.17.1",
|
||||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
|
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
|
||||||
"integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==",
|
"integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"reusify": "^1.0.4"
|
"reusify": "^1.0.4"
|
||||||
}
|
}
|
||||||
@ -1168,6 +1192,7 @@
|
|||||||
"version": "7.1.1",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"to-regex-range": "^5.0.1"
|
"to-regex-range": "^5.0.1"
|
||||||
},
|
},
|
||||||
@ -1179,6 +1204,7 @@
|
|||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz",
|
||||||
"integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==",
|
"integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cross-spawn": "^7.0.0",
|
"cross-spawn": "^7.0.0",
|
||||||
"signal-exit": "^4.0.1"
|
"signal-exit": "^4.0.1"
|
||||||
@ -1194,6 +1220,7 @@
|
|||||||
"version": "4.3.7",
|
"version": "4.3.7",
|
||||||
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
|
||||||
"integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==",
|
"integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "*"
|
"node": "*"
|
||||||
},
|
},
|
||||||
@ -1206,6 +1233,7 @@
|
|||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||||
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -1219,6 +1247,7 @@
|
|||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||||
|
"dev": true,
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
@ -1227,6 +1256,7 @@
|
|||||||
"version": "10.4.1",
|
"version": "10.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz",
|
||||||
"integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==",
|
"integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"foreground-child": "^3.1.0",
|
"foreground-child": "^3.1.0",
|
||||||
"jackspeak": "^3.1.2",
|
"jackspeak": "^3.1.2",
|
||||||
@ -1248,6 +1278,7 @@
|
|||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||||
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
|
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"is-glob": "^4.0.3"
|
"is-glob": "^4.0.3"
|
||||||
},
|
},
|
||||||
@ -1259,6 +1290,7 @@
|
|||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"function-bind": "^1.1.2"
|
"function-bind": "^1.1.2"
|
||||||
},
|
},
|
||||||
@ -1266,15 +1298,11 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/is-arrayish": {
|
|
||||||
"version": "0.3.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
|
|
||||||
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
|
|
||||||
},
|
|
||||||
"node_modules/is-binary-path": {
|
"node_modules/is-binary-path": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"binary-extensions": "^2.0.0"
|
"binary-extensions": "^2.0.0"
|
||||||
},
|
},
|
||||||
@ -1286,6 +1314,7 @@
|
|||||||
"version": "2.13.1",
|
"version": "2.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
|
||||||
"integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==",
|
"integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"hasown": "^2.0.0"
|
"hasown": "^2.0.0"
|
||||||
},
|
},
|
||||||
@ -1297,6 +1326,7 @@
|
|||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@ -1305,6 +1335,7 @@
|
|||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
@ -1313,6 +1344,7 @@
|
|||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"is-extglob": "^2.1.1"
|
"is-extglob": "^2.1.1"
|
||||||
},
|
},
|
||||||
@ -1324,6 +1356,7 @@
|
|||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.12.0"
|
"node": ">=0.12.0"
|
||||||
}
|
}
|
||||||
@ -1331,12 +1364,14 @@
|
|||||||
"node_modules/isexe": {
|
"node_modules/isexe": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
|
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/jackspeak": {
|
"node_modules/jackspeak": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.1.2.tgz",
|
||||||
"integrity": "sha512-kWmLKn2tRtfYMF/BakihVVRzBKOxz4gJMiL2Rj91WnAB5TPZumSH99R/Yf1qE1u4uRimvCSJfm6hnxohXeEXjQ==",
|
"integrity": "sha512-kWmLKn2tRtfYMF/BakihVVRzBKOxz4gJMiL2Rj91WnAB5TPZumSH99R/Yf1qE1u4uRimvCSJfm6hnxohXeEXjQ==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@isaacs/cliui": "^8.0.2"
|
"@isaacs/cliui": "^8.0.2"
|
||||||
},
|
},
|
||||||
@ -1354,6 +1389,7 @@
|
|||||||
"version": "1.21.0",
|
"version": "1.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz",
|
||||||
"integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==",
|
"integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==",
|
||||||
|
"dev": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"jiti": "bin/jiti.js"
|
"jiti": "bin/jiti.js"
|
||||||
}
|
}
|
||||||
@ -1371,6 +1407,7 @@
|
|||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
|
||||||
"integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==",
|
"integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
@ -1378,12 +1415,14 @@
|
|||||||
"node_modules/lines-and-columns": {
|
"node_modules/lines-and-columns": {
|
||||||
"version": "1.2.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
||||||
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
|
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/lru-cache": {
|
"node_modules/lru-cache": {
|
||||||
"version": "10.2.2",
|
"version": "10.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz",
|
||||||
"integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==",
|
"integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "14 || >=16.14"
|
"node": "14 || >=16.14"
|
||||||
}
|
}
|
||||||
@ -1404,6 +1443,7 @@
|
|||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||||
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
|
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
@ -1412,6 +1452,7 @@
|
|||||||
"version": "4.0.7",
|
"version": "4.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz",
|
||||||
"integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==",
|
"integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"braces": "^3.0.3",
|
"braces": "^3.0.3",
|
||||||
"picomatch": "^2.3.1"
|
"picomatch": "^2.3.1"
|
||||||
@ -1424,6 +1465,7 @@
|
|||||||
"version": "9.0.4",
|
"version": "9.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
|
||||||
"integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==",
|
"integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"brace-expansion": "^2.0.1"
|
"brace-expansion": "^2.0.1"
|
||||||
},
|
},
|
||||||
@ -1438,6 +1480,7 @@
|
|||||||
"version": "7.1.2",
|
"version": "7.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||||
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16 || 14 >=14.17"
|
"node": ">=16 || 14 >=14.17"
|
||||||
}
|
}
|
||||||
@ -1452,6 +1495,7 @@
|
|||||||
"version": "2.7.0",
|
"version": "2.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
|
||||||
"integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
|
"integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"any-promise": "^1.0.0",
|
"any-promise": "^1.0.0",
|
||||||
"object-assign": "^4.0.1",
|
"object-assign": "^4.0.1",
|
||||||
@ -1478,12 +1522,14 @@
|
|||||||
"node_modules/node-releases": {
|
"node_modules/node-releases": {
|
||||||
"version": "2.0.14",
|
"version": "2.0.14",
|
||||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
|
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
|
||||||
"integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw=="
|
"integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/normalize-path": {
|
"node_modules/normalize-path": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@ -1492,6 +1538,7 @@
|
|||||||
"version": "0.1.2",
|
"version": "0.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
|
||||||
"integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
|
"integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@ -1500,6 +1547,7 @@
|
|||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@ -1508,6 +1556,7 @@
|
|||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
|
||||||
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
|
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
@ -1516,6 +1565,7 @@
|
|||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
@ -1523,12 +1573,14 @@
|
|||||||
"node_modules/path-parse": {
|
"node_modules/path-parse": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
|
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/path-scurry": {
|
"node_modules/path-scurry": {
|
||||||
"version": "1.11.1",
|
"version": "1.11.1",
|
||||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
|
||||||
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
|
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lru-cache": "^10.2.0",
|
"lru-cache": "^10.2.0",
|
||||||
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
|
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
|
||||||
@ -1549,6 +1601,7 @@
|
|||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8.6"
|
"node": ">=8.6"
|
||||||
},
|
},
|
||||||
@ -1560,6 +1613,7 @@
|
|||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
|
||||||
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
|
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@ -1568,6 +1622,7 @@
|
|||||||
"version": "4.0.6",
|
"version": "4.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
|
||||||
"integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==",
|
"integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
@ -1603,6 +1658,7 @@
|
|||||||
"version": "15.1.0",
|
"version": "15.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
|
||||||
"integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
|
"integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"postcss-value-parser": "^4.0.0",
|
"postcss-value-parser": "^4.0.0",
|
||||||
"read-cache": "^1.0.0",
|
"read-cache": "^1.0.0",
|
||||||
@ -1637,6 +1693,7 @@
|
|||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
|
||||||
"integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
|
"integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
|
||||||
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@ -1671,6 +1728,7 @@
|
|||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz",
|
||||||
"integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==",
|
"integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
},
|
},
|
||||||
@ -1682,6 +1740,7 @@
|
|||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz",
|
||||||
"integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==",
|
"integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"postcss-selector-parser": "^6.0.11"
|
"postcss-selector-parser": "^6.0.11"
|
||||||
},
|
},
|
||||||
@ -1700,6 +1759,7 @@
|
|||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz",
|
||||||
"integrity": "sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==",
|
"integrity": "sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cssesc": "^3.0.0",
|
"cssesc": "^3.0.0",
|
||||||
"util-deprecate": "^1.0.2"
|
"util-deprecate": "^1.0.2"
|
||||||
@ -1711,12 +1771,14 @@
|
|||||||
"node_modules/postcss-value-parser": {
|
"node_modules/postcss-value-parser": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
||||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
|
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/queue-microtask": {
|
"node_modules/queue-microtask": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"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==",
|
||||||
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@ -1736,6 +1798,7 @@
|
|||||||
"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",
|
||||||
"integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
|
"integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"pify": "^2.3.0"
|
"pify": "^2.3.0"
|
||||||
}
|
}
|
||||||
@ -1744,6 +1807,7 @@
|
|||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"picomatch": "^2.2.1"
|
"picomatch": "^2.2.1"
|
||||||
},
|
},
|
||||||
@ -1755,6 +1819,7 @@
|
|||||||
"version": "1.22.8",
|
"version": "1.22.8",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
|
||||||
"integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
|
"integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"is-core-module": "^2.13.0",
|
"is-core-module": "^2.13.0",
|
||||||
"path-parse": "^1.0.7",
|
"path-parse": "^1.0.7",
|
||||||
@ -1771,6 +1836,7 @@
|
|||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
|
||||||
"integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
|
"integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"iojs": ">=1.0.0",
|
"iojs": ">=1.0.0",
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
@ -1795,6 +1861,7 @@
|
|||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||||
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
|
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
|
||||||
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@ -1817,6 +1884,7 @@
|
|||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"shebang-regex": "^3.0.0"
|
"shebang-regex": "^3.0.0"
|
||||||
},
|
},
|
||||||
@ -1828,6 +1896,7 @@
|
|||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
@ -1836,6 +1905,7 @@
|
|||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||||
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
},
|
},
|
||||||
@ -1843,14 +1913,6 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/simple-swizzle": {
|
|
||||||
"version": "0.2.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
|
|
||||||
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
|
|
||||||
"dependencies": {
|
|
||||||
"is-arrayish": "^0.3.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
|
||||||
@ -1870,6 +1932,7 @@
|
|||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||||
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
|
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"eastasianwidth": "^0.2.0",
|
"eastasianwidth": "^0.2.0",
|
||||||
"emoji-regex": "^9.2.2",
|
"emoji-regex": "^9.2.2",
|
||||||
@ -1887,6 +1950,7 @@
|
|||||||
"version": "4.2.3",
|
"version": "4.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"emoji-regex": "^8.0.0",
|
"emoji-regex": "^8.0.0",
|
||||||
"is-fullwidth-code-point": "^3.0.0",
|
"is-fullwidth-code-point": "^3.0.0",
|
||||||
@ -1900,6 +1964,7 @@
|
|||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
@ -1907,12 +1972,14 @@
|
|||||||
"node_modules/string-width-cjs/node_modules/emoji-regex": {
|
"node_modules/string-width-cjs/node_modules/emoji-regex": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/string-width-cjs/node_modules/strip-ansi": {
|
"node_modules/string-width-cjs/node_modules/strip-ansi": {
|
||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-regex": "^5.0.1"
|
"ansi-regex": "^5.0.1"
|
||||||
},
|
},
|
||||||
@ -1924,6 +1991,7 @@
|
|||||||
"version": "7.1.0",
|
"version": "7.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
|
||||||
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
|
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-regex": "^6.0.1"
|
"ansi-regex": "^6.0.1"
|
||||||
},
|
},
|
||||||
@ -1939,6 +2007,7 @@
|
|||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-regex": "^5.0.1"
|
"ansi-regex": "^5.0.1"
|
||||||
},
|
},
|
||||||
@ -1950,6 +2019,7 @@
|
|||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
@ -1958,6 +2028,7 @@
|
|||||||
"version": "3.35.0",
|
"version": "3.35.0",
|
||||||
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
|
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
|
||||||
"integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==",
|
"integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/gen-mapping": "^0.3.2",
|
"@jridgewell/gen-mapping": "^0.3.2",
|
||||||
"commander": "^4.0.0",
|
"commander": "^4.0.0",
|
||||||
@ -1979,6 +2050,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
||||||
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
|
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
},
|
},
|
||||||
@ -2011,6 +2083,7 @@
|
|||||||
"version": "3.4.3",
|
"version": "3.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.3.tgz",
|
||||||
"integrity": "sha512-U7sxQk/n397Bmx4JHbJx/iSOOv5G+II3f1kpLpY2QeUv5DcPdcTsYLlusZfq1NthHS1c1cZoyFmmkex1rzke0A==",
|
"integrity": "sha512-U7sxQk/n397Bmx4JHbJx/iSOOv5G+II3f1kpLpY2QeUv5DcPdcTsYLlusZfq1NthHS1c1cZoyFmmkex1rzke0A==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alloc/quick-lru": "^5.2.0",
|
"@alloc/quick-lru": "^5.2.0",
|
||||||
"arg": "^5.0.2",
|
"arg": "^5.0.2",
|
||||||
@ -2047,6 +2120,7 @@
|
|||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
|
||||||
"integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
|
"integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"any-promise": "^1.0.0"
|
"any-promise": "^1.0.0"
|
||||||
}
|
}
|
||||||
@ -2055,6 +2129,7 @@
|
|||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
|
||||||
"integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
|
"integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"thenify": ">= 3.1.0 < 4"
|
"thenify": ">= 3.1.0 < 4"
|
||||||
},
|
},
|
||||||
@ -2066,6 +2141,7 @@
|
|||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"is-number": "^7.0.0"
|
"is-number": "^7.0.0"
|
||||||
},
|
},
|
||||||
@ -2076,12 +2152,14 @@
|
|||||||
"node_modules/ts-interface-checker": {
|
"node_modules/ts-interface-checker": {
|
||||||
"version": "0.1.13",
|
"version": "0.1.13",
|
||||||
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
|
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
|
||||||
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="
|
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/update-browserslist-db": {
|
"node_modules/update-browserslist-db": {
|
||||||
"version": "1.0.16",
|
"version": "1.0.16",
|
||||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz",
|
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz",
|
||||||
"integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==",
|
"integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==",
|
||||||
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@ -2110,7 +2188,8 @@
|
|||||||
"node_modules/util-deprecate": {
|
"node_modules/util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "3.2.10",
|
"version": "3.2.10",
|
||||||
@ -2179,6 +2258,7 @@
|
|||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"isexe": "^2.0.0"
|
"isexe": "^2.0.0"
|
||||||
},
|
},
|
||||||
@ -2193,6 +2273,7 @@
|
|||||||
"version": "8.1.0",
|
"version": "8.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
|
||||||
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
|
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-styles": "^6.1.0",
|
"ansi-styles": "^6.1.0",
|
||||||
"string-width": "^5.0.1",
|
"string-width": "^5.0.1",
|
||||||
@ -2210,6 +2291,7 @@
|
|||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-styles": "^4.0.0",
|
"ansi-styles": "^4.0.0",
|
||||||
"string-width": "^4.1.0",
|
"string-width": "^4.1.0",
|
||||||
@ -2226,6 +2308,7 @@
|
|||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
@ -2234,6 +2317,7 @@
|
|||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"color-convert": "^2.0.1"
|
"color-convert": "^2.0.1"
|
||||||
},
|
},
|
||||||
@ -2247,12 +2331,14 @@
|
|||||||
"node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
|
"node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/wrap-ansi-cjs/node_modules/string-width": {
|
"node_modules/wrap-ansi-cjs/node_modules/string-width": {
|
||||||
"version": "4.2.3",
|
"version": "4.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"emoji-regex": "^8.0.0",
|
"emoji-regex": "^8.0.0",
|
||||||
"is-fullwidth-code-point": "^3.0.0",
|
"is-fullwidth-code-point": "^3.0.0",
|
||||||
@ -2266,6 +2352,7 @@
|
|||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-regex": "^5.0.1"
|
"ansi-regex": "^5.0.1"
|
||||||
},
|
},
|
||||||
@ -2277,6 +2364,7 @@
|
|||||||
"version": "2.4.2",
|
"version": "2.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz",
|
||||||
"integrity": "sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==",
|
"integrity": "sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==",
|
||||||
|
"dev": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"yaml": "bin.mjs"
|
"yaml": "bin.mjs"
|
||||||
},
|
},
|
||||||
|
@ -20,6 +20,6 @@
|
|||||||
"@tauri-apps/api": "^2.0.0-beta.13",
|
"@tauri-apps/api": "^2.0.0-beta.13",
|
||||||
"@tauri-apps/plugin-dialog": "^2.0.0-beta.5",
|
"@tauri-apps/plugin-dialog": "^2.0.0-beta.5",
|
||||||
"@tauri-apps/plugin-os": "^2.0.0-beta.5",
|
"@tauri-apps/plugin-os": "^2.0.0-beta.5",
|
||||||
"daisyui": "^2.51.5"
|
"daisyui": "^4.12.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
2768
src-tauri/Cargo.lock
generated
2768
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -28,12 +28,11 @@ serde = { version = "1.0", features = ["derive"] }
|
|||||||
tauri = { version = "2.0.0-beta", features = ["tray-icon"] }
|
tauri = { version = "2.0.0-beta", features = ["tray-icon"] }
|
||||||
sodiumoxide = "0.2.7"
|
sodiumoxide = "0.2.7"
|
||||||
tokio = { version = ">=1.19", features = ["full"] }
|
tokio = { version = ">=1.19", features = ["full"] }
|
||||||
sqlx = { version = "0.6.2", features = ["sqlite", "runtime-tokio-rustls"] }
|
|
||||||
sysinfo = "0.26.8"
|
sysinfo = "0.26.8"
|
||||||
aws-types = "0.52.0"
|
aws-config = "1.5.3"
|
||||||
aws-sdk-sts = "0.22.0"
|
aws-types = "1.3.2"
|
||||||
aws-smithy-types = "0.52.0"
|
aws-sdk-sts = "1.33.0"
|
||||||
aws-config = "0.52.0"
|
aws-smithy-types = "1.2.0"
|
||||||
thiserror = "1.0.38"
|
thiserror = "1.0.38"
|
||||||
once_cell = "1.16.0"
|
once_cell = "1.16.0"
|
||||||
strum = "0.24"
|
strum = "0.24"
|
||||||
@ -49,7 +48,14 @@ windows = { version = "0.51.1", features = ["Win32_Foundation", "Win32_System_Pi
|
|||||||
time = "0.3.31"
|
time = "0.3.31"
|
||||||
tauri-plugin-single-instance = "2.0.0-beta.9"
|
tauri-plugin-single-instance = "2.0.0-beta.9"
|
||||||
tauri-plugin-global-shortcut = "2.0.0-beta.6"
|
tauri-plugin-global-shortcut = "2.0.0-beta.6"
|
||||||
rfd = "0.14.1"
|
tauri-plugin-os = "2.0.0-beta.6"
|
||||||
|
tauri-plugin-dialog = "2.0.0-beta.9"
|
||||||
|
rfd = "0.13.0"
|
||||||
|
ssh-agent-lib = "0.4.0"
|
||||||
|
ssh-key = { version = "0.6.6", features = ["rsa", "ed25519", "encryption"] }
|
||||||
|
signature = "2.2.0"
|
||||||
|
tokio-stream = "0.1.15"
|
||||||
|
sqlx = { version = "0.7.4", features = ["sqlite", "runtime-tokio", "uuid"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# by default Tauri runs in production mode
|
# by default Tauri runs in production mode
|
||||||
|
@ -12,6 +12,8 @@
|
|||||||
"app:default",
|
"app:default",
|
||||||
"resources:default",
|
"resources:default",
|
||||||
"menu:default",
|
"menu:default",
|
||||||
"tray:default"
|
"tray:default",
|
||||||
|
"os:allow-os-type",
|
||||||
|
"dialog:allow-open"
|
||||||
]
|
]
|
||||||
}
|
}
|
File diff suppressed because one or more lines are too long
@ -1 +1 @@
|
|||||||
{"migrated":{"identifier":"migrated","description":"permissions that were migrated from v1","local":true,"windows":["main"],"permissions":["path:default","event:default","window:default","app:default","resources:default","menu:default","tray:default"]}}
|
{"migrated":{"identifier":"migrated","description":"permissions that were migrated from v1","local":true,"windows":["main"],"permissions":["path:default","event:default","window:default","app:default","resources:default","menu:default","tray:default","os:allow-os-type","dialog:allow-open"]}}
|
@ -247,6 +247,82 @@
|
|||||||
"app:deny-version"
|
"app:deny-version"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"dialog:default"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "dialog:allow-ask -> Enables the ask command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"dialog:allow-ask"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "dialog:allow-confirm -> Enables the confirm command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"dialog:allow-confirm"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "dialog:allow-message -> Enables the message command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"dialog:allow-message"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "dialog:allow-open -> Enables the open command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"dialog:allow-open"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "dialog:allow-save -> Enables the save command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"dialog:allow-save"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "dialog:deny-ask -> Denies the ask command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"dialog:deny-ask"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "dialog:deny-confirm -> Denies the confirm command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"dialog:deny-confirm"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "dialog:deny-message -> Denies the message command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"dialog:deny-message"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "dialog:deny-open -> Denies the open command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"dialog:deny-open"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "dialog:deny-save -> Denies the save command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"dialog:deny-save"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "event:default -> Default permissions for the plugin.",
|
"description": "event:default -> Default permissions for the plugin.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@ -778,6 +854,124 @@
|
|||||||
"menu:deny-text"
|
"menu:deny-text"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"os:default"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "os:allow-arch -> Enables the arch command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"os:allow-arch"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "os:allow-exe-extension -> Enables the exe_extension command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"os:allow-exe-extension"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "os:allow-family -> Enables the family command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"os:allow-family"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "os:allow-hostname -> Enables the hostname command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"os:allow-hostname"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "os:allow-locale -> Enables the locale command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"os:allow-locale"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "os:allow-os-type -> Enables the os_type command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"os:allow-os-type"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "os:allow-platform -> Enables the platform command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"os:allow-platform"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "os:allow-version -> Enables the version command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"os:allow-version"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "os:deny-arch -> Denies the arch command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"os:deny-arch"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "os:deny-exe-extension -> Denies the exe_extension command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"os:deny-exe-extension"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "os:deny-family -> Denies the family command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"os:deny-family"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "os:deny-hostname -> Denies the hostname command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"os:deny-hostname"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "os:deny-locale -> Denies the locale command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"os:deny-locale"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "os:deny-os-type -> Denies the os_type command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"os:deny-os-type"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "os:deny-platform -> Denies the platform command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"os:deny-platform"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "os:deny-version -> Denies the version command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"os:deny-version"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "path:default -> Default permissions for the plugin.",
|
"description": "path:default -> Default permissions for the plugin.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
@ -247,6 +247,82 @@
|
|||||||
"app:deny-version"
|
"app:deny-version"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"dialog:default"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "dialog:allow-ask -> Enables the ask command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"dialog:allow-ask"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "dialog:allow-confirm -> Enables the confirm command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"dialog:allow-confirm"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "dialog:allow-message -> Enables the message command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"dialog:allow-message"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "dialog:allow-open -> Enables the open command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"dialog:allow-open"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "dialog:allow-save -> Enables the save command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"dialog:allow-save"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "dialog:deny-ask -> Denies the ask command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"dialog:deny-ask"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "dialog:deny-confirm -> Denies the confirm command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"dialog:deny-confirm"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "dialog:deny-message -> Denies the message command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"dialog:deny-message"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "dialog:deny-open -> Denies the open command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"dialog:deny-open"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "dialog:deny-save -> Denies the save command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"dialog:deny-save"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "event:default -> Default permissions for the plugin.",
|
"description": "event:default -> Default permissions for the plugin.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@ -778,6 +854,124 @@
|
|||||||
"menu:deny-text"
|
"menu:deny-text"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"os:default"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "os:allow-arch -> Enables the arch command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"os:allow-arch"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "os:allow-exe-extension -> Enables the exe_extension command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"os:allow-exe-extension"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "os:allow-family -> Enables the family command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"os:allow-family"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "os:allow-hostname -> Enables the hostname command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"os:allow-hostname"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "os:allow-locale -> Enables the locale command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"os:allow-locale"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "os:allow-os-type -> Enables the os_type command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"os:allow-os-type"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "os:allow-platform -> Enables the platform command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"os:allow-platform"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "os:allow-version -> Enables the version command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"os:allow-version"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "os:deny-arch -> Denies the arch command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"os:deny-arch"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "os:deny-exe-extension -> Denies the exe_extension command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"os:deny-exe-extension"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "os:deny-family -> Denies the family command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"os:deny-family"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "os:deny-hostname -> Denies the hostname command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"os:deny-hostname"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "os:deny-locale -> Denies the locale command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"os:deny-locale"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "os:deny-os-type -> Denies the os_type command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"os:deny-os-type"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "os:deny-platform -> Denies the platform command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"os:deny-platform"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "os:deny-version -> Denies the version command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"os:deny-version"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "path:default -> Default permissions for the plugin.",
|
"description": "path:default -> Default permissions for the plugin.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
11
src-tauri/migrations/20240612192956_kv.sql
Normal file
11
src-tauri/migrations/20240612192956_kv.sql
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
-- key-value store, will be used for various one-off values, serialized to bytes
|
||||||
|
CREATE TABLE kv (
|
||||||
|
name TEXT PRIMARY KEY,
|
||||||
|
value BLOB
|
||||||
|
);
|
||||||
|
|
||||||
|
-- config is currently stored in its own table, as text
|
||||||
|
INSERT INTO kv (name, value)
|
||||||
|
SELECT 'config', CAST(data AS BLOB) FROM config;
|
||||||
|
|
||||||
|
DROP TABLE config;
|
77
src-tauri/migrations/20240617142724_credential_split.sql
Normal file
77
src-tauri/migrations/20240617142724_credential_split.sql
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
-- app structure is changing - instead of passphrase/salt being per credential,
|
||||||
|
-- we now have a single app-wide key, which is generated by hashing the passphrase
|
||||||
|
-- with the known salt. To verify the key thus produced, we store a value previously
|
||||||
|
-- encrypted with that key, and attempt decryption once the key has been re-generated.
|
||||||
|
|
||||||
|
-- For migration purposes, we want convert the passphrase for the most recent set of
|
||||||
|
-- AWS credentials and turn it into the app-wide passphrase. The only value that we
|
||||||
|
-- have which is encrypted with that passphrase is the secret key for those credentials,
|
||||||
|
-- so we will just use that as the `verify_blob`. Feels a little weird, but oh well.
|
||||||
|
WITH latest_creds AS (
|
||||||
|
SELECT *
|
||||||
|
FROM credentials
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 1
|
||||||
|
)
|
||||||
|
|
||||||
|
INSERT INTO kv (name, value)
|
||||||
|
SELECT 'salt', salt FROM latest_creds
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'verify_nonce', nonce FROM latest_creds
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'verify_blob', secret_key_enc FROM latest_creds;
|
||||||
|
|
||||||
|
|
||||||
|
-- Credentials are now going to be stored in a main table
|
||||||
|
-- plus ancillary tables for type-specific data
|
||||||
|
|
||||||
|
-- stash existing AWS creds in temporary table so that we can remake it
|
||||||
|
CREATE TABLE aws_tmp (id, access_key_id, secret_key_enc, nonce, created_at);
|
||||||
|
|
||||||
|
INSERT INTO aws_tmp
|
||||||
|
SELECT randomblob(16), access_key_id, secret_key_enc, nonce, created_at
|
||||||
|
FROM credentials
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
-- we only ever used one at a time in the past
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
-- new master credentials table
|
||||||
|
DROP TABLE credentials;
|
||||||
|
CREATE TABLE credentials (
|
||||||
|
-- id is a UUID so we can generate it on the frontend
|
||||||
|
id BLOB UNIQUE NOT NULL,
|
||||||
|
name TEXT UNIQUE NOT NULL,
|
||||||
|
credential_type TEXT NOT NULL,
|
||||||
|
is_default BOOLEAN NOT NULL,
|
||||||
|
created_at INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- populate with basic data from existing AWS credential
|
||||||
|
INSERT INTO credentials (id, name, credential_type, is_default, created_at)
|
||||||
|
SELECT id, 'default', 'aws', 1, created_at FROM aws_tmp;
|
||||||
|
|
||||||
|
-- new AWS-specific table
|
||||||
|
CREATE TABLE aws_credentials (
|
||||||
|
id BLOB UNIQUE NOT NULL,
|
||||||
|
access_key_id TEXT NOT NULL,
|
||||||
|
secret_key_enc BLOB NOT NULL,
|
||||||
|
nonce BLOB NOT NULL,
|
||||||
|
FOREIGN KEY(id) REFERENCES credentials(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- populate with AWS-specific data from existing credential
|
||||||
|
INSERT INTO aws_credentials (id, access_key_id, secret_key_enc, nonce)
|
||||||
|
SELECT id, access_key_id, secret_key_enc, nonce
|
||||||
|
FROM aws_tmp;
|
||||||
|
|
||||||
|
-- done with this now
|
||||||
|
DROP TABLE aws_tmp;
|
||||||
|
|
||||||
|
|
||||||
|
-- SSH keys are the new hotness
|
||||||
|
CREATE TABLE ssh_keys (
|
||||||
|
name TEXT UNIQUE NOT NULL,
|
||||||
|
public_key BLOB NOT NULL,
|
||||||
|
private_key_enc BLOB NOT NULL,
|
||||||
|
nonce BLOB NOT NULL
|
||||||
|
);
|
@ -23,7 +23,7 @@ use tauri::menu::MenuItem;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{self, AppConfig},
|
config::{self, AppConfig},
|
||||||
credentials::Session,
|
credentials::AppSession,
|
||||||
ipc,
|
ipc,
|
||||||
server::Server,
|
server::Server,
|
||||||
errors::*,
|
errors::*,
|
||||||
@ -43,28 +43,26 @@ pub fn run() -> tauri::Result<()> {
|
|||||||
.error_popup("Failed to show main window")
|
.error_popup("Failed to show main window")
|
||||||
}))
|
}))
|
||||||
.plugin(tauri_plugin_global_shortcut::Builder::default().build())
|
.plugin(tauri_plugin_global_shortcut::Builder::default().build())
|
||||||
|
.plugin(tauri_plugin_os::init())
|
||||||
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
ipc::unlock,
|
ipc::unlock,
|
||||||
|
ipc::lock,
|
||||||
|
ipc::reset_session,
|
||||||
|
ipc::set_passphrase,
|
||||||
ipc::respond,
|
ipc::respond,
|
||||||
ipc::get_session_status,
|
ipc::get_session_status,
|
||||||
ipc::signal_activity,
|
ipc::signal_activity,
|
||||||
ipc::save_credentials,
|
ipc::save_credential,
|
||||||
|
ipc::delete_credential,
|
||||||
|
ipc::list_credentials,
|
||||||
ipc::get_config,
|
ipc::get_config,
|
||||||
ipc::save_config,
|
ipc::save_config,
|
||||||
ipc::launch_terminal,
|
ipc::launch_terminal,
|
||||||
ipc::get_setup_errors,
|
ipc::get_setup_errors,
|
||||||
|
ipc::exit,
|
||||||
])
|
])
|
||||||
.setup(|app| {
|
.setup(|app| rt::block_on(setup(app)))
|
||||||
let res = rt::block_on(setup(app));
|
|
||||||
if let Err(ref e) = res {
|
|
||||||
MessageDialog::new()
|
|
||||||
.set_level(MessageLevel::Error)
|
|
||||||
.set_title("Creddy failed to start")
|
|
||||||
.set_description(format!("{e}"))
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
res
|
|
||||||
})
|
|
||||||
.build(tauri::generate_context!())?
|
.build(tauri::generate_context!())?
|
||||||
.run(|app, run_event| {
|
.run(|app, run_event| {
|
||||||
if let RunEvent::WindowEvent { event, .. } = run_event {
|
if let RunEvent::WindowEvent { event, .. } = run_event {
|
||||||
@ -100,7 +98,7 @@ async fn setup(app: &mut App) -> Result<(), Box<dyn Error>> {
|
|||||||
|
|
||||||
let mut conf = match AppConfig::load(&pool).await {
|
let mut conf = match AppConfig::load(&pool).await {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(SetupError::ConfigParseError(_)) => {
|
Err(LoadKvError::Invalid(_)) => {
|
||||||
setup_errors.push(
|
setup_errors.push(
|
||||||
"Could not load configuration from database. Reverting to defaults.".into()
|
"Could not load configuration from database. Reverting to defaults.".into()
|
||||||
);
|
);
|
||||||
@ -109,7 +107,7 @@ async fn setup(app: &mut App) -> Result<(), Box<dyn Error>> {
|
|||||||
err => err?,
|
err => err?,
|
||||||
};
|
};
|
||||||
|
|
||||||
let session = Session::load(&pool).await?;
|
let app_session = AppSession::load(&pool).await?;
|
||||||
Server::start(app.handle().clone())?;
|
Server::start(app.handle().clone())?;
|
||||||
|
|
||||||
config::set_auto_launch(conf.start_on_login)?;
|
config::set_auto_launch(conf.start_on_login)?;
|
||||||
@ -128,12 +126,11 @@ async fn setup(app: &mut App) -> Result<(), Box<dyn Error>> {
|
|||||||
.map(|names| names.split(':').any(|n| n == "GNOME"))
|
.map(|names| names.split(':').any(|n| n == "GNOME"))
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
|
||||||
// if session is empty, this is probably the first launch, so don't autohide
|
|
||||||
if !conf.start_minimized || is_first_launch {
|
if !conf.start_minimized || is_first_launch {
|
||||||
show_main_window(&app.handle())?;
|
show_main_window(&app.handle())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let state = AppState::new(conf, session, pool, setup_errors, desktop_is_gnome);
|
let state = AppState::new(conf, app_session, pool, setup_errors, desktop_is_gnome);
|
||||||
app.manage(state);
|
app.manage(state);
|
||||||
|
|
||||||
// make sure we do this after managing app state, so that it doesn't panic
|
// make sure we do this after managing app state, so that it doesn't panic
|
||||||
|
7
src-tauri/src/bin/agent.rs
Normal file
7
src-tauri/src/bin/agent.rs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
use creddy::server::ssh_agent;
|
||||||
|
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
ssh_agent::run().await;
|
||||||
|
}
|
13
src-tauri/src/bin/key.rs
Normal file
13
src-tauri/src/bin/key.rs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
use ssh_key::private::PrivateKey;
|
||||||
|
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
// let passphrase = std::env::var("PRIVKEY_PASSPHRASE").unwrap();
|
||||||
|
let p = AsRef::<std::path::Path>::as_ref("/home/joe/.ssh/test");
|
||||||
|
let privkey = PrivateKey::read_openssh_file(p)
|
||||||
|
.unwrap();
|
||||||
|
// .decrypt(passphrase.as_bytes())
|
||||||
|
// .unwrap();
|
||||||
|
|
||||||
|
dbg!(String::from_utf8_lossy(&privkey.to_bytes().unwrap()));
|
||||||
|
}
|
@ -12,7 +12,6 @@ use clap::{
|
|||||||
};
|
};
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
|
|
||||||
use crate::credentials::Credentials;
|
|
||||||
use crate::errors::*;
|
use crate::errors::*;
|
||||||
use crate::server::{Request, Response};
|
use crate::server::{Request, Response};
|
||||||
use crate::shortcuts::ShortcutAction;
|
use crate::shortcuts::ShortcutAction;
|
||||||
@ -80,9 +79,10 @@ pub fn parser() -> Command<'static> {
|
|||||||
|
|
||||||
pub fn get(args: &ArgMatches) -> Result<(), CliError> {
|
pub fn get(args: &ArgMatches) -> Result<(), CliError> {
|
||||||
let base = args.get_one("base").unwrap_or(&false);
|
let base = args.get_one("base").unwrap_or(&false);
|
||||||
let output = match get_credentials(*base)? {
|
let output = match make_request(&Request::GetAwsCredentials { base: *base })? {
|
||||||
Credentials::Base(creds) => serde_json::to_string(&creds).unwrap(),
|
Response::AwsBase(creds) => serde_json::to_string(&creds).unwrap(),
|
||||||
Credentials::Session(creds) => serde_json::to_string(&creds).unwrap(),
|
Response::AwsSession(creds) => serde_json::to_string(&creds).unwrap(),
|
||||||
|
r => return Err(RequestError::Unexpected(r).into()),
|
||||||
};
|
};
|
||||||
println!("{output}");
|
println!("{output}");
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -98,16 +98,17 @@ pub fn exec(args: &ArgMatches) -> Result<(), CliError> {
|
|||||||
let mut cmd = ChildCommand::new(cmd_name);
|
let mut cmd = ChildCommand::new(cmd_name);
|
||||||
cmd.args(cmd_line);
|
cmd.args(cmd_line);
|
||||||
|
|
||||||
match get_credentials(base)? {
|
match make_request(&Request::GetAwsCredentials { base })? {
|
||||||
Credentials::Base(creds) => {
|
Response::AwsBase(creds) => {
|
||||||
cmd.env("AWS_ACCESS_KEY_ID", creds.access_key_id);
|
cmd.env("AWS_ACCESS_KEY_ID", creds.access_key_id);
|
||||||
cmd.env("AWS_SECRET_ACCESS_KEY", creds.secret_access_key);
|
cmd.env("AWS_SECRET_ACCESS_KEY", creds.secret_access_key);
|
||||||
},
|
},
|
||||||
Credentials::Session(creds) => {
|
Response::AwsSession(creds) => {
|
||||||
cmd.env("AWS_ACCESS_KEY_ID", creds.access_key_id);
|
cmd.env("AWS_ACCESS_KEY_ID", creds.access_key_id);
|
||||||
cmd.env("AWS_SECRET_ACCESS_KEY", creds.secret_access_key);
|
cmd.env("AWS_SECRET_ACCESS_KEY", creds.secret_access_key);
|
||||||
cmd.env("AWS_SESSION_TOKEN", creds.session_token);
|
cmd.env("AWS_SESSION_TOKEN", creds.session_token);
|
||||||
}
|
},
|
||||||
|
r => return Err(RequestError::Unexpected(r).into()),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
@ -157,16 +158,6 @@ pub fn invoke_shortcut(args: &ArgMatches) -> Result<(), CliError> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn get_credentials(base: bool) -> Result<Credentials, RequestError> {
|
|
||||||
let req = Request::GetAwsCredentials { base };
|
|
||||||
match make_request(&req) {
|
|
||||||
Ok(Response::Aws(creds)) => Ok(creds),
|
|
||||||
Ok(r) => Err(RequestError::Unexpected(r)),
|
|
||||||
Err(e) => Err(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn make_request(req: &Request) -> Result<Response, RequestError> {
|
async fn make_request(req: &Request) -> Result<Response, RequestError> {
|
||||||
let mut data = serde_json::to_string(req).unwrap();
|
let mut data = serde_json::to_string(req).unwrap();
|
||||||
|
@ -7,6 +7,7 @@ use serde::{Serialize, Deserialize};
|
|||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
|
|
||||||
use crate::errors::*;
|
use crate::errors::*;
|
||||||
|
use crate::kv;
|
||||||
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
@ -77,31 +78,16 @@ impl Default for AppConfig {
|
|||||||
|
|
||||||
|
|
||||||
impl AppConfig {
|
impl AppConfig {
|
||||||
pub async fn load(pool: &SqlitePool) -> Result<AppConfig, SetupError> {
|
pub async fn load(pool: &SqlitePool) -> Result<AppConfig, LoadKvError> {
|
||||||
let res = sqlx::query!("SELECT * from config where name = 'main'")
|
let config = kv::load(pool, "config")
|
||||||
.fetch_optional(pool)
|
.await?
|
||||||
.await?;
|
.unwrap_or_else(|| AppConfig::default());
|
||||||
|
|
||||||
let row = match res {
|
Ok(config)
|
||||||
Some(row) => row,
|
|
||||||
None => return Ok(AppConfig::default()),
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(serde_json::from_str(&row.data)?)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn save(&self, pool: &SqlitePool) -> Result<(), sqlx::error::Error> {
|
pub async fn save(&self, pool: &SqlitePool) -> Result<(), sqlx::error::Error> {
|
||||||
let data = serde_json::to_string(self).unwrap();
|
kv::save(pool, "config", self).await
|
||||||
sqlx::query(
|
|
||||||
"INSERT INTO config (name, data) VALUES ('main', ?)
|
|
||||||
ON CONFLICT (name) DO UPDATE SET data = ?"
|
|
||||||
)
|
|
||||||
.bind(&data)
|
|
||||||
.bind(&data)
|
|
||||||
.execute(pool)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
258
src-tauri/src/credentials/aws.rs
Normal file
258
src-tauri/src/credentials/aws.rs
Normal file
@ -0,0 +1,258 @@
|
|||||||
|
use std::fmt::{self, Formatter};
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
use aws_config::BehaviorVersion;
|
||||||
|
use aws_smithy_types::date_time::{DateTime, Format};
|
||||||
|
use chacha20poly1305::XNonce;
|
||||||
|
use serde::{
|
||||||
|
Serialize,
|
||||||
|
Deserialize,
|
||||||
|
Serializer,
|
||||||
|
Deserializer,
|
||||||
|
};
|
||||||
|
use serde::de::{self, Visitor};
|
||||||
|
use sqlx::{
|
||||||
|
FromRow,
|
||||||
|
Sqlite,
|
||||||
|
Transaction,
|
||||||
|
types::Uuid,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{Credential, Crypto, PersistentCredential};
|
||||||
|
|
||||||
|
use crate::errors::*;
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, FromRow)]
|
||||||
|
pub struct AwsRow {
|
||||||
|
id: Uuid,
|
||||||
|
access_key_id: String,
|
||||||
|
secret_key_enc: Vec<u8>,
|
||||||
|
nonce: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "PascalCase")]
|
||||||
|
pub struct AwsBaseCredential {
|
||||||
|
#[serde(default = "default_credentials_version")]
|
||||||
|
pub version: usize,
|
||||||
|
pub access_key_id: String,
|
||||||
|
pub secret_access_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl AwsBaseCredential {
|
||||||
|
pub fn new(access_key_id: String, secret_access_key: String) -> Self {
|
||||||
|
Self {version: 1, access_key_id, secret_access_key}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PersistentCredential for AwsBaseCredential {
|
||||||
|
type Row = AwsRow;
|
||||||
|
|
||||||
|
fn type_name() -> &'static str { "aws" }
|
||||||
|
|
||||||
|
fn into_credential(self) -> Credential { Credential::AwsBase(self) }
|
||||||
|
|
||||||
|
fn row_id(row: &AwsRow) -> Uuid { row.id }
|
||||||
|
|
||||||
|
fn from_row(row: AwsRow, crypto: &Crypto) -> Result<Self, LoadCredentialsError> {
|
||||||
|
let nonce = XNonce::clone_from_slice(&row.nonce);
|
||||||
|
let secret_key_bytes = crypto.decrypt(&nonce, &row.secret_key_enc)?;
|
||||||
|
let secret_key = String::from_utf8(secret_key_bytes)
|
||||||
|
.map_err(|_| LoadCredentialsError::InvalidData)?;
|
||||||
|
|
||||||
|
Ok(Self::new(row.access_key_id, secret_key))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn save_details(&self, id: &Uuid, crypto: &Crypto, txn: &mut Transaction<'_, Sqlite>) -> Result<(), SaveCredentialsError> {
|
||||||
|
let (nonce, ciphertext) = crypto.encrypt(self.secret_access_key.as_bytes())?;
|
||||||
|
let nonce_bytes = &nonce.as_slice();
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
"INSERT OR REPLACE INTO aws_credentials (
|
||||||
|
id,
|
||||||
|
access_key_id,
|
||||||
|
secret_key_enc,
|
||||||
|
nonce
|
||||||
|
)
|
||||||
|
VALUES (?, ?, ?, ?);",
|
||||||
|
id, self.access_key_id, ciphertext, nonce_bytes,
|
||||||
|
).execute(&mut **txn).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "PascalCase")]
|
||||||
|
pub struct AwsSessionCredential {
|
||||||
|
#[serde(default = "default_credentials_version")]
|
||||||
|
pub version: usize,
|
||||||
|
pub access_key_id: String,
|
||||||
|
pub secret_access_key: String,
|
||||||
|
pub session_token: String,
|
||||||
|
#[serde(serialize_with = "serialize_expiration")]
|
||||||
|
#[serde(deserialize_with = "deserialize_expiration")]
|
||||||
|
pub expiration: DateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AwsSessionCredential {
|
||||||
|
pub async fn from_base(base: &AwsBaseCredential) -> Result<Self, GetSessionError> {
|
||||||
|
let req_creds = aws_sdk_sts::config::Credentials::new(
|
||||||
|
&base.access_key_id,
|
||||||
|
&base.secret_access_key,
|
||||||
|
None, // token
|
||||||
|
None, //expiration
|
||||||
|
"Creddy", // "provider name" apparently
|
||||||
|
);
|
||||||
|
let config = aws_config::defaults(BehaviorVersion::latest())
|
||||||
|
.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 session_creds = AwsSessionCredential {
|
||||||
|
version: 1,
|
||||||
|
access_key_id: aws_session.access_key_id,
|
||||||
|
secret_access_key: aws_session.secret_access_key,
|
||||||
|
session_token: aws_session.session_token,
|
||||||
|
expiration: aws_session.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 default_credentials_version() -> usize { 1 }
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use sqlx::SqlitePool;
|
||||||
|
use sqlx::types::uuid::uuid;
|
||||||
|
|
||||||
|
|
||||||
|
fn creds() -> AwsBaseCredential {
|
||||||
|
AwsBaseCredential::new(
|
||||||
|
"AKIAIOSFODNN7EXAMPLE".into(),
|
||||||
|
"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY".into(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn creds_2() -> AwsBaseCredential {
|
||||||
|
AwsBaseCredential::new(
|
||||||
|
"AKIAIOSFODNN7EXAMPL2".into(),
|
||||||
|
"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKE2".into(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_uuid() -> Uuid {
|
||||||
|
Uuid::try_parse("00000000-0000-0000-0000-000000000000").unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_uuid_2() -> Uuid {
|
||||||
|
Uuid::try_parse("ffffffff-ffff-ffff-ffff-ffffffffffff").unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_uuid_random() -> Uuid {
|
||||||
|
let bytes = Crypto::salt();
|
||||||
|
Uuid::from_slice(&bytes[..16]).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[sqlx::test(fixtures("aws_credentials"))]
|
||||||
|
async fn test_load(pool: SqlitePool) {
|
||||||
|
let crypt = Crypto::fixed();
|
||||||
|
let id = uuid!("00000000-0000-0000-0000-000000000000");
|
||||||
|
let loaded = AwsBaseCredential::load(&id, &crypt, &pool).await.unwrap();
|
||||||
|
assert_eq!(creds(), loaded);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[sqlx::test(fixtures("aws_credentials"))]
|
||||||
|
async fn test_load_by_name(pool: SqlitePool) {
|
||||||
|
let crypt = Crypto::fixed();
|
||||||
|
let loaded = AwsBaseCredential::load_by_name("test2", &crypt, &pool).await.unwrap();
|
||||||
|
assert_eq!(creds_2(), loaded);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[sqlx::test(fixtures("aws_credentials"))]
|
||||||
|
async fn test_load_default(pool: SqlitePool) {
|
||||||
|
let crypt = Crypto::fixed();
|
||||||
|
let loaded = AwsBaseCredential::load_default(&crypt, &pool).await.unwrap();
|
||||||
|
assert_eq!(creds(), loaded)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[sqlx::test(fixtures("aws_credentials"))]
|
||||||
|
async fn test_list(pool: SqlitePool) {
|
||||||
|
let crypt = Crypto::fixed();
|
||||||
|
let list: Vec<_> = AwsBaseCredential::list(&crypt, &pool)
|
||||||
|
.await
|
||||||
|
.expect("Failed to load credentials")
|
||||||
|
.into_iter()
|
||||||
|
.map(|(_, cred)| cred)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
assert_eq!(&creds().into_credential(), &list[0]);
|
||||||
|
assert_eq!(&creds_2().into_credential(), &list[1]);
|
||||||
|
}
|
||||||
|
}
|
116
src-tauri/src/credentials/crypto.rs
Normal file
116
src-tauri/src/credentials/crypto.rs
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
use std::fmt::{Debug, Formatter};
|
||||||
|
use argon2::{
|
||||||
|
Argon2,
|
||||||
|
Algorithm,
|
||||||
|
Version,
|
||||||
|
ParamsBuilder,
|
||||||
|
password_hash::rand_core::{RngCore, OsRng},
|
||||||
|
};
|
||||||
|
use chacha20poly1305::{
|
||||||
|
XChaCha20Poly1305,
|
||||||
|
XNonce,
|
||||||
|
aead::{
|
||||||
|
Aead,
|
||||||
|
AeadCore,
|
||||||
|
KeyInit,
|
||||||
|
generic_array::GenericArray,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::errors::*;
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub 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;
|
||||||
|
|
||||||
|
|
||||||
|
pub 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 })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub fn random() -> Crypto {
|
||||||
|
// salt and key are the same length, so we can just use this
|
||||||
|
let key = Crypto::salt();
|
||||||
|
let cipher = XChaCha20Poly1305::new(GenericArray::from_slice(&key));
|
||||||
|
Crypto { cipher }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub fn fixed() -> Crypto {
|
||||||
|
let key = [
|
||||||
|
1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
|
||||||
|
17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32,
|
||||||
|
];
|
||||||
|
|
||||||
|
let cipher = XChaCha20Poly1305::new(GenericArray::from_slice(&key));
|
||||||
|
Crypto { cipher }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn salt() -> [u8; 32] {
|
||||||
|
let mut salt = [0; 32];
|
||||||
|
OsRng.fill_bytes(&mut salt);
|
||||||
|
salt
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encrypt(&self, data: &[u8]) -> Result<(XNonce, Vec<u8>), CryptoError> {
|
||||||
|
let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
|
||||||
|
let ciphertext = self.cipher.encrypt(&nonce, data)?;
|
||||||
|
Ok((nonce, ciphertext))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decrypt(&self, nonce: &XNonce, data: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
||||||
|
let plaintext = self.cipher.decrypt(nonce, data)?;
|
||||||
|
Ok(plaintext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for Crypto {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||||
|
write!(f, "Crypto {{ [...] }}")
|
||||||
|
}
|
||||||
|
}
|
19
src-tauri/src/credentials/fixtures/aws_credentials.sql
Normal file
19
src-tauri/src/credentials/fixtures/aws_credentials.sql
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
INSERT INTO credentials (id, name, credential_type, is_default, created_at)
|
||||||
|
VALUES
|
||||||
|
(X'00000000000000000000000000000000', 'test', 'aws', 1, strftime('%s')),
|
||||||
|
(X'ffffffffffffffffffffffffffffffff', 'test2', 'aws', 0, strftime('%s'));
|
||||||
|
|
||||||
|
INSERT INTO aws_credentials (id, access_key_id, secret_key_enc, nonce)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
X'00000000000000000000000000000000',
|
||||||
|
'AKIAIOSFODNN7EXAMPLE',
|
||||||
|
X'B09ACDADD07E295A3FD9146D8D3672FA5C2518BFB15CF039E68820C42EFD3BC3BE3156ACF438C2C957EC113EF8617DBC71790EAFE39B3DE8',
|
||||||
|
X'DB777F2C6315DC0E12ADF322256E69D09D7FB586AAE614A6'
|
||||||
|
),
|
||||||
|
(
|
||||||
|
X'ffffffffffffffffffffffffffffffff',
|
||||||
|
'AKIAIOSFODNN7EXAMPL2',
|
||||||
|
X'ED6125FF40EF6F61929DF5FFD7141CD2B5A302A51C20152156477F8CC77980C614AB1B212AC06983F3CED35C4F3C54D4EE38964859930FBF',
|
||||||
|
X'962396B78DAA98DFDCC0AC0C9B7D688EC121F5759EBA790A'
|
||||||
|
);
|
116
src-tauri/src/credentials/mod.rs
Normal file
116
src-tauri/src/credentials/mod.rs
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
use sqlx::{
|
||||||
|
FromRow,
|
||||||
|
Sqlite,
|
||||||
|
SqlitePool,
|
||||||
|
sqlite::SqliteRow,
|
||||||
|
Transaction,
|
||||||
|
types::Uuid,
|
||||||
|
};
|
||||||
|
use tokio_stream::StreamExt;
|
||||||
|
|
||||||
|
use crate::errors::*;
|
||||||
|
|
||||||
|
mod aws;
|
||||||
|
pub use aws::{AwsBaseCredential, AwsSessionCredential};
|
||||||
|
|
||||||
|
mod record;
|
||||||
|
pub use record::CredentialRecord;
|
||||||
|
|
||||||
|
mod session;
|
||||||
|
pub use session::AppSession;
|
||||||
|
|
||||||
|
mod crypto;
|
||||||
|
pub use crypto::Crypto;
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "type")]
|
||||||
|
pub enum Credential {
|
||||||
|
AwsBase(AwsBaseCredential),
|
||||||
|
AwsSession(AwsSessionCredential),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub trait PersistentCredential: for<'a> Deserialize<'a> + Sized {
|
||||||
|
type Row: Send + Unpin + for<'r> FromRow<'r, SqliteRow>;
|
||||||
|
|
||||||
|
fn type_name() -> &'static str;
|
||||||
|
|
||||||
|
fn into_credential(self) -> Credential;
|
||||||
|
|
||||||
|
fn row_id(row: &Self::Row) -> Uuid;
|
||||||
|
|
||||||
|
fn from_row(row: Self::Row, crypto: &Crypto) -> Result<Self, LoadCredentialsError>;
|
||||||
|
|
||||||
|
// save_details needs to be implemented per-type because we don't know the number of parameters in advance
|
||||||
|
async fn save_details(&self, id: &Uuid, crypto: &Crypto, txn: &mut Transaction<'_, Sqlite>) -> Result<(), SaveCredentialsError>;
|
||||||
|
|
||||||
|
fn table_name() -> String {
|
||||||
|
format!("{}_credentials", Self::type_name())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load(id: &Uuid, crypto: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError> {
|
||||||
|
let q = format!("SELECT * FROM {} WHERE id = ?", Self::table_name());
|
||||||
|
let row: Self::Row = sqlx::query_as(&q)
|
||||||
|
.bind(id)
|
||||||
|
.fetch_optional(pool)
|
||||||
|
.await?
|
||||||
|
.ok_or(LoadCredentialsError::NoCredentials)?;
|
||||||
|
|
||||||
|
Self::from_row(row, crypto)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_by_name(name: &str, crypto: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError> {
|
||||||
|
let q = format!(
|
||||||
|
"SELECT * FROM {} WHERE id = (SELECT id FROM credentials WHERE name = ?)",
|
||||||
|
Self::table_name(),
|
||||||
|
);
|
||||||
|
let row: Self::Row = sqlx::query_as(&q)
|
||||||
|
.bind(name)
|
||||||
|
.fetch_optional(pool)
|
||||||
|
.await?
|
||||||
|
.ok_or(LoadCredentialsError::NoCredentials)?;
|
||||||
|
|
||||||
|
Self::from_row(row, crypto)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_default(crypto: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError> {
|
||||||
|
let q = format!(
|
||||||
|
"SELECT details.*
|
||||||
|
FROM {} details
|
||||||
|
JOIN credentials c
|
||||||
|
ON c.id = details.id
|
||||||
|
AND c.is_default = 1",
|
||||||
|
Self::table_name(),
|
||||||
|
);
|
||||||
|
let row: Self::Row = sqlx::query_as(&q)
|
||||||
|
.fetch_optional(pool)
|
||||||
|
.await?
|
||||||
|
.ok_or(LoadCredentialsError::NoCredentials)?;
|
||||||
|
|
||||||
|
Self::from_row(row, crypto)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list(crypto: &Crypto, pool: &SqlitePool) -> Result<Vec<(Uuid, Credential)>, LoadCredentialsError> {
|
||||||
|
let q = format!(
|
||||||
|
"SELECT details.*
|
||||||
|
FROM
|
||||||
|
{} details
|
||||||
|
JOIN credentials c
|
||||||
|
ON c.id = details.id
|
||||||
|
ORDER BY c.created_at",
|
||||||
|
Self::table_name(),
|
||||||
|
);
|
||||||
|
let mut rows = sqlx::query_as::<_, Self::Row>(&q).fetch(pool);
|
||||||
|
|
||||||
|
let mut creds = Vec::new();
|
||||||
|
while let Some(row) = rows.try_next().await? {
|
||||||
|
let id = Self::row_id(&row);
|
||||||
|
let cred = Self::from_row(row, crypto)?.into_credential();
|
||||||
|
creds.push((id, cred));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(creds)
|
||||||
|
}
|
||||||
|
}
|
410
src-tauri/src/credentials/record.rs
Normal file
410
src-tauri/src/credentials/record.rs
Normal file
@ -0,0 +1,410 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fmt::{self, Debug, Formatter};
|
||||||
|
use serde::{
|
||||||
|
Serialize,
|
||||||
|
Deserialize,
|
||||||
|
Serializer,
|
||||||
|
Deserializer,
|
||||||
|
};
|
||||||
|
use serde::de::{self, Visitor};
|
||||||
|
use sqlx::{
|
||||||
|
Error as SqlxError,
|
||||||
|
FromRow,
|
||||||
|
SqlitePool,
|
||||||
|
types::Uuid,
|
||||||
|
};
|
||||||
|
use tokio_stream::StreamExt;
|
||||||
|
|
||||||
|
use crate::errors::*;
|
||||||
|
use super::{
|
||||||
|
AwsBaseCredential,
|
||||||
|
Credential,
|
||||||
|
Crypto,
|
||||||
|
PersistentCredential,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, FromRow)]
|
||||||
|
struct CredentialRow {
|
||||||
|
id: Uuid,
|
||||||
|
name: String,
|
||||||
|
credential_type: String,
|
||||||
|
is_default: bool,
|
||||||
|
created_at: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct CredentialRecord {
|
||||||
|
#[serde(serialize_with = "serialize_uuid")]
|
||||||
|
#[serde(deserialize_with = "deserialize_uuid")]
|
||||||
|
pub id: Uuid, // UUID so it can be generated on the frontend
|
||||||
|
pub name: String, // user-facing identifier so it can be changed
|
||||||
|
pub is_default: bool,
|
||||||
|
pub credential: Credential,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CredentialRecord {
|
||||||
|
pub async fn save(&self, crypto: &Crypto, pool: &SqlitePool) -> Result<(), SaveCredentialsError> {
|
||||||
|
let type_name = match &self.credential {
|
||||||
|
Credential::AwsBase(_) => AwsBaseCredential::type_name(),
|
||||||
|
_ => return Err(SaveCredentialsError::NotPersistent),
|
||||||
|
};
|
||||||
|
|
||||||
|
// if the credential being saved is default, make sure it's the only default of its type
|
||||||
|
let mut txn = pool.begin().await?;
|
||||||
|
if self.is_default {
|
||||||
|
sqlx::query!(
|
||||||
|
"UPDATE credentials SET is_default = 0 WHERE credential_type = ?",
|
||||||
|
type_name
|
||||||
|
).execute(&mut *txn).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// save to parent credentials table
|
||||||
|
let res = sqlx::query!(
|
||||||
|
"INSERT INTO credentials (id, name, credential_type, is_default, created_at)
|
||||||
|
VALUES (?, ?, ?, ?, strftime('%s'))
|
||||||
|
ON CONFLICT(id) DO UPDATE SET
|
||||||
|
name = excluded.name,
|
||||||
|
credential_type = excluded.credential_type,
|
||||||
|
is_default = excluded.is_default",
|
||||||
|
self.id, self.name, type_name, self.is_default
|
||||||
|
).execute(&mut *txn).await;
|
||||||
|
|
||||||
|
// if id is unique, but name is not, we will get an error
|
||||||
|
// (if id is not unique, this becomes an upsert due to ON CONFLICT clause)
|
||||||
|
match res {
|
||||||
|
Err(SqlxError::Database(e)) if e.is_unique_violation() => Err(SaveCredentialsError::Duplicate),
|
||||||
|
Err(e) => Err(SaveCredentialsError::DbError(e)),
|
||||||
|
Ok(_) => Ok(())
|
||||||
|
}?;
|
||||||
|
|
||||||
|
// save credential details to child table
|
||||||
|
match &self.credential {
|
||||||
|
Credential::AwsBase(b) => b.save_details(&self.id, crypto, &mut txn).await,
|
||||||
|
_ => Err(SaveCredentialsError::NotPersistent),
|
||||||
|
}?;
|
||||||
|
|
||||||
|
// make it real
|
||||||
|
txn.commit().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_parts(row: CredentialRow, credential: Credential) -> Self {
|
||||||
|
CredentialRecord {
|
||||||
|
id: row.id,
|
||||||
|
name: row.name,
|
||||||
|
is_default: row.is_default,
|
||||||
|
credential,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_credential(row: CredentialRow, crypto: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError> {
|
||||||
|
let credential = match row.credential_type.as_str() {
|
||||||
|
"aws" => Credential::AwsBase(AwsBaseCredential::load(&row.id, crypto, pool).await?),
|
||||||
|
_ => return Err(LoadCredentialsError::InvalidData),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self::from_parts(row, credential))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn load(id: &Uuid, crypto: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError> {
|
||||||
|
let row: CredentialRow = sqlx::query_as("SELECT * FROM credentials WHERE id = ?")
|
||||||
|
.bind(id)
|
||||||
|
.fetch_optional(pool)
|
||||||
|
.await?
|
||||||
|
.ok_or(LoadCredentialsError::NoCredentials)?;
|
||||||
|
|
||||||
|
Self::load_credential(row, crypto, pool).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn load_default(credential_type: &str, crypto: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError> {
|
||||||
|
let row: CredentialRow = sqlx::query_as(
|
||||||
|
"SELECT * FROM credentials
|
||||||
|
WHERE credential_type = ? AND is_default = 1"
|
||||||
|
).bind(credential_type)
|
||||||
|
.fetch_optional(pool)
|
||||||
|
.await?
|
||||||
|
.ok_or(LoadCredentialsError::NoCredentials)?;
|
||||||
|
|
||||||
|
Self::load_credential(row, crypto, pool).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list(crypto: &Crypto, pool: &SqlitePool) -> Result<Vec<Self>, LoadCredentialsError> {
|
||||||
|
let mut parent_rows = sqlx::query_as::<_, CredentialRow>(
|
||||||
|
"SELECT * FROM credentials"
|
||||||
|
).fetch(pool);
|
||||||
|
|
||||||
|
let mut parent_map = HashMap::new();
|
||||||
|
while let Some(row) = parent_rows.try_next().await? {
|
||||||
|
parent_map.insert(row.id, row);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut records = Vec::with_capacity(parent_map.len());
|
||||||
|
|
||||||
|
for (id, credential) in AwsBaseCredential::list(crypto, pool).await? {
|
||||||
|
let parent = parent_map.remove(&id)
|
||||||
|
.ok_or(LoadCredentialsError::InvalidData)?;
|
||||||
|
records.push(Self::from_parts(parent, credential));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(records)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn rekey(old: &Crypto, new: &Crypto, pool: &SqlitePool) -> Result<(), SaveCredentialsError> {
|
||||||
|
for record in Self::list(old, pool).await? {
|
||||||
|
record.save(new, pool).await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fn serialize_uuid<S: Serializer>(u: &Uuid, s: S) -> Result<S::Ok, S::Error> {
|
||||||
|
let mut buf = Uuid::encode_buffer();
|
||||||
|
s.serialize_str(u.as_hyphenated().encode_lower(&mut buf))
|
||||||
|
}
|
||||||
|
|
||||||
|
struct UuidVisitor;
|
||||||
|
|
||||||
|
impl<'de> Visitor<'de> for UuidVisitor {
|
||||||
|
type Value = Uuid;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
|
||||||
|
write!(formatter, "a hyphenated UUID")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_str<E: de::Error>(self, v: &str) -> Result<Uuid, E> {
|
||||||
|
Uuid::try_parse(v)
|
||||||
|
.map_err(|_| E::custom(format!("Could not interpret string as UUID: {v}")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_uuid<'de, D: Deserializer<'de>>(ds: D) -> Result<Uuid, D::Error> {
|
||||||
|
ds.deserialize_str(UuidVisitor)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use sqlx::types::uuid::uuid;
|
||||||
|
|
||||||
|
|
||||||
|
fn aws_record() -> CredentialRecord {
|
||||||
|
let id = uuid!("00000000-0000-0000-0000-000000000000");
|
||||||
|
let aws = AwsBaseCredential::new(
|
||||||
|
"AKIAIOSFODNN7EXAMPLE".into(),
|
||||||
|
"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY".into(),
|
||||||
|
);
|
||||||
|
|
||||||
|
CredentialRecord {
|
||||||
|
id,
|
||||||
|
name: "test".into(),
|
||||||
|
is_default: true,
|
||||||
|
credential: Credential::AwsBase(aws),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn aws_record_2() -> CredentialRecord {
|
||||||
|
let id = uuid!("ffffffff-ffff-ffff-ffff-ffffffffffff");
|
||||||
|
let aws = AwsBaseCredential::new(
|
||||||
|
"AKIAIOSFODNN7EXAMPL2".into(),
|
||||||
|
"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKE2".into(),
|
||||||
|
);
|
||||||
|
|
||||||
|
CredentialRecord {
|
||||||
|
id,
|
||||||
|
name: "test2".into(),
|
||||||
|
is_default: false,
|
||||||
|
credential: Credential::AwsBase(aws),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn random_uuid() -> Uuid {
|
||||||
|
let bytes = Crypto::salt();
|
||||||
|
Uuid::from_slice(&bytes[..16]).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[sqlx::test(fixtures("aws_credentials"))]
|
||||||
|
async fn test_load_aws(pool: SqlitePool) {
|
||||||
|
let crypt = Crypto::fixed();
|
||||||
|
let id = uuid!("00000000-0000-0000-0000-000000000000");
|
||||||
|
let loaded = CredentialRecord::load(&id, &crypt, &pool).await
|
||||||
|
.expect("Failed to load record");
|
||||||
|
|
||||||
|
assert_eq!(aws_record(), loaded);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[sqlx::test(fixtures("aws_credentials"))]
|
||||||
|
async fn test_load_aws_default(pool: SqlitePool) {
|
||||||
|
let crypt = Crypto::fixed();
|
||||||
|
let loaded = CredentialRecord::load_default("aws", &crypt, &pool).await
|
||||||
|
.expect("Failed to load record");
|
||||||
|
|
||||||
|
assert_eq!(aws_record(), loaded);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[sqlx::test]
|
||||||
|
async fn test_save_aws(pool: SqlitePool) {
|
||||||
|
let crypt = Crypto::random();
|
||||||
|
let mut record = aws_record();
|
||||||
|
record.id = random_uuid();
|
||||||
|
|
||||||
|
aws_record().save(&crypt, &pool).await
|
||||||
|
.expect("Failed to save record");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[sqlx::test]
|
||||||
|
async fn test_save_load(pool: SqlitePool) {
|
||||||
|
let crypt = Crypto::random();
|
||||||
|
let mut record = aws_record();
|
||||||
|
record.id = random_uuid();
|
||||||
|
|
||||||
|
record.save(&crypt, &pool).await
|
||||||
|
.expect("Failed to save record");
|
||||||
|
let loaded = CredentialRecord::load(&record.id, &crypt, &pool).await
|
||||||
|
.expect("Failed to load record");
|
||||||
|
|
||||||
|
assert_eq!(record, loaded);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[sqlx::test]
|
||||||
|
async fn test_overwrite_aws(pool: SqlitePool) {
|
||||||
|
let crypt = Crypto::fixed();
|
||||||
|
|
||||||
|
let original = aws_record();
|
||||||
|
original.save(&crypt, &pool).await
|
||||||
|
.expect("Failed to save first record");
|
||||||
|
|
||||||
|
let mut updated = aws_record_2();
|
||||||
|
updated.id = original.id;
|
||||||
|
updated.save(&crypt, &pool).await
|
||||||
|
.expect("Failed to overwrite first record with second record");
|
||||||
|
|
||||||
|
// make sure update went through
|
||||||
|
let loaded = CredentialRecord::load(&updated.id, &crypt, &pool).await.unwrap();
|
||||||
|
assert_eq!(updated, loaded);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[sqlx::test(fixtures("aws_credentials"))]
|
||||||
|
async fn test_duplicate_name(pool: SqlitePool) {
|
||||||
|
let crypt = Crypto::random();
|
||||||
|
|
||||||
|
let mut record = aws_record();
|
||||||
|
record.id = random_uuid();
|
||||||
|
let resp = record.save(&crypt, &pool).await;
|
||||||
|
|
||||||
|
if !matches!(resp, Err(SaveCredentialsError::Duplicate)) {
|
||||||
|
panic!("Attempt to create duplicate entry returned {resp:?}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[sqlx::test(fixtures("aws_credentials"))]
|
||||||
|
async fn test_change_default(pool: SqlitePool) {
|
||||||
|
let crypt = Crypto::fixed();
|
||||||
|
let id = uuid!("ffffffff-ffff-ffff-ffff-ffffffffffff");
|
||||||
|
|
||||||
|
// confirm that record as it currently exists in the database is not default
|
||||||
|
let mut record = CredentialRecord::load(&id, &crypt, &pool).await
|
||||||
|
.expect("Failed to load record");
|
||||||
|
assert!(!record.is_default);
|
||||||
|
|
||||||
|
record.is_default = true;
|
||||||
|
record.save(&crypt, &pool).await
|
||||||
|
.expect("Failed to save record");
|
||||||
|
|
||||||
|
let loaded = CredentialRecord::load(&id, &crypt, &pool).await
|
||||||
|
.expect("Failed to re-load record");
|
||||||
|
assert!(loaded.is_default);
|
||||||
|
|
||||||
|
let other_id = uuid!("00000000-0000-0000-0000-000000000000");
|
||||||
|
let other_loaded = CredentialRecord::load(&other_id, &crypt, &pool).await
|
||||||
|
.expect("Failed to load other credential");
|
||||||
|
assert!(!other_loaded.is_default);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[sqlx::test(fixtures("aws_credentials"))]
|
||||||
|
async fn test_list(pool: SqlitePool) {
|
||||||
|
let crypt = Crypto::fixed();
|
||||||
|
|
||||||
|
let records = CredentialRecord::list(&crypt, &pool).await
|
||||||
|
.expect("Failed to list credentials");
|
||||||
|
|
||||||
|
assert_eq!(aws_record(), records[0]);
|
||||||
|
assert_eq!(aws_record_2(), records[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[sqlx::test(fixtures("aws_credentials"))]
|
||||||
|
async fn test_rekey(pool: SqlitePool) {
|
||||||
|
let old = Crypto::fixed();
|
||||||
|
let new = Crypto::random();
|
||||||
|
|
||||||
|
CredentialRecord::rekey(&old, &new, &pool).await
|
||||||
|
.expect("Failed to rekey credentials");
|
||||||
|
|
||||||
|
let records = CredentialRecord::list(&new, &pool).await
|
||||||
|
.expect("Failed to re-list credentials");
|
||||||
|
|
||||||
|
assert_eq!(aws_record(), records[0]);
|
||||||
|
assert_eq!(aws_record_2(), records[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod uuid_tests {
|
||||||
|
use super::*;
|
||||||
|
use sqlx::types::uuid::uuid;
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
|
struct UuidWrapper {
|
||||||
|
#[serde(serialize_with = "serialize_uuid")]
|
||||||
|
#[serde(deserialize_with = "deserialize_uuid")]
|
||||||
|
id: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialize_uuid() {
|
||||||
|
let u = UuidWrapper {
|
||||||
|
id: uuid!("693f84d2-4c1b-41e5-8483-cbe178324e04")
|
||||||
|
};
|
||||||
|
let computed = serde_json::to_string(&u).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
"{\"id\":\"693f84d2-4c1b-41e5-8483-cbe178324e04\"}",
|
||||||
|
&computed,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deserialize_uuid() {
|
||||||
|
let s = "{\"id\":\"045bd359-8630-4b76-9b7d-e4a86ed2222c\"}";
|
||||||
|
let computed = serde_json::from_str(s).unwrap();
|
||||||
|
let expected = UuidWrapper {
|
||||||
|
id: uuid!("045bd359-8630-4b76-9b7d-e4a86ed2222c"),
|
||||||
|
};
|
||||||
|
assert_eq!(expected, computed);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialize_deserialize_uuid() {
|
||||||
|
let buf = Crypto::salt();
|
||||||
|
let expected = UuidWrapper{
|
||||||
|
id: Uuid::from_slice(&buf[..16]).unwrap()
|
||||||
|
};
|
||||||
|
let serialized = serde_json::to_string(&expected).unwrap();
|
||||||
|
let computed = serde_json::from_str(&serialized).unwrap();
|
||||||
|
assert_eq!(expected, computed)
|
||||||
|
}
|
||||||
|
}
|
120
src-tauri/src/credentials/session.rs
Normal file
120
src-tauri/src/credentials/session.rs
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
use chacha20poly1305::XNonce;
|
||||||
|
use sqlx::SqlitePool;
|
||||||
|
|
||||||
|
use crate::errors::*;
|
||||||
|
use crate::kv;
|
||||||
|
use super::Crypto;
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum AppSession {
|
||||||
|
Unlocked {
|
||||||
|
salt: [u8; 32],
|
||||||
|
crypto: Crypto,
|
||||||
|
},
|
||||||
|
Locked {
|
||||||
|
salt: [u8; 32],
|
||||||
|
verify_nonce: XNonce,
|
||||||
|
verify_blob: Vec<u8>
|
||||||
|
},
|
||||||
|
Empty,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppSession {
|
||||||
|
pub fn new(passphrase: &str) -> Result<Self, CryptoError> {
|
||||||
|
let salt = Crypto::salt();
|
||||||
|
let crypto = Crypto::new(passphrase, &salt)?;
|
||||||
|
Ok(Self::Unlocked {salt, crypto})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unlock(&mut self, passphrase: &str) -> Result<(), UnlockError> {
|
||||||
|
let (salt, nonce, blob) = match self {
|
||||||
|
Self::Empty => return Err(UnlockError::NoCredentials),
|
||||||
|
Self::Unlocked {..} => return Err(UnlockError::NotLocked),
|
||||||
|
Self::Locked {salt, verify_nonce, verify_blob} => (salt, verify_nonce, verify_blob),
|
||||||
|
};
|
||||||
|
|
||||||
|
let crypto = Crypto::new(passphrase, salt)
|
||||||
|
.map_err(|e| CryptoError::Argon2(e))?;
|
||||||
|
|
||||||
|
// if passphrase is incorrect, this will fail
|
||||||
|
let _verify = crypto.decrypt(&nonce, &blob)?;
|
||||||
|
|
||||||
|
*self = Self::Unlocked {crypto, salt: *salt};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn load(pool: &SqlitePool) -> Result<Self, LoadCredentialsError> {
|
||||||
|
match kv::load_bytes_multi!(pool, "salt", "verify_nonce", "verify_blob").await? {
|
||||||
|
Some((salt, nonce, blob)) => {
|
||||||
|
|
||||||
|
Ok(Self::Locked {
|
||||||
|
salt: salt.try_into().map_err(|_| LoadCredentialsError::InvalidData)?,
|
||||||
|
// note: replace this with try_from at some point
|
||||||
|
verify_nonce: XNonce::clone_from_slice(&nonce),
|
||||||
|
verify_blob: blob,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
None => Ok(Self::Empty),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn save(&self, pool: &SqlitePool) -> Result<(), SaveCredentialsError> {
|
||||||
|
match self {
|
||||||
|
Self::Unlocked {salt, crypto} => {
|
||||||
|
let (nonce, blob) = crypto.encrypt(b"correct horse battery staple")?;
|
||||||
|
kv::save_bytes(pool, "salt", salt).await?;
|
||||||
|
kv::save_bytes(pool, "verify_nonce", &nonce.as_slice()).await?;
|
||||||
|
kv::save_bytes(pool, "verify_blob", &blob).await?;
|
||||||
|
},
|
||||||
|
Self::Locked {salt, verify_nonce, verify_blob} => {
|
||||||
|
kv::save_bytes(pool, "salt", salt).await?;
|
||||||
|
kv::save_bytes(pool, "verify_nonce", &verify_nonce.as_slice()).await?;
|
||||||
|
kv::save_bytes(pool, "verify_blob", verify_blob).await?;
|
||||||
|
},
|
||||||
|
// "saving" an empty session just means doing nothing
|
||||||
|
Self::Empty => (),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn reset(&mut self, pool: &SqlitePool) -> Result<(), SaveCredentialsError> {
|
||||||
|
match self {
|
||||||
|
Self::Unlocked {..} | Self::Locked {..} => {
|
||||||
|
kv::delete_multi(pool, &["salt", "verify_nonce", "verify_blob"]).await?;
|
||||||
|
*self = Self::Empty;
|
||||||
|
},
|
||||||
|
Self::Empty => (),
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn try_get_crypto(&self) -> Result<&Crypto, GetCredentialsError> {
|
||||||
|
match self {
|
||||||
|
Self::Empty => Err(GetCredentialsError::Empty),
|
||||||
|
Self::Locked {..} => Err(GetCredentialsError::Locked),
|
||||||
|
Self::Unlocked {crypto, ..} => Ok(crypto),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn try_encrypt(&self, data: &[u8]) -> Result<(XNonce, Vec<u8>), GetCredentialsError> {
|
||||||
|
let crypto = match self {
|
||||||
|
Self::Empty => return Err(GetCredentialsError::Empty),
|
||||||
|
Self::Locked {..} => return Err(GetCredentialsError::Locked),
|
||||||
|
Self::Unlocked {crypto, ..} => crypto,
|
||||||
|
};
|
||||||
|
let res = crypto.encrypt(data)?;
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn try_decrypt(&self, nonce: XNonce, data: &[u8]) -> Result<Vec<u8>, GetCredentialsError> {
|
||||||
|
let crypto = match self {
|
||||||
|
Self::Empty => return Err(GetCredentialsError::Empty),
|
||||||
|
Self::Locked {..} => return Err(GetCredentialsError::Locked),
|
||||||
|
Self::Unlocked {crypto, ..} => crypto,
|
||||||
|
};
|
||||||
|
let res = crypto.decrypt(&nonce, data)?;
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
}
|
@ -6,8 +6,9 @@ use strum_macros::AsRefStr;
|
|||||||
|
|
||||||
use thiserror::Error as ThisError;
|
use thiserror::Error as ThisError;
|
||||||
use aws_sdk_sts::{
|
use aws_sdk_sts::{
|
||||||
types::SdkError as AwsSdkError,
|
error::SdkError as AwsSdkError,
|
||||||
error::GetSessionTokenError,
|
operation::get_session_token::GetSessionTokenError,
|
||||||
|
error::ProvideErrorMetadata,
|
||||||
};
|
};
|
||||||
use rfd::{
|
use rfd::{
|
||||||
AsyncMessageDialog,
|
AsyncMessageDialog,
|
||||||
@ -127,10 +128,10 @@ pub enum SetupError {
|
|||||||
InvalidRecord, // e.g. wrong size blob for nonce or salt
|
InvalidRecord, // e.g. wrong size blob for nonce or salt
|
||||||
#[error("Error from database: {0}")]
|
#[error("Error from database: {0}")]
|
||||||
DbError(#[from] SqlxError),
|
DbError(#[from] SqlxError),
|
||||||
|
#[error("Error loading data: {0}")]
|
||||||
|
KvError(#[from] LoadKvError),
|
||||||
#[error("Error running migrations: {0}")]
|
#[error("Error running migrations: {0}")]
|
||||||
MigrationError(#[from] MigrateError),
|
MigrationError(#[from] MigrateError),
|
||||||
#[error("Error parsing configuration from database")]
|
|
||||||
ConfigParseError(#[from] serde_json::Error),
|
|
||||||
#[error("Failed to set up start-on-login: {0}")]
|
#[error("Failed to set up start-on-login: {0}")]
|
||||||
AutoLaunchError(#[from] auto_launch::Error),
|
AutoLaunchError(#[from] auto_launch::Error),
|
||||||
#[error("Failed to start listener: {0}")]
|
#[error("Failed to start listener: {0}")]
|
||||||
@ -208,6 +209,12 @@ pub enum GetCredentialsError {
|
|||||||
Locked,
|
Locked,
|
||||||
#[error("No credentials are known")]
|
#[error("No credentials are known")]
|
||||||
Empty,
|
Empty,
|
||||||
|
#[error(transparent)]
|
||||||
|
Crypto(#[from] CryptoError),
|
||||||
|
#[error(transparent)]
|
||||||
|
Load(#[from] LoadCredentialsError),
|
||||||
|
#[error(transparent)]
|
||||||
|
GetSession(#[from] GetSessionError),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -245,12 +252,58 @@ pub enum UnlockError {
|
|||||||
pub enum LockError {
|
pub enum LockError {
|
||||||
#[error("App is not unlocked")]
|
#[error("App is not unlocked")]
|
||||||
NotUnlocked,
|
NotUnlocked,
|
||||||
#[error("Database error: {0}")]
|
#[error(transparent)]
|
||||||
DbError(#[from] SqlxError),
|
LoadCredentials(#[from] LoadCredentialsError),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Setup(#[from] SetupError),
|
Setup(#[from] SetupError),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
TauriError(#[from] tauri::Error),
|
TauriError(#[from] tauri::Error),
|
||||||
|
#[error(transparent)]
|
||||||
|
Crypto(#[from] CryptoError),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, ThisError, AsRefStr)]
|
||||||
|
pub enum SaveCredentialsError {
|
||||||
|
#[error("Database error: {0}")]
|
||||||
|
DbError(#[from] SqlxError),
|
||||||
|
#[error("Encryption error: {0}")]
|
||||||
|
Crypto(#[from] CryptoError),
|
||||||
|
#[error(transparent)]
|
||||||
|
Session(#[from] GetCredentialsError),
|
||||||
|
#[error("App is locked")]
|
||||||
|
Locked,
|
||||||
|
#[error("Credential is temporary and cannot be saved")]
|
||||||
|
NotPersistent,
|
||||||
|
#[error("A credential with that name already exists")]
|
||||||
|
Duplicate,
|
||||||
|
// rekeying is fundamentally a save operation,
|
||||||
|
// but involves loading in order to re-save
|
||||||
|
#[error(transparent)]
|
||||||
|
LoadCredentials(#[from] LoadCredentialsError),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, ThisError, AsRefStr)]
|
||||||
|
pub enum LoadCredentialsError {
|
||||||
|
#[error("Database error: {0}")]
|
||||||
|
DbError(#[from] SqlxError),
|
||||||
|
#[error("Invalid passphrase")] // pretty sure this is the only way decryption fails
|
||||||
|
Encryption(#[from] CryptoError),
|
||||||
|
#[error("Credentials not found")]
|
||||||
|
NoCredentials,
|
||||||
|
#[error("Could not decode credential data")]
|
||||||
|
InvalidData,
|
||||||
|
#[error(transparent)]
|
||||||
|
LoadKv(#[from] LoadKvError),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, ThisError, AsRefStr)]
|
||||||
|
pub enum LoadKvError {
|
||||||
|
#[error("Database error: {0}")]
|
||||||
|
DbError(#[from] SqlxError),
|
||||||
|
#[error("Could not parse value from database: {0}")]
|
||||||
|
Invalid(#[from] serde_json::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -260,6 +313,10 @@ pub enum CryptoError {
|
|||||||
Argon2(#[from] argon2::Error),
|
Argon2(#[from] argon2::Error),
|
||||||
#[error("Invalid passphrase")] // I think this is the only way decryption fails
|
#[error("Invalid passphrase")] // I think this is the only way decryption fails
|
||||||
Aead(#[from] chacha20poly1305::aead::Error),
|
Aead(#[from] chacha20poly1305::aead::Error),
|
||||||
|
#[error("App is currently locked")]
|
||||||
|
Locked,
|
||||||
|
#[error("No passphrase has been specified")]
|
||||||
|
Empty,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -377,6 +434,8 @@ impl_serialize_basic!(GetCredentialsError);
|
|||||||
impl_serialize_basic!(ClientInfoError);
|
impl_serialize_basic!(ClientInfoError);
|
||||||
impl_serialize_basic!(WindowError);
|
impl_serialize_basic!(WindowError);
|
||||||
impl_serialize_basic!(LockError);
|
impl_serialize_basic!(LockError);
|
||||||
|
impl_serialize_basic!(SaveCredentialsError);
|
||||||
|
impl_serialize_basic!(LoadCredentialsError);
|
||||||
|
|
||||||
|
|
||||||
impl Serialize for HandlerError {
|
impl Serialize for HandlerError {
|
||||||
|
13
src-tauri/src/fixtures/kv.sql
Normal file
13
src-tauri/src/fixtures/kv.sql
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
INSERT INTO kv (name, value)
|
||||||
|
VALUES
|
||||||
|
-- b"hello world" (raw bytes)
|
||||||
|
('test_bytes', X'68656C6C6F20776F726C64'),
|
||||||
|
|
||||||
|
-- b"\"hello world\"" (JSON string)
|
||||||
|
('test_string', X'2268656C6C6F20776F726C6422'),
|
||||||
|
|
||||||
|
-- b"123" (JSON integer)
|
||||||
|
('test_int', X'313233'),
|
||||||
|
|
||||||
|
-- b"true" (JSON bool)
|
||||||
|
('test_bool', X'74727565')
|
@ -1,8 +1,12 @@
|
|||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
use tauri::State;
|
use sqlx::types::Uuid;
|
||||||
|
use tauri::{AppHandle, State};
|
||||||
|
|
||||||
use crate::config::AppConfig;
|
use crate::config::AppConfig;
|
||||||
use crate::credentials::{Session,BaseCredentials};
|
use crate::credentials::{
|
||||||
|
AppSession,
|
||||||
|
CredentialRecord
|
||||||
|
};
|
||||||
use crate::errors::*;
|
use crate::errors::*;
|
||||||
use crate::clientinfo::Client;
|
use crate::clientinfo::Client;
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
@ -17,6 +21,32 @@ pub struct AwsRequestNotification {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct SshRequestNotification {
|
||||||
|
pub id: u64,
|
||||||
|
pub client: Client,
|
||||||
|
pub key_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "type")]
|
||||||
|
pub enum RequestNotification {
|
||||||
|
Aws(AwsRequestNotification),
|
||||||
|
Ssh(SshRequestNotification),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RequestNotification {
|
||||||
|
pub fn new_aws(id: u64, client: Client, base: bool) -> Self {
|
||||||
|
Self::Aws(AwsRequestNotification {id, client, base})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_ssh(id: u64, client: Client, key_name: String) -> Self {
|
||||||
|
Self::Ssh(SshRequestNotification {id, client, key_name})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct RequestResponse {
|
pub struct RequestResponse {
|
||||||
pub id: u64,
|
pub id: u64,
|
||||||
@ -44,13 +74,31 @@ pub async fn unlock(passphrase: String, app_state: State<'_, AppState>) -> Resul
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn lock(app_state: State<'_, AppState>) -> Result<(), LockError> {
|
||||||
|
app_state.lock().await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn reset_session(app_state: State<'_, AppState>) -> Result<(), SaveCredentialsError> {
|
||||||
|
app_state.reset_session().await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn set_passphrase(passphrase: &str, app_state: State<'_, AppState>) -> Result<(), SaveCredentialsError> {
|
||||||
|
app_state.set_passphrase(passphrase).await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_session_status(app_state: State<'_, AppState>) -> Result<String, ()> {
|
pub async fn get_session_status(app_state: State<'_, AppState>) -> Result<String, ()> {
|
||||||
let session = app_state.session.read().await;
|
let session = app_state.app_session.read().await;
|
||||||
let status = match *session {
|
let status = match *session {
|
||||||
Session::Locked(_) => "locked".into(),
|
AppSession::Locked{..} => "locked".into(),
|
||||||
Session::Unlocked{..} => "unlocked".into(),
|
AppSession::Unlocked{..} => "unlocked".into(),
|
||||||
Session::Empty => "empty".into()
|
AppSession::Empty => "empty".into(),
|
||||||
};
|
};
|
||||||
Ok(status)
|
Ok(status)
|
||||||
}
|
}
|
||||||
@ -64,12 +112,25 @@ pub async fn signal_activity(app_state: State<'_, AppState>) -> Result<(), ()> {
|
|||||||
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn save_credentials(
|
pub async fn save_credential(
|
||||||
credentials: BaseCredentials,
|
record: CredentialRecord,
|
||||||
passphrase: String,
|
|
||||||
app_state: State<'_, AppState>
|
app_state: State<'_, AppState>
|
||||||
) -> Result<(), UnlockError> {
|
) -> Result<(), SaveCredentialsError> {
|
||||||
app_state.new_creds(credentials, &passphrase).await
|
app_state.save_credential(record).await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn delete_credential(id: &str, app_state: State<'_, AppState>) -> Result<(), SaveCredentialsError> {
|
||||||
|
let id = Uuid::try_parse(id)
|
||||||
|
.map_err(|_| LoadCredentialsError::NoCredentials)?;
|
||||||
|
app_state.delete_credential(&id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn list_credentials(app_state: State<'_, AppState>) -> Result<Vec<CredentialRecord>, GetCredentialsError> {
|
||||||
|
app_state.list_credentials().await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -91,7 +152,8 @@ pub async fn save_config(config: AppConfig, app_state: State<'_, AppState>) -> R
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn launch_terminal(base: bool) -> Result<(), LaunchTerminalError> {
|
pub async fn launch_terminal(base: bool) -> Result<(), LaunchTerminalError> {
|
||||||
terminal::launch(base).await
|
let res = terminal::launch(base).await;
|
||||||
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -99,3 +161,9 @@ pub async fn launch_terminal(base: bool) -> Result<(), LaunchTerminalError> {
|
|||||||
pub async fn get_setup_errors(app_state: State<'_, AppState>) -> Result<Vec<String>, ()> {
|
pub async fn get_setup_errors(app_state: State<'_, AppState>) -> Result<Vec<String>, ()> {
|
||||||
Ok(app_state.setup_errors.clone())
|
Ok(app_state.setup_errors.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn exit(app_handle: AppHandle) {
|
||||||
|
app_handle.exit(0)
|
||||||
|
}
|
||||||
|
210
src-tauri/src/kv.rs
Normal file
210
src-tauri/src/kv.rs
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
use serde::Serialize;
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
use sqlx::SqlitePool;
|
||||||
|
|
||||||
|
use crate::errors::*;
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn save<T>(pool: &SqlitePool, name: &str, value: &T) -> Result<(), sqlx::Error>
|
||||||
|
where T: Serialize + ?Sized
|
||||||
|
{
|
||||||
|
let bytes = serde_json::to_vec(value).unwrap();
|
||||||
|
save_bytes(pool, name, &bytes).await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn save_bytes(pool: &SqlitePool, name: &str, bytes: &[u8]) -> Result<(), sqlx::Error> {
|
||||||
|
sqlx::query!(
|
||||||
|
"INSERT INTO kv (name, value) VALUES (?, ?)
|
||||||
|
ON CONFLICT(name) DO UPDATE SET value = excluded.value;",
|
||||||
|
name,
|
||||||
|
bytes,
|
||||||
|
).execute(pool).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn load<T>(pool: &SqlitePool, name: &str) -> Result<Option<T>, LoadKvError>
|
||||||
|
where T: DeserializeOwned
|
||||||
|
{
|
||||||
|
let v = load_bytes(pool, name)
|
||||||
|
.await?
|
||||||
|
.map(|bytes| serde_json::from_slice(&bytes))
|
||||||
|
.transpose()?;
|
||||||
|
Ok(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn load_bytes(pool: &SqlitePool, name: &str) -> Result<Option<Vec<u8>>, sqlx::Error> {
|
||||||
|
sqlx::query!("SELECT name, value FROM kv WHERE name = ?", name)
|
||||||
|
.map(|row| row.value)
|
||||||
|
.fetch_optional(pool)
|
||||||
|
.await
|
||||||
|
.map(|o| o.flatten())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn delete(pool: &SqlitePool, name: &str) -> Result<(), sqlx::Error> {
|
||||||
|
sqlx::query!("DELETE FROM kv WHERE name = ?", name)
|
||||||
|
.execute(pool)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn delete_multi(pool: &SqlitePool, names: &[&str]) -> Result<(), sqlx::Error> {
|
||||||
|
let placeholder = names.iter()
|
||||||
|
.map(|_| "?")
|
||||||
|
.collect::<Vec<&str>>()
|
||||||
|
.join(",");
|
||||||
|
let query = format!("DELETE FROM kv WHERE name IN ({})", placeholder);
|
||||||
|
|
||||||
|
let mut q = sqlx::query(&query);
|
||||||
|
for name in names {
|
||||||
|
q = q.bind(name);
|
||||||
|
}
|
||||||
|
q.execute(pool).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
macro_rules! load_bytes_multi {
|
||||||
|
(
|
||||||
|
$pool:expr,
|
||||||
|
$($name:literal),*
|
||||||
|
) => {
|
||||||
|
// wrap everything up in an async block for easy short-circuiting...
|
||||||
|
async {
|
||||||
|
// ...returning a Result...
|
||||||
|
Ok::<_, sqlx::Error>(
|
||||||
|
//containing an Option...
|
||||||
|
Some(
|
||||||
|
// containing a tuple...
|
||||||
|
(
|
||||||
|
// ...with one item for each repetition of $name
|
||||||
|
$(
|
||||||
|
// load_bytes returns Result<Option<_>>, the Result is handled by
|
||||||
|
// the ? and we match on the Option
|
||||||
|
match crate::kv::load_bytes($pool, $name).await? {
|
||||||
|
Some(v) => v,
|
||||||
|
None => return Ok(None)
|
||||||
|
},
|
||||||
|
)*
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) use load_bytes_multi;
|
||||||
|
|
||||||
|
|
||||||
|
// macro_rules! load_multi {
|
||||||
|
// (
|
||||||
|
// $pool:expr,
|
||||||
|
// $($name:literal),*
|
||||||
|
// ) => {
|
||||||
|
// (|| {
|
||||||
|
// (
|
||||||
|
// $(
|
||||||
|
// match load(pool, $name)? {
|
||||||
|
// Some(v) => v,
|
||||||
|
// None => return Ok(None)
|
||||||
|
// },
|
||||||
|
// )*
|
||||||
|
// )
|
||||||
|
// })()
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
|
||||||
|
#[sqlx::test]
|
||||||
|
async fn test_save_bytes(pool: SqlitePool) {
|
||||||
|
save_bytes(&pool, "test_bytes", b"hello world").await
|
||||||
|
.expect("Failed to save bytes");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[sqlx::test]
|
||||||
|
async fn test_save(pool: SqlitePool) {
|
||||||
|
save(&pool, "test_string", "hello world").await
|
||||||
|
.expect("Failed to save string");
|
||||||
|
save(&pool, "test_int", &123).await
|
||||||
|
.expect("Failed to save integer");
|
||||||
|
save(&pool, "test_bool", &true).await
|
||||||
|
.expect("Failed to save bool");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[sqlx::test(fixtures("kv"))]
|
||||||
|
async fn test_load_bytes(pool: SqlitePool) {
|
||||||
|
let bytes = load_bytes(&pool, "test_bytes").await
|
||||||
|
.expect("Failed to load bytes")
|
||||||
|
.expect("Test data not found in database");
|
||||||
|
|
||||||
|
assert_eq!(bytes, Vec::from(b"hello world"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[sqlx::test(fixtures("kv"))]
|
||||||
|
async fn test_load(pool: SqlitePool) {
|
||||||
|
let string: String = load(&pool, "test_string").await
|
||||||
|
.expect("Failed to load string")
|
||||||
|
.expect("Test data not found in database");
|
||||||
|
assert_eq!(string, "hello world".to_string());
|
||||||
|
|
||||||
|
let integer: usize = load(&pool, "test_int").await
|
||||||
|
.expect("Failed to load integer")
|
||||||
|
.expect("Test data not found in database");
|
||||||
|
assert_eq!(integer, 123);
|
||||||
|
|
||||||
|
let boolean: bool = load(&pool, "test_bool").await
|
||||||
|
.expect("Failed to load boolean")
|
||||||
|
.expect("Test data not found in database");
|
||||||
|
assert_eq!(boolean, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[sqlx::test(fixtures("kv"))]
|
||||||
|
async fn test_load_multi(pool: SqlitePool) {
|
||||||
|
let (bytes, boolean) = load_bytes_multi!(&pool, "test_bytes", "test_bool")
|
||||||
|
.await
|
||||||
|
.expect("Failed to load items")
|
||||||
|
.expect("Test data not found in database");
|
||||||
|
|
||||||
|
assert_eq!(bytes, Vec::from(b"hello world"));
|
||||||
|
assert_eq!(boolean, Vec::from(b"true"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[sqlx::test(fixtures("kv"))]
|
||||||
|
async fn test_delete(pool: SqlitePool) {
|
||||||
|
delete(&pool, "test_bytes").await
|
||||||
|
.expect("Failed to delete data");
|
||||||
|
|
||||||
|
let loaded = load_bytes(&pool, "test_bytes").await
|
||||||
|
.expect("Failed to load data");
|
||||||
|
assert_eq!(loaded, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[sqlx::test(fixtures("kv"))]
|
||||||
|
async fn test_delete_multi(pool: SqlitePool) {
|
||||||
|
delete_multi(&pool, &["test_bytes", "test_string"]).await
|
||||||
|
.expect("Failed to delete keys");
|
||||||
|
|
||||||
|
let bytes_opt = load_bytes(&pool, "test_bytes").await
|
||||||
|
.expect("Failed to load bytes");
|
||||||
|
assert_eq!(bytes_opt, None);
|
||||||
|
|
||||||
|
let string_opt = load_bytes(&pool, "test_string").await
|
||||||
|
.expect("Failed to load string");
|
||||||
|
assert_eq!(string_opt, None);
|
||||||
|
}
|
||||||
|
}
|
@ -5,8 +5,9 @@ mod credentials;
|
|||||||
pub mod errors;
|
pub mod errors;
|
||||||
mod clientinfo;
|
mod clientinfo;
|
||||||
mod ipc;
|
mod ipc;
|
||||||
|
mod kv;
|
||||||
mod state;
|
mod state;
|
||||||
mod server;
|
pub mod server;
|
||||||
mod shortcuts;
|
mod shortcuts;
|
||||||
mod terminal;
|
mod terminal;
|
||||||
mod tray;
|
mod tray;
|
||||||
|
@ -7,8 +7,11 @@ use tauri::{AppHandle, Manager};
|
|||||||
|
|
||||||
use crate::errors::*;
|
use crate::errors::*;
|
||||||
use crate::clientinfo::{self, Client};
|
use crate::clientinfo::{self, Client};
|
||||||
use crate::credentials::Credentials;
|
use crate::credentials::{
|
||||||
use crate::ipc::{Approval, AwsRequestNotification};
|
AwsBaseCredential,
|
||||||
|
AwsSessionCredential,
|
||||||
|
};
|
||||||
|
use crate::ipc::{Approval, RequestNotification};
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
use crate::shortcuts::{self, ShortcutAction};
|
use crate::shortcuts::{self, ShortcutAction};
|
||||||
|
|
||||||
@ -26,6 +29,8 @@ pub use server_unix::Server;
|
|||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use server_unix::Stream;
|
use server_unix::Stream;
|
||||||
|
|
||||||
|
pub mod ssh_agent;
|
||||||
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub enum Request {
|
pub enum Request {
|
||||||
@ -38,7 +43,8 @@ pub enum Request {
|
|||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub enum Response {
|
pub enum Response {
|
||||||
Aws(Credentials),
|
AwsBase(AwsBaseCredential),
|
||||||
|
AwsSession(AwsSessionCredential),
|
||||||
Empty,
|
Empty,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,8 +131,8 @@ async fn get_aws_credentials(
|
|||||||
// but ? returns immediately, and we want to unregister the request before returning
|
// but ? returns immediately, and we want to unregister the request before returning
|
||||||
// so we bundle it all up in an async block and return a Result so we can handle errors
|
// so we bundle it all up in an async block and return a Result so we can handle errors
|
||||||
let proceed = async {
|
let proceed = async {
|
||||||
let notification = AwsRequestNotification {id: request_id, client, base};
|
let notification = RequestNotification::new_aws(request_id, client, base);
|
||||||
app_handle.emit("credentials-request", ¬ification)?;
|
app_handle.emit("credential-request", ¬ification)?;
|
||||||
|
|
||||||
let response = tokio::select! {
|
let response = tokio::select! {
|
||||||
r = chan_recv => r?,
|
r = chan_recv => r?,
|
||||||
@ -139,12 +145,12 @@ async fn get_aws_credentials(
|
|||||||
match response.approval {
|
match response.approval {
|
||||||
Approval::Approved => {
|
Approval::Approved => {
|
||||||
if response.base {
|
if response.base {
|
||||||
let creds = state.base_creds_cloned().await?;
|
let creds = state.get_aws_default().await?;
|
||||||
Ok(Response::Aws(Credentials::Base(creds)))
|
Ok(Response::AwsBase(creds))
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
let creds = state.session_creds_cloned().await?;
|
let creds = state.get_aws_default_session().await?;
|
||||||
Ok(Response::Aws(Credentials::Session(creds)))
|
Ok(Response::AwsSession(creds.clone()))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Approval::Denied => Err(HandlerError::Denied),
|
Approval::Denied => Err(HandlerError::Denied),
|
||||||
|
77
src-tauri/src/server/ssh_agent.rs
Normal file
77
src-tauri/src/server/ssh_agent.rs
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
use signature::Signer;
|
||||||
|
use ssh_agent_lib::agent::{Agent, Session};
|
||||||
|
use ssh_agent_lib::proto::message::Message;
|
||||||
|
use ssh_key::public::PublicKey;
|
||||||
|
use ssh_key::private::PrivateKey;
|
||||||
|
use tokio::net::UnixListener;
|
||||||
|
|
||||||
|
|
||||||
|
struct SshAgent;
|
||||||
|
|
||||||
|
impl std::default::Default for SshAgent {
|
||||||
|
fn default() -> Self {
|
||||||
|
SshAgent {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ssh_agent_lib::async_trait]
|
||||||
|
impl Session for SshAgent {
|
||||||
|
async fn handle(&mut self, message: Message) -> Result<Message, Box<dyn std::error::Error>> {
|
||||||
|
println!("Received message");
|
||||||
|
match message {
|
||||||
|
Message::RequestIdentities => {
|
||||||
|
let p = std::path::PathBuf::from("/home/joe/.ssh/id_ed25519.pub");
|
||||||
|
let pubkey = PublicKey::read_openssh_file(&p).unwrap();
|
||||||
|
let id = ssh_agent_lib::proto::message::Identity {
|
||||||
|
pubkey_blob: pubkey.to_bytes().unwrap(),
|
||||||
|
comment: pubkey.comment().to_owned(),
|
||||||
|
};
|
||||||
|
Ok(Message::IdentitiesAnswer(vec![id]))
|
||||||
|
},
|
||||||
|
Message::SignRequest(req) => {
|
||||||
|
println!("Received sign request");
|
||||||
|
let mut req_bytes = vec![13];
|
||||||
|
encode_string(&mut req_bytes, &req.pubkey_blob);
|
||||||
|
encode_string(&mut req_bytes, &req.data);
|
||||||
|
req_bytes.extend(req.flags.to_be_bytes());
|
||||||
|
std::fs::File::create("/tmp/signreq").unwrap().write(&req_bytes).unwrap();
|
||||||
|
|
||||||
|
let p = std::path::PathBuf::from("/home/joe/.ssh/id_ed25519");
|
||||||
|
let passphrase = std::env::var("PRIVKEY_PASSPHRASE").unwrap();
|
||||||
|
let privkey = PrivateKey::read_openssh_file(&p)
|
||||||
|
.unwrap()
|
||||||
|
.decrypt(passphrase.as_bytes())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let sig = Signer::sign(&privkey, &req.data);
|
||||||
|
use std::io::Write;
|
||||||
|
std::fs::File::create("/tmp/sig").unwrap().write(sig.as_bytes()).unwrap();
|
||||||
|
|
||||||
|
let mut payload = Vec::with_capacity(128);
|
||||||
|
encode_string(&mut payload, "ssh-ed25519".as_bytes());
|
||||||
|
encode_string(&mut payload, sig.as_bytes());
|
||||||
|
println!("Payload length: {}", payload.len());
|
||||||
|
std::fs::File::create("/tmp/payload").unwrap().write(&payload).unwrap();
|
||||||
|
Ok(Message::SignResponse(payload))
|
||||||
|
},
|
||||||
|
_ => Ok(Message::Failure),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fn encode_string(buf: &mut Vec<u8>, s: &[u8]) {
|
||||||
|
let len = s.len() as u32;
|
||||||
|
buf.extend(len.to_be_bytes());
|
||||||
|
buf.extend(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn run() {
|
||||||
|
let socket = "/tmp/creddy-agent.sock";
|
||||||
|
let _ = std::fs::remove_file(socket);
|
||||||
|
let listener = UnixListener::bind(socket).unwrap();
|
||||||
|
SshAgent.listen(listener).await.unwrap();
|
||||||
|
}
|
@ -1,12 +1,14 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::collections::hash_map::Entry;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
use tokio::{
|
use tokio::{
|
||||||
sync::RwLock,
|
sync::{RwLock, RwLockReadGuard},
|
||||||
sync::oneshot::{self, Sender},
|
sync::oneshot::{self, Sender},
|
||||||
};
|
};
|
||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
|
use sqlx::types::Uuid;
|
||||||
use tauri::{
|
use tauri::{
|
||||||
Manager,
|
Manager,
|
||||||
async_runtime as rt,
|
async_runtime as rt,
|
||||||
@ -14,12 +16,17 @@ use tauri::{
|
|||||||
|
|
||||||
use crate::app;
|
use crate::app;
|
||||||
use crate::credentials::{
|
use crate::credentials::{
|
||||||
Session,
|
AppSession,
|
||||||
BaseCredentials,
|
AwsSessionCredential,
|
||||||
SessionCredentials,
|
|
||||||
};
|
};
|
||||||
use crate::{config, config::AppConfig};
|
use crate::{config, config::AppConfig};
|
||||||
use crate::ipc::{self, Approval, RequestResponse};
|
use crate::credentials::{
|
||||||
|
AwsBaseCredential,
|
||||||
|
Credential,
|
||||||
|
CredentialRecord,
|
||||||
|
PersistentCredential
|
||||||
|
};
|
||||||
|
use crate::ipc::{self, RequestResponse};
|
||||||
use crate::errors::*;
|
use crate::errors::*;
|
||||||
use crate::shortcuts;
|
use crate::shortcuts;
|
||||||
|
|
||||||
@ -101,7 +108,9 @@ impl VisibilityLease {
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub config: RwLock<AppConfig>,
|
pub config: RwLock<AppConfig>,
|
||||||
pub session: RwLock<Session>,
|
pub app_session: RwLock<AppSession>,
|
||||||
|
// session cache is keyed on id rather than name because names can change
|
||||||
|
pub aws_sessions: RwLock<HashMap<Uuid, AwsSessionCredential>>,
|
||||||
pub last_activity: RwLock<OffsetDateTime>,
|
pub last_activity: RwLock<OffsetDateTime>,
|
||||||
pub request_count: RwLock<u64>,
|
pub request_count: RwLock<u64>,
|
||||||
pub waiting_requests: RwLock<HashMap<u64, Sender<RequestResponse>>>,
|
pub waiting_requests: RwLock<HashMap<u64, Sender<RequestResponse>>>,
|
||||||
@ -116,14 +125,15 @@ pub struct AppState {
|
|||||||
impl AppState {
|
impl AppState {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
config: AppConfig,
|
config: AppConfig,
|
||||||
session: Session,
|
app_session: AppSession,
|
||||||
pool: SqlitePool,
|
pool: SqlitePool,
|
||||||
setup_errors: Vec<String>,
|
setup_errors: Vec<String>,
|
||||||
desktop_is_gnome: bool,
|
desktop_is_gnome: bool,
|
||||||
) -> AppState {
|
) -> AppState {
|
||||||
AppState {
|
AppState {
|
||||||
config: RwLock::new(config),
|
config: RwLock::new(config),
|
||||||
session: RwLock::new(session),
|
app_session: RwLock::new(app_session),
|
||||||
|
aws_sessions: RwLock::new(HashMap::new()),
|
||||||
last_activity: RwLock::new(OffsetDateTime::now_utc()),
|
last_activity: RwLock::new(OffsetDateTime::now_utc()),
|
||||||
request_count: RwLock::new(0),
|
request_count: RwLock::new(0),
|
||||||
waiting_requests: RwLock::new(HashMap::new()),
|
waiting_requests: RwLock::new(HashMap::new()),
|
||||||
@ -135,12 +145,43 @@ impl AppState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn new_creds(&self, base_creds: BaseCredentials, passphrase: &str) -> Result<(), UnlockError> {
|
pub async fn save_credential(&self, record: CredentialRecord) -> Result<(), SaveCredentialsError> {
|
||||||
let locked = base_creds.encrypt(passphrase)?;
|
let session = self.app_session.read().await;
|
||||||
// do this first so that if it fails we don't save bad credentials
|
let crypto = session.try_get_crypto()?;
|
||||||
self.new_session(base_creds).await?;
|
record.save(crypto, &self.pool).await
|
||||||
locked.save(&self.pool).await?;
|
}
|
||||||
|
|
||||||
|
pub async fn delete_credential(&self, id: &Uuid) -> Result<(), SaveCredentialsError> {
|
||||||
|
sqlx::query!("DELETE FROM credentials WHERE id = ?", id)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_credentials(&self) -> Result<Vec<CredentialRecord>, GetCredentialsError> {
|
||||||
|
let session = self.app_session.read().await;
|
||||||
|
let crypto = session.try_get_crypto()?;
|
||||||
|
let list = CredentialRecord::list(crypto, &self.pool).await?;
|
||||||
|
Ok(list)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_passphrase(&self, passphrase: &str) -> Result<(), SaveCredentialsError> {
|
||||||
|
let mut cur_session = self.app_session.write().await;
|
||||||
|
if let AppSession::Locked {..} = *cur_session {
|
||||||
|
return Err(SaveCredentialsError::Locked);
|
||||||
|
}
|
||||||
|
|
||||||
|
let new_session = AppSession::new(passphrase)?;
|
||||||
|
if let AppSession::Unlocked {salt: _, ref crypto} = *cur_session {
|
||||||
|
CredentialRecord::rekey(
|
||||||
|
crypto,
|
||||||
|
new_session.try_get_crypto().expect("AppSession::new() should always return Unlocked"),
|
||||||
|
&self.pool,
|
||||||
|
).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
new_session.save(&self.pool).await?;
|
||||||
|
*cur_session = new_session;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -187,11 +228,6 @@ 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 {
|
|
||||||
let mut session = self.session.write().await;
|
|
||||||
session.renew_if_expired().await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut waiting_requests = self.waiting_requests.write().await;
|
let mut waiting_requests = self.waiting_requests.write().await;
|
||||||
waiting_requests
|
waiting_requests
|
||||||
.remove(&response.id)
|
.remove(&response.id)
|
||||||
@ -201,24 +237,17 @@ 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.app_session.write().await;
|
||||||
Session::Empty => {return Err(UnlockError::NoCredentials);},
|
session.unlock(passphrase)
|
||||||
Session::Unlocked{..} => {return Err(UnlockError::NotLocked);},
|
|
||||||
Session::Locked(ref locked) => locked.decrypt(passphrase)?,
|
|
||||||
};
|
|
||||||
// Read lock is dropped here, so this doesn't deadlock
|
|
||||||
self.new_session(base_creds).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn lock(&self) -> Result<(), LockError> {
|
pub async fn lock(&self) -> Result<(), LockError> {
|
||||||
let mut session = self.session.write().await;
|
let mut session = self.app_session.write().await;
|
||||||
match *session {
|
match *session {
|
||||||
Session::Empty => Err(LockError::NotUnlocked),
|
AppSession::Empty => Err(LockError::NotUnlocked),
|
||||||
Session::Locked(_) => Err(LockError::NotUnlocked),
|
AppSession::Locked{..} => Err(LockError::NotUnlocked),
|
||||||
Session::Unlocked{..} => {
|
AppSession::Unlocked{..} => {
|
||||||
*session = Session::load(&self.pool).await?;
|
*session = AppSession::load(&self.pool).await?;
|
||||||
|
|
||||||
let app_handle = app::APP.get().unwrap();
|
let app_handle = app::APP.get().unwrap();
|
||||||
app_handle.emit("locked", None::<usize>)?;
|
app_handle.emit("locked", None::<usize>)?;
|
||||||
@ -228,6 +257,51 @@ impl AppState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn reset_session(&self) -> Result<(), SaveCredentialsError> {
|
||||||
|
let mut session = self.app_session.write().await;
|
||||||
|
session.reset(&self.pool).await?;
|
||||||
|
sqlx::query!("DELETE FROM credentials").execute(&self.pool).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_aws_default(&self) -> Result<AwsBaseCredential, GetCredentialsError> {
|
||||||
|
let app_session = self.app_session.read().await;
|
||||||
|
let crypto = app_session.try_get_crypto()?;
|
||||||
|
let record = CredentialRecord::load_default("aws", crypto, &self.pool).await?;
|
||||||
|
let creds = match record.credential {
|
||||||
|
Credential::AwsBase(b) => Ok(b),
|
||||||
|
_ => Err(LoadCredentialsError::NoCredentials)
|
||||||
|
}?;
|
||||||
|
Ok(creds)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_aws_default_session(&self) -> Result<RwLockReadGuard<'_, AwsSessionCredential>, GetCredentialsError> {
|
||||||
|
let app_session = self.app_session.read().await;
|
||||||
|
let crypto = app_session.try_get_crypto()?;
|
||||||
|
let record = CredentialRecord::load_default("aws", crypto, &self.pool).await?;
|
||||||
|
let base = match &record.credential {
|
||||||
|
Credential::AwsBase(b) => Ok(b),
|
||||||
|
_ => Err(LoadCredentialsError::NoCredentials)
|
||||||
|
}?;
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut aws_sessions = self.aws_sessions.write().await;
|
||||||
|
match aws_sessions.entry(record.id) {
|
||||||
|
Entry::Vacant(e) => {
|
||||||
|
e.insert(AwsSessionCredential::from_base(&base).await?);
|
||||||
|
},
|
||||||
|
Entry::Occupied(mut e) if e.get().is_expired() => {
|
||||||
|
*(e.get_mut()) = AwsSessionCredential::from_base(&base).await?;
|
||||||
|
},
|
||||||
|
_ => ()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// we know the unwrap is safe, because we just made sure of it
|
||||||
|
let s = RwLockReadGuard::map(self.aws_sessions.read().await, |map| map.get(&record.id).unwrap());
|
||||||
|
Ok(s)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn signal_activity(&self) {
|
pub async fn signal_activity(&self) {
|
||||||
let mut last_activity = self.last_activity.write().await;
|
let mut last_activity = self.last_activity.write().await;
|
||||||
*last_activity = OffsetDateTime::now_utc();
|
*last_activity = OffsetDateTime::now_utc();
|
||||||
@ -235,7 +309,7 @@ impl AppState {
|
|||||||
|
|
||||||
pub async fn should_auto_lock(&self) -> bool {
|
pub async fn should_auto_lock(&self) -> bool {
|
||||||
let config = self.config.read().await;
|
let config = self.config.read().await;
|
||||||
if !config.auto_lock || !self.is_unlocked().await {
|
if !config.auto_lock || self.is_locked().await {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -244,28 +318,9 @@ impl AppState {
|
|||||||
elapsed >= config.lock_after
|
elapsed >= config.lock_after
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn is_unlocked(&self) -> bool {
|
pub async fn is_locked(&self) -> bool {
|
||||||
let session = self.session.read().await;
|
let session = self.app_session.read().await;
|
||||||
matches!(*session, Session::Unlocked{..})
|
matches!(*session, AppSession::Locked {..})
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn base_creds_cloned(&self) -> Result<BaseCredentials, GetCredentialsError> {
|
|
||||||
let app_session = self.session.read().await;
|
|
||||||
let (base, _session) = app_session.try_get()?;
|
|
||||||
Ok(base.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn session_creds_cloned(&self) -> Result<SessionCredentials, GetCredentialsError> {
|
|
||||||
let app_session = self.session.read().await;
|
|
||||||
let (_base, session) = app_session.try_get()?;
|
|
||||||
Ok(session.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn new_session(&self, base: BaseCredentials) -> Result<(), GetSessionError> {
|
|
||||||
let session = SessionCredentials::from_base(&base).await?;
|
|
||||||
let mut app_session = self.session.write().await;
|
|
||||||
*app_session = Session::Unlocked {base, session};
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn register_terminal_request(&self) -> Result<(), ()> {
|
pub async fn register_terminal_request(&self) -> Result<(), ()> {
|
||||||
@ -285,3 +340,36 @@ impl AppState {
|
|||||||
*req = false;
|
*req = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::credentials::Crypto;
|
||||||
|
use sqlx::types::Uuid;
|
||||||
|
|
||||||
|
|
||||||
|
fn test_state(pool: SqlitePool) -> AppState {
|
||||||
|
let salt = [0u8; 32];
|
||||||
|
let crypto = Crypto::fixed();
|
||||||
|
AppState::new(
|
||||||
|
AppConfig::default(),
|
||||||
|
AppSession::Unlocked { salt, crypto },
|
||||||
|
pool,
|
||||||
|
vec![],
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[sqlx::test(fixtures("./credentials/fixtures/aws_credentials.sql"))]
|
||||||
|
fn test_delete_credential(pool: SqlitePool) {
|
||||||
|
let state = test_state(pool);
|
||||||
|
let id = Uuid::try_parse("00000000-0000-0000-0000-000000000000").unwrap();
|
||||||
|
state.delete_credential(&id).await.unwrap();
|
||||||
|
|
||||||
|
// ensure delete-cascade went through correctly
|
||||||
|
let res = AwsBaseCredential::load(&id, &Crypto::fixed(), &state.pool).await;
|
||||||
|
assert!(matches!(res, Err(LoadCredentialsError::NoCredentials)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use tauri::Manager;
|
use tauri::{AppHandle, Manager};
|
||||||
|
use tokio::time::sleep;
|
||||||
|
|
||||||
use crate::app::APP;
|
use crate::app::APP;
|
||||||
use crate::errors::*;
|
use crate::errors::*;
|
||||||
@ -16,6 +18,18 @@ pub async fn launch(use_base: bool) -> Result<(), LaunchTerminalError> {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let res = do_launch(app, use_base).await;
|
||||||
|
|
||||||
|
state.unregister_terminal_request().await;
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// this handles most of the work, the outer function is just to ensure we properly
|
||||||
|
// unregister the request if there's an error
|
||||||
|
async fn do_launch(app: &AppHandle, use_base: bool) -> Result<(), LaunchTerminalError> {
|
||||||
|
let state = app.state::<AppState>();
|
||||||
|
|
||||||
let mut cmd = {
|
let mut cmd = {
|
||||||
let config = state.config.read().await;
|
let config = state.config.read().await;
|
||||||
let mut cmd = Command::new(&config.terminal.exec);
|
let mut cmd = Command::new(&config.terminal.exec);
|
||||||
@ -23,56 +37,50 @@ pub async fn launch(use_base: bool) -> Result<(), LaunchTerminalError> {
|
|||||||
cmd
|
cmd
|
||||||
};
|
};
|
||||||
|
|
||||||
// if session is locked or empty, wait for credentials from frontend
|
// if session is locked, wait for credentials from frontend
|
||||||
if !state.is_unlocked().await {
|
if state.is_locked().await {
|
||||||
app.emit("launch-terminal-request", ())?;
|
|
||||||
let lease = state.acquire_visibility_lease(0).await
|
let lease = state.acquire_visibility_lease(0).await
|
||||||
.map_err(|_e| LaunchTerminalError::NoMainWindow)?; // automate conversion eventually?
|
.map_err(|_e| LaunchTerminalError::NoMainWindow)?; // automate conversion eventually?
|
||||||
|
|
||||||
let (tx, rx) = tokio::sync::oneshot::channel();
|
let (tx, rx) = tokio::sync::oneshot::channel();
|
||||||
app.once("credentials-event", move |e| {
|
app.once("unlocked", move |_| {
|
||||||
let success = match e.payload() {
|
let _ = tx.send(());
|
||||||
"\"unlocked\"" | "\"entered\"" => true,
|
|
||||||
_ => false,
|
|
||||||
};
|
|
||||||
let _ = tx.send(success);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if !rx.await.unwrap_or(false) {
|
let timeout = Duration::from_secs(60);
|
||||||
state.unregister_terminal_request().await;
|
tokio::select! {
|
||||||
return Ok(()); // request was canceled by user
|
// if the frontend is unlocked within 60 seconds, release visibility lock and proceed
|
||||||
}
|
_ = rx => lease.release(),
|
||||||
lease.release();
|
// otherwise, dump this request, but return Ok so we don't get an error popup
|
||||||
}
|
_ = sleep(timeout) => {
|
||||||
|
eprintln!("WARNING: Request to launch terminal timed out after 60 seconds.");
|
||||||
// more lock-management
|
return Ok(());
|
||||||
{
|
},
|
||||||
let app_session = state.session.read().await;
|
|
||||||
// session should really be unlocked at this point, but if the frontend misbehaves
|
|
||||||
// (i.e. lies about unlocking) we could end up here with a locked session
|
|
||||||
// this will result in an error popup to the user (see main hotkey handler)
|
|
||||||
let (base_creds, session_creds) = app_session.try_get()?;
|
|
||||||
if use_base {
|
|
||||||
cmd.env("AWS_ACCESS_KEY_ID", &base_creds.access_key_id);
|
|
||||||
cmd.env("AWS_SECRET_ACCESS_KEY", &base_creds.secret_access_key);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
cmd.env("AWS_ACCESS_KEY_ID", &session_creds.access_key_id);
|
|
||||||
cmd.env("AWS_SECRET_ACCESS_KEY", &session_creds.secret_access_key);
|
|
||||||
cmd.env("AWS_SESSION_TOKEN", &session_creds.session_token);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let res = match cmd.spawn() {
|
// session should really be unlocked at this point, but if the frontend misbehaves
|
||||||
|
// (i.e. lies about unlocking) we could end up here with a locked session
|
||||||
|
// this will result in an error popup to the user (see main hotkey handler)
|
||||||
|
if use_base {
|
||||||
|
let base_creds = state.get_aws_default().await?;
|
||||||
|
cmd.env("AWS_ACCESS_KEY_ID", &base_creds.access_key_id);
|
||||||
|
cmd.env("AWS_SECRET_ACCESS_KEY", &base_creds.secret_access_key);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let session_creds = state.get_aws_default_session().await?;
|
||||||
|
cmd.env("AWS_ACCESS_KEY_ID", &session_creds.access_key_id);
|
||||||
|
cmd.env("AWS_SECRET_ACCESS_KEY", &session_creds.secret_access_key);
|
||||||
|
cmd.env("AWS_SESSION_TOKEN", &session_creds.session_token);
|
||||||
|
}
|
||||||
|
|
||||||
|
match cmd.spawn() {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(e) if std::io::ErrorKind::NotFound == e.kind() => {
|
Err(e) if std::io::ErrorKind::NotFound == e.kind() => {
|
||||||
Err(ExecError::NotFound(cmd.get_program().to_owned()))
|
Err(ExecError::NotFound(cmd.get_program().to_owned()))
|
||||||
},
|
},
|
||||||
Err(e) => Err(ExecError::ExecutionFailed(e)),
|
Err(e) => Err(ExecError::ExecutionFailed(e)),
|
||||||
};
|
}?;
|
||||||
|
|
||||||
state.unregister_terminal_request().await;
|
|
||||||
|
|
||||||
res?; // ? auto-conversion is more liberal than .into()
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -7,15 +7,22 @@ import { getVersion } from '@tauri-apps/api/app';
|
|||||||
import { appState, acceptRequest, cleanupRequest } from './lib/state.js';
|
import { appState, acceptRequest, cleanupRequest } from './lib/state.js';
|
||||||
import { views, currentView, navigate } from './lib/routing.js';
|
import { views, currentView, navigate } from './lib/routing.js';
|
||||||
|
|
||||||
|
import Approve from './views/Approve.svelte';
|
||||||
|
import CreatePassphrase from './views/CreatePassphrase.svelte';
|
||||||
|
import Unlock from './views/Unlock.svelte';
|
||||||
|
|
||||||
$views = import.meta.glob('./views/*.svelte', {eager: true});
|
// set up app state
|
||||||
navigate('Home');
|
|
||||||
|
|
||||||
invoke('get_config').then(config => $appState.config = config);
|
invoke('get_config').then(config => $appState.config = config);
|
||||||
invoke('get_session_status').then(status => $appState.credentialStatus = status);
|
invoke('get_session_status').then(status => $appState.sessionStatus = status);
|
||||||
getVersion().then(version => $appState.appVersion = version);
|
getVersion().then(version => $appState.appVersion = version);
|
||||||
|
invoke('get_setup_errors')
|
||||||
|
.then(errs => {
|
||||||
|
$appState.setupErrors = errs.map(e => ({msg: e, show: true}));
|
||||||
|
});
|
||||||
|
|
||||||
listen('credentials-request', (tauriEvent) => {
|
|
||||||
|
// set up event handlers
|
||||||
|
listen('credential-request', (tauriEvent) => {
|
||||||
$appState.pendingRequests.put(tauriEvent.payload);
|
$appState.pendingRequests.put(tauriEvent.payload);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -29,29 +36,17 @@ listen('request-cancelled', (tauriEvent) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
listen('launch-terminal-request', async (tauriEvent) => {
|
|
||||||
if ($appState.currentRequest === null) {
|
|
||||||
let status = await invoke('get_session_status');
|
|
||||||
if (status === 'locked') {
|
|
||||||
navigate('Unlock');
|
|
||||||
}
|
|
||||||
else if (status === 'empty') {
|
|
||||||
navigate('EnterCredentials');
|
|
||||||
}
|
|
||||||
// else, session is unlocked, so do nothing
|
|
||||||
// (although we shouldn't even get the event in that case)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
listen('locked', () => {
|
listen('locked', () => {
|
||||||
$appState.credentialStatus = 'locked';
|
$appState.sessionStatus = 'locked';
|
||||||
});
|
});
|
||||||
|
|
||||||
invoke('get_setup_errors')
|
|
||||||
.then(errs => {
|
|
||||||
$appState.setupErrors = errs.map(e => ({msg: e, show: true}));
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// set up navigation
|
||||||
|
$views = import.meta.glob('./views/*.svelte', {eager: true});
|
||||||
|
navigate('Home');
|
||||||
|
|
||||||
|
|
||||||
|
// ready to rock and roll
|
||||||
acceptRequest();
|
acceptRequest();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -61,4 +56,17 @@ acceptRequest();
|
|||||||
on:keydown={() => invoke('signal_activity')}
|
on:keydown={() => invoke('signal_activity')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<svelte:component this="{$currentView}" />
|
|
||||||
|
{#if $appState.sessionStatus === 'empty'}
|
||||||
|
<!-- Empty state (no passphrase) takes precedence over everything -->
|
||||||
|
<CreatePassphrase />
|
||||||
|
{:else if $appState.currentRequest !== null}
|
||||||
|
<!-- if a request is pending, show approval flow (will include unlock if necessary) -->
|
||||||
|
<Approve />
|
||||||
|
{:else if $appState.sessionStatus === 'locked'}
|
||||||
|
<!-- if session is locked and no request is pending, show unlock screen -->
|
||||||
|
<Unlock />
|
||||||
|
{:else}
|
||||||
|
<!-- normal operation -->
|
||||||
|
<svelte:component this="{$currentView}" />
|
||||||
|
{/if}
|
||||||
|
@ -7,7 +7,7 @@ import { navigate, currentView, previousView } from './routing.js';
|
|||||||
export let appState = writable({
|
export let appState = writable({
|
||||||
currentRequest: null,
|
currentRequest: null,
|
||||||
pendingRequests: queue(),
|
pendingRequests: queue(),
|
||||||
credentialStatus: 'locked',
|
sessionStatus: 'locked',
|
||||||
setupErrors: [],
|
setupErrors: [],
|
||||||
appVersion: '',
|
appVersion: '',
|
||||||
});
|
});
|
||||||
@ -25,11 +25,11 @@ export async function acceptRequest() {
|
|||||||
|
|
||||||
|
|
||||||
export function cleanupRequest() {
|
export function cleanupRequest() {
|
||||||
|
currentView.set(get(previousView));
|
||||||
|
previousView.set(null);
|
||||||
appState.update($appState => {
|
appState.update($appState => {
|
||||||
$appState.currentRequest = null;
|
$appState.currentRequest = null;
|
||||||
return $appState;
|
return $appState;
|
||||||
});
|
});
|
||||||
currentView.set(get(previousView));
|
|
||||||
previousView.set(null);
|
|
||||||
acceptRequest();
|
acceptRequest();
|
||||||
}
|
}
|
||||||
|
@ -7,11 +7,34 @@
|
|||||||
export let slideDuration = 150;
|
export let slideDuration = 150;
|
||||||
let animationClass = "";
|
let animationClass = "";
|
||||||
|
|
||||||
export function shake() {
|
let error = null;
|
||||||
|
|
||||||
|
function shake() {
|
||||||
animationClass = 'shake';
|
animationClass = 'shake';
|
||||||
window.setTimeout(() => animationClass = "", 400);
|
window.setTimeout(() => animationClass = "", 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function run(fallible) {
|
||||||
|
try {
|
||||||
|
const ret = await Promise.resolve(fallible());
|
||||||
|
error = null;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
if (error) shake();
|
||||||
|
error = e;
|
||||||
|
// re-throw so it can be caught by the caller if necessary
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is a method rather than a prop so that we can re-shake every time
|
||||||
|
// the error occurs, even if the error message doesn't change
|
||||||
|
export function setError(e) {
|
||||||
|
if (error) shake();
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
@ -51,17 +74,17 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
<div in:slide="{{duration: slideDuration}}" class="alert alert-error shadow-lg {animationClass} {extraClasses}">
|
{#if error}
|
||||||
<div>
|
<div transition:slide="{{duration: slideDuration}}" class="alert alert-error shadow-lg {animationClass} {extraClasses}">
|
||||||
<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="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
<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="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
||||||
<span>
|
<span>
|
||||||
<slot></slot>
|
<slot {error}>{error.msg || error}</slot>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if $$slots.buttons}
|
{#if $$slots.buttons}
|
||||||
<div>
|
<div>
|
||||||
<slot name="buttons"></slot>
|
<slot name="buttons"></slot>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
let classes = "";
|
let classes = "";
|
||||||
export {classes as class};
|
export {classes as class};
|
||||||
|
|
||||||
let svg = ICONS[`./icons/${name}.svelte`].default;
|
$: svg = ICONS[`./icons/${name}.svelte`].default;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:component this={svg} class={classes} />
|
<svelte:component this={svg} class={classes} />
|
@ -31,6 +31,7 @@
|
|||||||
&& shift === event.shiftKey
|
&& shift === event.shiftKey
|
||||||
) {
|
) {
|
||||||
click();
|
click();
|
||||||
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
46
src/ui/PassphraseInput.svelte
Normal file
46
src/ui/PassphraseInput.svelte
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<script>
|
||||||
|
import Icon from './Icon.svelte';
|
||||||
|
|
||||||
|
export let value = '';
|
||||||
|
export let placeholder = '';
|
||||||
|
export let autofocus = false;
|
||||||
|
let classes = '';
|
||||||
|
export {classes as class};
|
||||||
|
|
||||||
|
let show = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<style>
|
||||||
|
button {
|
||||||
|
border: 1px solid oklch(var(--bc) / 0.2);
|
||||||
|
border-left: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="join w-full">
|
||||||
|
<input
|
||||||
|
type={show ? 'text' : 'password'}
|
||||||
|
{value} {placeholder} {autofocus}
|
||||||
|
on:input={e => value = e.target.value}
|
||||||
|
on:input on:change on:focus on:blur
|
||||||
|
class="input input-bordered flex-grow join-item placeholder:text-gray-500 {classes}"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-ghost join-item swap swap-rotate"
|
||||||
|
class:swap-active={show}
|
||||||
|
on:click={() => show = !show}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name="eye"
|
||||||
|
class="w-5 h-5 swap-off"
|
||||||
|
/>
|
||||||
|
<Icon
|
||||||
|
name="eye-slash"
|
||||||
|
class="w-5 h-5 swap-on"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
8
src/ui/icons/arrow-right-start-on-rectangle.svelte
Normal file
8
src/ui/icons/arrow-right-start-on-rectangle.svelte
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<script>
|
||||||
|
let classes = '';
|
||||||
|
export {classes as class}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class={classes}>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0 0 13.5 3h-6a2.25 2.25 0 0 0-2.25 2.25v13.5A2.25 2.25 0 0 0 7.5 21h6a2.25 2.25 0 0 0 2.25-2.25V15m3 0 3-3m0 0-3-3m3 3H9" />
|
||||||
|
</svg>
|
8
src/ui/icons/command-line.svelte
Normal file
8
src/ui/icons/command-line.svelte
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<script>
|
||||||
|
let classes = '';
|
||||||
|
export {classes as class}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class={classes}>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="m6.75 7.5 3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0 0 21 18V6a2.25 2.25 0 0 0-2.25-2.25H5.25A2.25 2.25 0 0 0 3 6v12a2.25 2.25 0 0 0 2.25 2.25Z" />
|
||||||
|
</svg>
|
9
src/ui/icons/eye-slash.svelte
Normal file
9
src/ui/icons/eye-slash.svelte
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<script>
|
||||||
|
let classes = "";
|
||||||
|
export {classes as class};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class={classes}>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M3.98 8.223A10.477 10.477 0 0 0 1.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.451 10.451 0 0 1 12 4.5c4.756 0 8.773 3.162 10.065 7.498a10.522 10.522 0 0 1-4.293 5.774M6.228 6.228 3 3m3.228 3.228 3.65 3.65m7.894 7.894L21 21m-3.228-3.228-3.65-3.65m0 0a3 3 0 1 0-4.243-4.243m4.242 4.242L9.88 9.88" />
|
||||||
|
</svg>
|
||||||
|
|
9
src/ui/icons/eye.svelte
Normal file
9
src/ui/icons/eye.svelte
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<script>
|
||||||
|
let classes = "";
|
||||||
|
export {classes as class};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class={classes}>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" />
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
|
||||||
|
</svg>
|
8
src/ui/icons/key.svelte
Normal file
8
src/ui/icons/key.svelte
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<script>
|
||||||
|
let classes = '';
|
||||||
|
export {classes as class}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class={classes}>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25a3 3 0 0 1 3 3m3 0a6 6 0 0 1-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1 1 21.75 8.25Z" />
|
||||||
|
</svg>
|
8
src/ui/icons/pencil.svelte
Normal file
8
src/ui/icons/pencil.svelte
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<script>
|
||||||
|
let classes = '';
|
||||||
|
export {classes as class};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class={classes}>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125" />
|
||||||
|
</svg>
|
9
src/ui/icons/plus-circle-mini.svelte
Normal file
9
src/ui/icons/plus-circle-mini.svelte
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<script>
|
||||||
|
let classes = "";
|
||||||
|
export {classes as class};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class={classes}>
|
||||||
|
<path fill-rule="evenodd" d="M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16Zm.75-11.25a.75.75 0 0 0-1.5 0v2.5h-2.5a.75.75 0 0 0 0 1.5h2.5v2.5a.75.75 0 0 0 1.5 0v-2.5h2.5a.75.75 0 0 0 0-1.5h-2.5v-2.5Z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
|
8
src/ui/icons/shield-check.svelte
Normal file
8
src/ui/icons/shield-check.svelte
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<script>
|
||||||
|
let classes = '';
|
||||||
|
export {classes as class}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class={classes}>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75m-3-7.036A11.959 11.959 0 0 1 3.598 6 11.99 11.99 0 0 0 3 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285Z" />
|
||||||
|
</svg>
|
8
src/ui/icons/trash.svelte
Normal file
8
src/ui/icons/trash.svelte
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<script>
|
||||||
|
let classes = '';
|
||||||
|
export {classes as class};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class={classes}>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
|
||||||
|
</svg>
|
@ -7,6 +7,13 @@
|
|||||||
export let value;
|
export let value;
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
async function pickFile() {
|
||||||
|
let file = await open();
|
||||||
|
if (file) {
|
||||||
|
value = file.path
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
@ -18,9 +25,10 @@
|
|||||||
bind:value
|
bind:value
|
||||||
on:change={() => dispatch('update', {value})}
|
on:change={() => dispatch('update', {value})}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
class="btn btn-sm btn-primary"
|
class="btn btn-sm btn-primary"
|
||||||
on:click={async () => value = await open()}
|
on:click={pickFile}
|
||||||
>Browse</button>
|
>Browse</button>
|
||||||
</div>
|
</div>
|
||||||
<slot name="description" slot="description"></slot>
|
<slot name="description" slot="description"></slot>
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
<script>
|
<script>
|
||||||
import { slide } from 'svelte/transition';
|
import { slide } from 'svelte/transition';
|
||||||
import ErrorAlert from '../ErrorAlert.svelte';
|
|
||||||
|
|
||||||
export let title;
|
export let title;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="flex flex-wrap justify-between gap-y-4">
|
<div class="flex flex-wrap justify-between gap-4">
|
||||||
<h3 class="text-lg font-bold shrink-0">{title}</h3>
|
<h3 class="text-lg font-bold shrink-0">{title}</h3>
|
||||||
{#if $$slots.input}
|
{#if $$slots.input}
|
||||||
<slot name="input"></slot>
|
<slot name="input"></slot>
|
||||||
|
@ -1,145 +1,64 @@
|
|||||||
<script>
|
<script>
|
||||||
import { onMount } from 'svelte';
|
import { appState, cleanupRequest } from '../lib/state.js';
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
|
||||||
import { navigate } from '../lib/routing.js';
|
|
||||||
import { appState, cleanupRequest } from '../lib/state.js';
|
|
||||||
import ErrorAlert from '../ui/ErrorAlert.svelte';
|
import ErrorAlert from '../ui/ErrorAlert.svelte';
|
||||||
import Link from '../ui/Link.svelte';
|
import CollectResponse from './approve/CollectResponse.svelte';
|
||||||
import KeyCombo from '../ui/KeyCombo.svelte';
|
import ShowResponse from './approve/ShowResponse.svelte';
|
||||||
|
import Unlock from './Unlock.svelte';
|
||||||
|
|
||||||
|
|
||||||
// Send response to backend, display error if applicable
|
// Extra 50ms so the window can finish disappearing before the redraw
|
||||||
let error, alert;
|
const rehideDelay = Math.min(5000, $appState.config.rehide_ms + 50);
|
||||||
async function respond() {
|
|
||||||
const response = {
|
let alert;
|
||||||
id: $appState.currentRequest.id,
|
let success = false;
|
||||||
...$appState.currentRequest.response,
|
async function sendResponse() {
|
||||||
};
|
|
||||||
try {
|
try {
|
||||||
await invoke('respond', {response});
|
await invoke('respond', {response: $appState.currentRequest.response});
|
||||||
navigate('ShowResponse');
|
success = true;
|
||||||
|
window.setTimeout(cleanupRequest, rehideDelay);
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
if (error) {
|
// reset to null so that we go back to asking for approval
|
||||||
alert.shake();
|
$appState.currentRequest.response = null;
|
||||||
}
|
// setTimeout forces this to not happen until the alert has been rendered
|
||||||
error = e;
|
window.setTimeout(() => alert.setError(e), 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Approval has one of several outcomes depending on current credential state
|
async function handleResponseCollected() {
|
||||||
async function approve(base) {
|
if (
|
||||||
$appState.currentRequest.response = {approval: 'Approved', base};
|
$appState.sessionStatus === 'unlocked'
|
||||||
let status = await invoke('get_session_status');
|
|| $appState.currentRequest.response.approval === 'Denied'
|
||||||
if (status === 'unlocked') {
|
) {
|
||||||
await respond();
|
await sendResponse();
|
||||||
}
|
|
||||||
else if (status === 'locked') {
|
|
||||||
navigate('Unlock');
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
navigate('EnterCredentials');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Denial has only one
|
|
||||||
async function deny() {
|
|
||||||
$appState.currentRequest.response = {approval: 'Denied', base: false};
|
|
||||||
await respond();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract executable name from full path
|
|
||||||
const client = $appState.currentRequest.client;
|
|
||||||
const m = client.exe?.match(/\/([^/]+?$)|\\([^\\]+?$)/);
|
|
||||||
const appName = m[1] || m[2];
|
|
||||||
|
|
||||||
// Executable paths can be long, so ensure they only break on \ or /
|
|
||||||
function breakPath(path) {
|
|
||||||
return path.replace(/(\\|\/)/g, '$1<wbr>');
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the request has already been approved/denied, send response immediately
|
|
||||||
onMount(async () => {
|
|
||||||
if ($appState.currentRequest.response) {
|
|
||||||
await respond();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<!-- Don't render at all if we're just going to immediately proceed to the next screen -->
|
{#if success}
|
||||||
{#if error || !$appState.currentRequest?.response}
|
<!-- if we have successfully sent a response, show it -->
|
||||||
|
<ShowResponse />
|
||||||
|
{:else if !$appState.currentRequest?.response}
|
||||||
|
<!-- if a response hasn't been collected, ask for it -->
|
||||||
<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}
|
<ErrorAlert bind:this={alert}>
|
||||||
<ErrorAlert bind:this={alert}>
|
<svelte:fragment slot="buttons">
|
||||||
{error.msg}
|
<button class="btn btn-sm btn-alert-error" on:click={cleanupRequest}>Cancel</button>
|
||||||
<svelte:fragment slot="buttons">
|
<button class="btn btn-sm btn-alert-error" on:click={sendResponse}>Retry</button>
|
||||||
<button class="btn btn-sm btn-alert-error" on:click={cleanupRequest}>Cancel</button>
|
</svelte:fragment>
|
||||||
<button class="btn btn-sm btn-alert-error" on:click={respond}>Retry</button>
|
</ErrorAlert>
|
||||||
</svelte:fragment>
|
|
||||||
</ErrorAlert>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if $appState.currentRequest?.base}
|
<CollectResponse on:response={handleResponseCollected} />
|
||||||
<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 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">
|
|
||||||
<h2 class="text-xl font-bold">{appName ? `"${appName}"` : 'An appplication'} would like to access your AWS credentials.</h2>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-[auto_1fr] gap-x-3">
|
|
||||||
<div class="text-right">Path:</div>
|
|
||||||
<code class="">{@html client.exe ? breakPath(client.exe) : 'Unknown'}</code>
|
|
||||||
<div class="text-right">PID:</div>
|
|
||||||
<code>{client.pid}</code>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="w-full grid grid-cols-[1fr_auto] items-center gap-y-6">
|
|
||||||
<!-- Don't display the option to approve with session credentials if base was specifically requested -->
|
|
||||||
{#if !$appState.currentRequest?.base}
|
|
||||||
<h3 class="font-semibold">
|
|
||||||
Approve with session credentials
|
|
||||||
</h3>
|
|
||||||
<Link target={() => approve(false)} hotkey="Enter" shift={true}>
|
|
||||||
<button class="w-full btn btn-success">
|
|
||||||
<KeyCombo keys={['Shift', 'Enter']} />
|
|
||||||
</button>
|
|
||||||
</Link>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<h3 class="font-semibold">
|
|
||||||
<span class="mr-2">
|
|
||||||
{#if $appState.currentRequest?.base}
|
|
||||||
Approve
|
|
||||||
{:else}
|
|
||||||
Approve with base credentials
|
|
||||||
{/if}
|
|
||||||
</span>
|
|
||||||
</h3>
|
|
||||||
<Link target={() => approve(true)} hotkey="Enter" shift={true} ctrl={true}>
|
|
||||||
<button class="w-full btn btn-warning">
|
|
||||||
<KeyCombo keys={['Ctrl', 'Shift', 'Enter']} />
|
|
||||||
</button>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<h3 class="font-semibold">
|
|
||||||
<span class="mr-2">Deny</span>
|
|
||||||
</h3>
|
|
||||||
<Link target={deny} hotkey="Escape">
|
|
||||||
<button class="w-full btn btn-error">
|
|
||||||
<KeyCombo keys={['Esc']} />
|
|
||||||
</button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
{:else if $appState.sessionStatus === 'locked'}
|
||||||
|
<!-- if session is locked and we do have a response, we must be waiting for unlock -->
|
||||||
|
<Unlock on:unlocked={sendResponse} />
|
||||||
|
{:else}
|
||||||
|
<!-- failsafe sanity check -->
|
||||||
|
<ErrorAlert>
|
||||||
|
Something is wrong. This message should never show up during normal operation.
|
||||||
|
</ErrorAlert>
|
||||||
{/if}
|
{/if}
|
||||||
|
14
src/views/ChangePassphrase.svelte
Normal file
14
src/views/ChangePassphrase.svelte
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<script>
|
||||||
|
import { navigate } from '../lib/routing.js';
|
||||||
|
|
||||||
|
import EnterPassphrase from './passphrase/EnterPassphrase.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="flex flex-col h-screen max-w-sm m-auto gap-y-8 justify-center">
|
||||||
|
<h1 class="text-2xl font-bold text-center">
|
||||||
|
Change passphrase
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<EnterPassphrase cancellable={true} on:save={() => navigate('Home')}/>
|
||||||
|
</div>
|
21
src/views/CreatePassphrase.svelte
Normal file
21
src/views/CreatePassphrase.svelte
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<script>
|
||||||
|
import EnterPassphrase from './passphrase/EnterPassphrase.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="flex flex-col h-screen max-w-lg m-auto justify-center">
|
||||||
|
<div class="space-y-8">
|
||||||
|
<h1 class="text-2xl font-bold text-center">Welcome to Creddy!</h1>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<p> Create a passphrase to get started.</p>
|
||||||
|
|
||||||
|
<p>Please note that if you forget your passphrase, there is no way to recover
|
||||||
|
your stored credentials. You will have to start over with a new passphrase.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="max-w-sm mx-auto">
|
||||||
|
<EnterPassphrase />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -1,94 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { onMount } from 'svelte';
|
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
|
||||||
import { emit } from '@tauri-apps/api/event';
|
|
||||||
import { getRootCause } from '../lib/errors.js';
|
|
||||||
|
|
||||||
import { appState } from '../lib/state.js';
|
|
||||||
import { navigate } from '../lib/routing.js';
|
|
||||||
import Link from '../ui/Link.svelte';
|
|
||||||
import ErrorAlert from '../ui/ErrorAlert.svelte';
|
|
||||||
import Spinner from '../ui/Spinner.svelte';
|
|
||||||
|
|
||||||
|
|
||||||
let errorMsg = null;
|
|
||||||
let alert;
|
|
||||||
let AccessKeyId, SecretAccessKey, passphrase, confirmPassphrase
|
|
||||||
|
|
||||||
function confirm() {
|
|
||||||
if (passphrase !== confirmPassphrase) {
|
|
||||||
errorMsg = 'Passphrases do not match.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let saving = false;
|
|
||||||
async function save() {
|
|
||||||
if (passphrase !== confirmPassphrase) {
|
|
||||||
alert.shake();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let credentials = {AccessKeyId, SecretAccessKey};
|
|
||||||
try {
|
|
||||||
saving = true;
|
|
||||||
await invoke('save_credentials', {credentials, passphrase});
|
|
||||||
emit('credentials-event', 'entered');
|
|
||||||
$appState.credentialStatus = 'unlocked';
|
|
||||||
if ($appState.currentRequest) {
|
|
||||||
navigate('Approve');
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
navigate('Home');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
const root = getRootCause(e);
|
|
||||||
if (e.code === 'GetSession' && root.code) {
|
|
||||||
errorMsg = `Error response from AWS (${root.code}): ${root.msg}`;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// some of the built-in Tauri errors are plain strings,
|
|
||||||
// so fall back to e if e.msg doesn't exist
|
|
||||||
errorMsg = e.msg || e;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the alert already existed, shake it
|
|
||||||
if (alert) {
|
|
||||||
alert.shake();
|
|
||||||
}
|
|
||||||
|
|
||||||
saving = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function cancel() {
|
|
||||||
emit('credentials-event', 'enter-canceled');
|
|
||||||
navigate('Home');
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<form action="#" on:submit|preventDefault="{save}" class="form-control space-y-4 max-w-sm m-auto p-4 h-screen justify-center">
|
|
||||||
<h2 class="text-2xl font-bold text-center">Enter your credentials</h2>
|
|
||||||
|
|
||||||
{#if errorMsg}
|
|
||||||
<ErrorAlert bind:this="{alert}">{errorMsg}</ErrorAlert>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<input type="text" placeholder="AWS Access Key ID" bind:value="{AccessKeyId}" class="input input-bordered" />
|
|
||||||
<input type="password" placeholder="AWS Secret Access Key" bind:value="{SecretAccessKey}" 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} />
|
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary">
|
|
||||||
{#if saving }
|
|
||||||
<Spinner class="w-5 h-5" thickness="12"/>
|
|
||||||
{:else}
|
|
||||||
Submit
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
<Link target={cancel} hotkey="Escape">
|
|
||||||
<button class="btn btn-sm btn-outline w-full">Cancel</button>
|
|
||||||
</Link>
|
|
||||||
</form>
|
|
@ -8,12 +8,24 @@
|
|||||||
import Icon from '../ui/Icon.svelte';
|
import Icon from '../ui/Icon.svelte';
|
||||||
import Link from '../ui/Link.svelte';
|
import Link from '../ui/Link.svelte';
|
||||||
|
|
||||||
import vaultDoorSvg from '../assets/vault_door.svg?raw';
|
let launchTerminalError;
|
||||||
|
async function launchTerminal() {
|
||||||
|
try {
|
||||||
|
await invoke('launch_terminal', {base: false});
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
launchTerminalError = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let launchBase = false;
|
async function lock() {
|
||||||
function launchTerminal() {
|
try {
|
||||||
invoke('launch_terminal', {base: launchBase});
|
await invoke('lock');
|
||||||
launchBase = false;
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -23,31 +35,38 @@
|
|||||||
</Nav>
|
</Nav>
|
||||||
|
|
||||||
<div class="flex flex-col h-screen items-center justify-center p-4 space-y-4">
|
<div class="flex flex-col h-screen items-center justify-center p-4 space-y-4">
|
||||||
<div class="flex flex-col items-center space-y-4">
|
<div class="grid grid-cols-2 gap-6">
|
||||||
{@html vaultDoorSvg}
|
<Link target="ManageCredentials">
|
||||||
{#if $appState.credentialStatus === 'locked'}
|
<div class="flex flex-col items-center gap-4 h-full max-w-56 rounded-box p-4 border border-primary hover:bg-base-200 transition-colors">
|
||||||
|
<Icon name="key" class="size-12 stroke-1 stroke-primary" />
|
||||||
|
<h3 class="text-lg font-bold">Credentials</h3>
|
||||||
|
<p class="text-sm">Add, remove, and change defaults credentials.</p>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link target={launchTerminal}>
|
||||||
|
<div class="flex flex-col items-center gap-4 h-full max-w-56 rounded-box p-4 border border-secondary hover:bg-base-200 transition-colors">
|
||||||
|
<Icon name="command-line" class="size-12 stroke-1 stroke-secondary" />
|
||||||
|
<h3 class="text-lg font-bold">Terminal</h3>
|
||||||
|
<p class="text-sm">Launch a terminal pre-configured with AWS credentials.</p>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
<h2 class="text-2xl font-bold">Creddy is locked</h2>
|
<Link target={lock}>
|
||||||
<Link target="Unlock" hotkey="Enter" class="w-64">
|
<div class="flex flex-col items-center gap-4 h-full max-w-56 rounded-box p-4 border border-warning hover:bg-base-200 transition-colors">
|
||||||
<button class="btn btn-primary w-full">Unlock</button>
|
<Icon name="shield-check" class="size-12 stroke-1 stroke-warning" />
|
||||||
</Link>
|
<h3 class="text-lg font-bold">Lock</h3>
|
||||||
|
<p class="text-sm">Lock Creddy.</p>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
{:else if $appState.credentialStatus === 'unlocked'}
|
<Link target={() => invoke('exit')}>
|
||||||
<h2 class="text-2xl font-bold">Waiting for requests</h2>
|
<div class="flex flex-col items-center gap-4 h-full max-w-56 rounded-box p-4 border border-accent hover:bg-base-200 transition-colors">
|
||||||
<button class="btn btn-primary w-full" on:click={launchTerminal}>
|
<Icon name="arrow-right-start-on-rectangle" class="size-12 stroke-1 stroke-accent" />
|
||||||
Launch Terminal
|
<h3 class="text-lg font-bold">Exit</h3>
|
||||||
</button>
|
<p class="text-sm">Close Creddy.</p>
|
||||||
<label class="label cursor-pointer flex items-center space-x-2">
|
</div>
|
||||||
<span class="label-text">Launch with long-lived credentials</span>
|
</Link>
|
||||||
<input type="checkbox" class="checkbox checkbox-sm" bind:checked={launchBase}>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
{:else if $appState.credentialStatus === 'empty'}
|
|
||||||
<h2 class="text-2xl font-bold">No credentials found</h2>
|
|
||||||
<Link target="EnterCredentials" hotkey="Enter" class="w-64">
|
|
||||||
<button class="btn btn-primary w-full">Enter Credentials</button>
|
|
||||||
</Link>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -56,10 +75,25 @@
|
|||||||
{#each $appState.setupErrors as error}
|
{#each $appState.setupErrors as error}
|
||||||
{#if error.show}
|
{#if error.show}
|
||||||
<div class="alert alert-error shadow-lg">
|
<div class="alert alert-error shadow-lg">
|
||||||
{error.msg}
|
<span>{error.msg}</span>
|
||||||
<button class="btn btn-sm btn-alert-error" on:click={() => error.show = false}>Ok</button>
|
<div>
|
||||||
|
<button class="btn btn-sm btn-alert-error" on:click={() => error.show = false}>Ok</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if launchTerminalError}
|
||||||
|
<div class="toast">
|
||||||
|
<div class="alert alert-error shadow-lg">
|
||||||
|
<span>{launchTerminalError.msg || launchTerminalError}</span>
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-alert-error" on:click={() => launchTerminalError = null}>
|
||||||
|
Ok
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
62
src/views/ManageCredentials.svelte
Normal file
62
src/views/ManageCredentials.svelte
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<script>
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { slide, fade } from 'svelte/transition';
|
||||||
|
import { writable } from 'svelte/store';
|
||||||
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
|
||||||
|
import AwsCredential from './credentials/AwsCredential.svelte';
|
||||||
|
import Icon from '../ui/Icon.svelte';
|
||||||
|
import Nav from '../ui/Nav.svelte';
|
||||||
|
|
||||||
|
let show = false;
|
||||||
|
|
||||||
|
let records = []
|
||||||
|
let defaults = writable({});
|
||||||
|
async function loadCreds() {
|
||||||
|
records = await invoke('list_credentials');
|
||||||
|
let pairs = records.filter(r => r.is_default).map(r => [r.credential.type, r.id]);
|
||||||
|
$defaults = Object.fromEntries(pairs);
|
||||||
|
}
|
||||||
|
onMount(loadCreds);
|
||||||
|
|
||||||
|
function newAws() {
|
||||||
|
records.push({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
name: null,
|
||||||
|
is_default: false,
|
||||||
|
credential: {type: 'AwsBase', AccessKeyId: null, SecretAccessKey: null},
|
||||||
|
isNew: true,
|
||||||
|
});
|
||||||
|
records = records;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<Nav>
|
||||||
|
<h1 slot="title" class="text-2xl font-bold">Credentials</h1>
|
||||||
|
</Nav>
|
||||||
|
|
||||||
|
<div class="max-w-xl mx-auto mb-12 flex flex-col gap-y-4 justify-center">
|
||||||
|
<div class="divider">
|
||||||
|
<h2 class="text-xl font-bold">AWS Access Keys</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if records.length > 0}
|
||||||
|
{#each records as record (record.id)}
|
||||||
|
<AwsCredential {record} {defaults} on:update={loadCreds} />
|
||||||
|
{/each}
|
||||||
|
<button class="btn btn-primary btn-wide mx-auto" on:click={newAws}>
|
||||||
|
<Icon name="plus-circle-mini" class="size-5" />
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<div class="flex flex-col gap-6 items-center rounded-box border-2 border-dashed border-neutral-content/30 p-6">
|
||||||
|
<div>You have no saved AWS credentials.</div>
|
||||||
|
<button class="btn btn-primary btn-wide mx-auto" on:click={newAws}>
|
||||||
|
<Icon name="plus-circle-mini" class="size-5" />
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
</div>
|
@ -5,7 +5,6 @@
|
|||||||
import { appState } from '../lib/state.js';
|
import { appState } from '../lib/state.js';
|
||||||
import Nav from '../ui/Nav.svelte';
|
import Nav from '../ui/Nav.svelte';
|
||||||
import Link from '../ui/Link.svelte';
|
import Link from '../ui/Link.svelte';
|
||||||
import ErrorAlert from '../ui/ErrorAlert.svelte';
|
|
||||||
import SettingsGroup from '../ui/settings/SettingsGroup.svelte';
|
import SettingsGroup from '../ui/settings/SettingsGroup.svelte';
|
||||||
import Keybind from '../ui/settings/Keybind.svelte';
|
import Keybind from '../ui/settings/Keybind.svelte';
|
||||||
import { Setting, ToggleSetting, NumericSetting, FileSetting, TextSetting, TimeSetting } from '../ui/settings';
|
import { Setting, ToggleSetting, NumericSetting, FileSetting, TextSetting, TimeSetting } from '../ui/settings';
|
||||||
@ -21,6 +20,7 @@
|
|||||||
let error = null;
|
let error = null;
|
||||||
async function save() {
|
async function save() {
|
||||||
try {
|
try {
|
||||||
|
throw('wtf');
|
||||||
await invoke('save_config', {config});
|
await invoke('save_config', {config});
|
||||||
$appState.config = await invoke('get_config');
|
$appState.config = await invoke('get_config');
|
||||||
}
|
}
|
||||||
@ -29,6 +29,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.getOsType = type;
|
||||||
let osType = null;
|
let osType = null;
|
||||||
type().then(t => osType = t);
|
type().then(t => osType = t);
|
||||||
</script>
|
</script>
|
||||||
@ -38,77 +39,78 @@
|
|||||||
<h1 slot="title" class="text-2xl font-bold">Settings</h1>
|
<h1 slot="title" class="text-2xl font-bold">Settings</h1>
|
||||||
</Nav>
|
</Nav>
|
||||||
|
|
||||||
<div class="max-w-lg mx-auto my-1.5 p-4 space-y-16">
|
<form on:submit|preventDefault={save}>
|
||||||
<SettingsGroup name="General">
|
<div class="max-w-lg mx-auto my-1.5 p-4 space-y-16">
|
||||||
<ToggleSetting title="Start on login" bind:value={config.start_on_login}>
|
<SettingsGroup name="General">
|
||||||
<svelte:fragment slot="description">
|
<ToggleSetting title="Start on login" bind:value={config.start_on_login}>
|
||||||
Start Creddy when you log in to your computer.
|
|
||||||
</svelte:fragment>
|
|
||||||
</ToggleSetting>
|
|
||||||
|
|
||||||
<ToggleSetting title="Start minimized" bind:value={config.start_minimized}>
|
|
||||||
<svelte:fragment slot="description">
|
|
||||||
Minimize to the system tray at startup.
|
|
||||||
</svelte:fragment>
|
|
||||||
</ToggleSetting>
|
|
||||||
|
|
||||||
<NumericSetting title="Re-hide delay" bind:value={config.rehide_ms} min={0} unit="Milliseconds">
|
|
||||||
<svelte:fragment slot="description">
|
|
||||||
How long to wait after a request is approved/denied before minimizing
|
|
||||||
the window to tray. Only applicable if the window was minimized
|
|
||||||
to tray before the request was received.
|
|
||||||
</svelte:fragment>
|
|
||||||
</NumericSetting>
|
|
||||||
|
|
||||||
<ToggleSetting title="Lock when idle" bind:value={config.auto_lock}>
|
|
||||||
<svelte:fragment slot="description">
|
|
||||||
Automatically lock Creddy after a period of inactivity.
|
|
||||||
</svelte:fragment>
|
|
||||||
</ToggleSetting>
|
|
||||||
|
|
||||||
{#if config.auto_lock}
|
|
||||||
<TimeSetting title="Idle timeout" bind:seconds={config.lock_after.secs}>
|
|
||||||
<svelte:fragment slot="description">
|
<svelte:fragment slot="description">
|
||||||
How long to wait before automatically locking.
|
Start Creddy when you log in to your computer.
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</TimeSetting>
|
</ToggleSetting>
|
||||||
{/if}
|
|
||||||
|
|
||||||
<Setting title="Update credentials">
|
<ToggleSetting title="Start minimized" bind:value={config.start_minimized}>
|
||||||
<Link slot="input" target="EnterCredentials">
|
<svelte:fragment slot="description">
|
||||||
<button class="btn btn-sm btn-primary">Update</button>
|
Minimize to the system tray at startup.
|
||||||
</Link>
|
</svelte:fragment>
|
||||||
<svelte:fragment slot="description">
|
</ToggleSetting>
|
||||||
Update or re-enter your encrypted credentials.
|
|
||||||
</svelte:fragment>
|
|
||||||
</Setting>
|
|
||||||
|
|
||||||
<FileSetting
|
<NumericSetting title="Re-hide delay" bind:value={config.rehide_ms} min={0} unit="Milliseconds">
|
||||||
title="Terminal emulator"
|
<svelte:fragment slot="description">
|
||||||
bind:value={config.terminal.exec}
|
How long to wait after a request is approved/denied before minimizing
|
||||||
|
the window to tray. Only applicable if the window was minimized
|
||||||
>
|
to tray before the request was received.
|
||||||
<svelte:fragment slot="description">
|
</svelte:fragment>
|
||||||
Choose your preferred terminal emulator (e.g. <code>gnome-terminal</code> or <code>wt.exe</code>.) May be an absolute path or an executable discoverable on <code>$PATH</code>.
|
</NumericSetting>
|
||||||
</svelte:fragment>
|
|
||||||
</FileSetting>
|
|
||||||
</SettingsGroup>
|
|
||||||
|
|
||||||
<SettingsGroup name="Hotkeys">
|
<ToggleSetting title="Lock when idle" bind:value={config.auto_lock}>
|
||||||
<div class="space-y-4">
|
<svelte:fragment slot="description">
|
||||||
<p>Click on a keybinding to modify it. Use the checkbox to enable or disable a keybinding entirely.</p>
|
Automatically lock Creddy after a period of inactivity.
|
||||||
|
</svelte:fragment>
|
||||||
|
</ToggleSetting>
|
||||||
|
|
||||||
<div class="grid grid-cols-[auto_1fr_auto] gap-y-3 items-center">
|
{#if config.auto_lock}
|
||||||
<Keybind description="Show Creddy" bind:value={config.hotkeys.show_window} />
|
<TimeSetting title="Idle timeout" bind:seconds={config.lock_after.secs}>
|
||||||
<Keybind description="Launch terminal" bind:value={config.hotkeys.launch_terminal} />
|
<svelte:fragment slot="description">
|
||||||
|
How long to wait before automatically locking.
|
||||||
|
</svelte:fragment>
|
||||||
|
</TimeSetting>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<Setting title="Update passphrase">
|
||||||
|
<Link slot="input" target="ChangePassphrase">
|
||||||
|
<button type="button" class="btn btn-sm btn-primary">Update</button>
|
||||||
|
</Link>
|
||||||
|
<svelte:fragment slot="description">
|
||||||
|
Change your master passphrase.
|
||||||
|
</svelte:fragment>
|
||||||
|
</Setting>
|
||||||
|
|
||||||
|
<FileSetting
|
||||||
|
title="Terminal emulator"
|
||||||
|
bind:value={config.terminal.exec}
|
||||||
|
>
|
||||||
|
<svelte:fragment slot="description">
|
||||||
|
Choose your preferred terminal emulator (e.g. <code>gnome-terminal</code> or <code>wt.exe</code>.) May be an absolute path or an executable discoverable on <code>$PATH</code>.
|
||||||
|
</svelte:fragment>
|
||||||
|
</FileSetting>
|
||||||
|
</SettingsGroup>
|
||||||
|
|
||||||
|
<SettingsGroup name="Hotkeys">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<p>Click on a keybinding to modify it. Use the checkbox to enable or disable a keybinding entirely.</p>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-[auto_1fr_auto] gap-y-3 items-center">
|
||||||
|
<Keybind description="Show Creddy" bind:value={config.hotkeys.show_window} />
|
||||||
|
<Keybind description="Launch terminal" bind:value={config.hotkeys.launch_terminal} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</SettingsGroup>
|
||||||
</SettingsGroup>
|
|
||||||
|
|
||||||
<p class="text-sm text-right">
|
<p class="text-sm text-right">
|
||||||
Creddy {$appState.appVersion}
|
Creddy {$appState.appVersion}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
{#if error}
|
{#if error}
|
||||||
<div transition:fly={{y: 100, easing: backInOut, duration: 400}} class="toast">
|
<div transition:fly={{y: 100, easing: backInOut, duration: 400}} class="toast">
|
||||||
|
@ -1,85 +1,59 @@
|
|||||||
<script>
|
<script>
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import { emit } from '@tauri-apps/api/event';
|
import { emit } from '@tauri-apps/api/event';
|
||||||
import { onMount } from 'svelte';
|
import { onMount, createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
import { appState } from '../lib/state.js';
|
import { appState } from '../lib/state.js';
|
||||||
import { navigate } from '../lib/routing.js';
|
import { navigate } from '../lib/routing.js';
|
||||||
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 PassphraseInput from '../ui/PassphraseInput.svelte';
|
||||||
|
import ResetPassphrase from './passphrase/ResetPassphrase.svelte';
|
||||||
import Spinner from '../ui/Spinner.svelte';
|
import Spinner from '../ui/Spinner.svelte';
|
||||||
|
import vaultDoorSvg from '../assets/vault_door.svg?raw';
|
||||||
|
|
||||||
|
|
||||||
let errorMsg = null;
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
let alert;
|
let alert;
|
||||||
let passphrase = '';
|
let passphrase = '';
|
||||||
let loadTime = 0;
|
|
||||||
let saving = false;
|
let saving = false;
|
||||||
async function unlock() {
|
async function unlock() {
|
||||||
// The hotkey for navigating here from homepage is Enter, which also
|
saving = true;
|
||||||
// happens to trigger the form submit event
|
|
||||||
if (Date.now() - loadTime < 10) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
saving = true;
|
await alert.run(async () => invoke('unlock', {passphrase}));
|
||||||
let r = await invoke('unlock', {passphrase});
|
$appState.sessionStatus = 'unlocked';
|
||||||
$appState.credentialStatus = 'unlocked';
|
emit('unlocked');
|
||||||
emit('credentials-event', 'unlocked');
|
dispatch('unlocked');
|
||||||
if ($appState.currentRequest) {
|
|
||||||
navigate('Approve');
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
navigate('Home');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (e) {
|
finally {
|
||||||
const root = getRootCause(e);
|
|
||||||
if (e.code === 'GetSession' && root.code) {
|
|
||||||
errorMsg = `Error response from AWS (${root.code}): ${root.msg}`;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
errorMsg = e.msg;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the alert already existed, shake it
|
|
||||||
if (alert) {
|
|
||||||
alert.shake();
|
|
||||||
}
|
|
||||||
|
|
||||||
saving = false;
|
saving = false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function cancel() {
|
|
||||||
emit('credentials-event', 'unlock-canceled');
|
|
||||||
if ($appState.currentRequest !== null) {
|
|
||||||
// dirty hack to prevent spurious error when returning to approve screen
|
|
||||||
delete $appState.currentRequest.response;
|
|
||||||
navigate('Approve');
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
navigate('Home');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
loadTime = Date.now();
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="fixed top-0 w-full p-2 text-center">
|
||||||
|
<h1 class="text-3xl font-bold">Creddy is locked</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
<form action="#" on:submit|preventDefault="{unlock}" class="form-control space-y-4 max-w-sm m-auto p-4 h-screen justify-center">
|
<form action="#" on:submit|preventDefault="{unlock}" class="form-control space-y-4 max-w-sm m-auto p-4 h-screen justify-center">
|
||||||
<h2 class="font-bold text-2xl text-center">Enter your passphrase</h2>
|
<div class="mx-auto">
|
||||||
|
{@html vaultDoorSvg}
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if errorMsg}
|
<label class="space-y-4">
|
||||||
<ErrorAlert bind:this="{alert}">{errorMsg}</ErrorAlert>
|
<h2 class="font-bold text-xl text-center">Please enter your passphrase</h2>
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-autofocus -->
|
<ErrorAlert bind:this="{alert}" />
|
||||||
<input autofocus name="password" type="password" placeholder="correct horse battery staple" bind:value="{passphrase}" class="input input-bordered" />
|
|
||||||
|
<!-- svelte-ignore a11y-autofocus -->
|
||||||
|
<PassphraseInput autofocus="true" bind:value={passphrase} placeholder="correct horse battery staple" />
|
||||||
|
</label>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary">
|
<button type="submit" class="btn btn-primary">
|
||||||
{#if saving}
|
{#if saving}
|
||||||
@ -89,7 +63,5 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<Link target={cancel} hotkey="Escape">
|
<ResetPassphrase />
|
||||||
<button class="btn btn-sm btn-outline w-full">Cancel</button>
|
|
||||||
</Link>
|
|
||||||
</form>
|
</form>
|
||||||
|
91
src/views/approve/CollectResponse.svelte
Normal file
91
src/views/approve/CollectResponse.svelte
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
<script>
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
import { appState, cleanupRequest } from '../../lib/state.js';
|
||||||
|
|
||||||
|
import Link from '../../ui/Link.svelte';
|
||||||
|
import KeyCombo from '../../ui/KeyCombo.svelte';
|
||||||
|
|
||||||
|
|
||||||
|
// Executable paths can be long, so ensure they only break on \ or /
|
||||||
|
function breakPath(path) {
|
||||||
|
return path.replace(/(\\|\/)/g, '$1<wbr>');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract executable name from full path
|
||||||
|
const client = $appState.currentRequest.client;
|
||||||
|
const m = client.exe?.match(/\/([^/]+?$)|\\([^\\]+?$)/);
|
||||||
|
const appName = m[1] || m[2];
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
function setResponse(approval, base) {
|
||||||
|
$appState.currentRequest.response = {
|
||||||
|
id: $appState.currentRequest.id,
|
||||||
|
approval,
|
||||||
|
base,
|
||||||
|
};
|
||||||
|
dispatch('response');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
{#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 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">
|
||||||
|
<h2 class="text-xl font-bold">{appName ? `"${appName}"` : 'An appplication'} would like to access your AWS credentials.</h2>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-[auto_1fr] gap-x-3">
|
||||||
|
<div class="text-right">Path:</div>
|
||||||
|
<code class="">{@html client.exe ? breakPath(client.exe) : 'Unknown'}</code>
|
||||||
|
<div class="text-right">PID:</div>
|
||||||
|
<code>{client.pid}</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-full grid grid-cols-[1fr_auto] items-center gap-y-6">
|
||||||
|
<!-- Don't display the option to approve with session credentials if base was specifically requested -->
|
||||||
|
{#if !$appState.currentRequest?.base}
|
||||||
|
<h3 class="font-semibold">
|
||||||
|
Approve with session credentials
|
||||||
|
</h3>
|
||||||
|
<Link target={() => setResponse('Approved', false)} hotkey="Enter" shift={true}>
|
||||||
|
<button class="w-full btn btn-success">
|
||||||
|
<KeyCombo keys={['Shift', 'Enter']} />
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<h3 class="font-semibold">
|
||||||
|
<span class="mr-2">
|
||||||
|
{#if $appState.currentRequest?.base}
|
||||||
|
Approve
|
||||||
|
{:else}
|
||||||
|
Approve with base credentials
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
</h3>
|
||||||
|
<Link target={() => setResponse('Approved', true)} hotkey="Enter" shift={true} ctrl={true}>
|
||||||
|
<button class="w-full btn btn-warning">
|
||||||
|
<KeyCombo keys={['Ctrl', 'Shift', 'Enter']} />
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<h3 class="font-semibold">
|
||||||
|
<span class="mr-2">Deny</span>
|
||||||
|
</h3>
|
||||||
|
<Link target={() => setResponse('Denied', false)} hotkey="Escape">
|
||||||
|
<button class="w-full btn btn-error">
|
||||||
|
<KeyCombo keys={['Esc']} />
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
@ -1,8 +1,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { onMount } from 'svelte';
|
|
||||||
import { draw, fade } from 'svelte/transition';
|
import { draw, fade } from 'svelte/transition';
|
||||||
|
|
||||||
import { appState, cleanupRequest } from '../lib/state.js';
|
import { appState } from '../../lib/state.js';
|
||||||
|
|
||||||
let success = false;
|
let success = false;
|
||||||
let error = null;
|
let error = null;
|
||||||
@ -10,14 +9,6 @@
|
|||||||
let drawDuration = $appState.config.rehide_ms >= 750 ? 500 : 0;
|
let drawDuration = $appState.config.rehide_ms >= 750 ? 500 : 0;
|
||||||
let fadeDuration = drawDuration * 0.6;
|
let fadeDuration = drawDuration * 0.6;
|
||||||
let fadeDelay = drawDuration * 0.4;
|
let fadeDelay = drawDuration * 0.4;
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
window.setTimeout(
|
|
||||||
cleanupRequest,
|
|
||||||
// Extra 50ms so the window can finish disappearing before the redraw
|
|
||||||
Math.min(5000, $appState.config.rehide_ms + 50),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
155
src/views/credentials/AwsCredential.svelte
Normal file
155
src/views/credentials/AwsCredential.svelte
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
<script>
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
import { fade, slide } from 'svelte/transition';
|
||||||
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
|
||||||
|
import ErrorAlert from '../../ui/ErrorAlert.svelte';
|
||||||
|
import Icon from '../../ui/Icon.svelte';
|
||||||
|
|
||||||
|
export let record;
|
||||||
|
export let defaults;
|
||||||
|
|
||||||
|
import PassphraseInput from '../../ui/PassphraseInput.svelte';
|
||||||
|
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
let showDetails = record.isNew ? true : false;
|
||||||
|
|
||||||
|
let localName = name;
|
||||||
|
let local = JSON.parse(JSON.stringify(record));
|
||||||
|
$: isModified = JSON.stringify(local) !== JSON.stringify(record);
|
||||||
|
|
||||||
|
// explicitly subscribe to updates to `default`, so that we can update
|
||||||
|
// our local copy even if the component hasn't been recreated
|
||||||
|
// (sadly we can't use a reactive binding because reasons I guess)
|
||||||
|
defaults.subscribe(d => local.is_default = local.id === d[local.credential.type])
|
||||||
|
|
||||||
|
let alert;
|
||||||
|
async function saveCredential() {
|
||||||
|
await invoke('save_credential', {record: local});
|
||||||
|
dispatch('update');
|
||||||
|
showDetails = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let deleteModal;
|
||||||
|
function conditionalDelete() {
|
||||||
|
if (!record.isNew) {
|
||||||
|
deleteModal.showModal();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
deleteCredential();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteCredential() {
|
||||||
|
try {
|
||||||
|
if (!record.isNew) {
|
||||||
|
await invoke('delete_credential', {id: record.id});
|
||||||
|
}
|
||||||
|
dispatch('update');
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
showDetails = true;
|
||||||
|
// wait for showDetails to take effect and the alert to be rendered
|
||||||
|
window.setTimeout(() => alert.setError(e), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<div
|
||||||
|
transition:slide|local={{duration: record.isNew ? 300 : 0}}
|
||||||
|
class="rounded-box space-y-4 bg-base-200 {record.is_default ? 'border border-accent' : ''}"
|
||||||
|
>
|
||||||
|
<div class="flex items-center px-6 py-4 gap-x-4">
|
||||||
|
<h3 class="text-lg font-bold">{record.name || ''}</h3>
|
||||||
|
|
||||||
|
{#if record.is_default}
|
||||||
|
<span class="badge badge-accent">Default</span>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="join ml-auto">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline join-item"
|
||||||
|
on:click={() => showDetails = !showDetails}
|
||||||
|
>
|
||||||
|
<Icon name="pencil" class="size-6" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline btn-error join-item"
|
||||||
|
on:click={conditionalDelete}
|
||||||
|
>
|
||||||
|
<Icon name="trash" class="size-6" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{#if showDetails}
|
||||||
|
<form
|
||||||
|
transition:slide|local={{duration: 200}}
|
||||||
|
class=" px-6 pb-4 space-y-4"
|
||||||
|
on:submit|preventDefault={() => alert.run(saveCredential)}
|
||||||
|
>
|
||||||
|
<ErrorAlert bind:this={alert} />
|
||||||
|
|
||||||
|
<div class="grid grid-cols-[auto_1fr] items-center gap-4">
|
||||||
|
{#if record.isNew}
|
||||||
|
<span class="justify-self-end">Name</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered bg-transparent"
|
||||||
|
bind:value={local.name}
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<span class="justify-self-end">Key ID</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered font-mono bg-transparent"
|
||||||
|
bind:value={local.credential.AccessKeyId}
|
||||||
|
>
|
||||||
|
|
||||||
|
<span>Secret key</span>
|
||||||
|
<div class="font-mono">
|
||||||
|
<PassphraseInput class="bg-transparent" bind:value={local.credential.SecretAccessKey} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<label class="label cursor-pointer justify-self-start space-x-4">
|
||||||
|
<span class="label-text">Default AWS access key</span>
|
||||||
|
<input type="checkbox" class="toggle toggle-accent" bind:checked={local.is_default}>
|
||||||
|
</label>
|
||||||
|
{#if isModified}
|
||||||
|
<button
|
||||||
|
transition:fade={{duration: 100}}
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-primary"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<dialog bind:this={deleteModal} class="modal">
|
||||||
|
<div class="modal-box">
|
||||||
|
<h3 class="text-lg font-bold">Delete AWS credential "{record.name}"?</h3>
|
||||||
|
<div class="modal-action">
|
||||||
|
<form method="dialog" class="flex gap-x-4">
|
||||||
|
<button class="btn btn-outline">Cancel</button>
|
||||||
|
<button
|
||||||
|
autofocus
|
||||||
|
class="btn btn-error"
|
||||||
|
on:click={deleteCredential}
|
||||||
|
>Delete</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
</div>
|
109
src/views/passphrase/EnterPassphrase.svelte
Normal file
109
src/views/passphrase/EnterPassphrase.svelte
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
<script>
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
import { appState } from '../../lib/state.js';
|
||||||
|
|
||||||
|
import ErrorAlert from '../../ui/ErrorAlert.svelte';
|
||||||
|
import Link from '../../ui/Link.svelte';
|
||||||
|
import PassphraseInput from '../../ui/PassphraseInput.svelte';
|
||||||
|
import ResetPassphrase from './ResetPassphrase.svelte';
|
||||||
|
import Spinner from '../../ui/Spinner.svelte';
|
||||||
|
|
||||||
|
export let cancellable = false;
|
||||||
|
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
let alert;
|
||||||
|
let saving = false;
|
||||||
|
let passphrase = '';
|
||||||
|
let confirmPassphrase = '';
|
||||||
|
|
||||||
|
// onChange only fires when an input loses focus, so always set the error if not set
|
||||||
|
function onChange() {
|
||||||
|
console.log(`onChange: passphrase=${passphrase}, confirmPassphrase=${confirmPassphrase}`)
|
||||||
|
if (passphrase !== confirmPassphrase) {
|
||||||
|
alert.setError('Passphrases do not match.');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
alert.setError(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// onInput fires on every keystroke, so only dismiss the error, don't create it
|
||||||
|
function onInput() {
|
||||||
|
console.log(`onInput: passphrase=${passphrase}, confirmPassphrase=${confirmPassphrase}`)
|
||||||
|
if (passphrase === confirmPassphrase) {
|
||||||
|
alert.setError(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
if (passphrase !== confirmPassphrase) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (passphrase === '') {
|
||||||
|
alert.setError('Passphrase is empty.')
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
saving = true;
|
||||||
|
try {
|
||||||
|
await alert.run(async () => {
|
||||||
|
await invoke('set_passphrase', {passphrase})
|
||||||
|
throw('something bad happened');
|
||||||
|
$appState.sessionStatus = 'unlocked';
|
||||||
|
dispatch('save');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
saving = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<form class="form-control gap-y-4" on:submit|preventDefault={save}>
|
||||||
|
<ErrorAlert bind:this={alert} />
|
||||||
|
|
||||||
|
<label class="form-control w-full">
|
||||||
|
<div class="label">
|
||||||
|
<span class="label-text">Passphrase</span>
|
||||||
|
</div>
|
||||||
|
<PassphraseInput
|
||||||
|
bind:value={passphrase}
|
||||||
|
on:input={onInput}
|
||||||
|
placeholder="correct horse battery staple"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="form-control w-full">
|
||||||
|
<div class="label">
|
||||||
|
<span class="label-text">Re-enter passphrase</span>
|
||||||
|
</div>
|
||||||
|
<PassphraseInput
|
||||||
|
bind:value={confirmPassphrase}
|
||||||
|
on:input={onInput} on:change={onChange}
|
||||||
|
placeholder="correct horse battery staple"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
{#if saving}
|
||||||
|
<Spinner class="w-5 h-5" thickness="12"/>
|
||||||
|
{:else}
|
||||||
|
Submit
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{#if cancellable}
|
||||||
|
<Link target="Settings" hotkey="Escape">
|
||||||
|
<button type="button" class="btn btn-outline btn-sm w-full">Cancel</button>
|
||||||
|
</Link>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if $appState.sessionStatus === 'locked'}
|
||||||
|
<ResetPassphrase />
|
||||||
|
{/if}
|
||||||
|
</form>
|
41
src/views/passphrase/ResetPassphrase.svelte
Normal file
41
src/views/passphrase/ResetPassphrase.svelte
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<script>
|
||||||
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
import { appState } from '../../lib/state.js';
|
||||||
|
|
||||||
|
import ErrorAlert from '../../ui/ErrorAlert.svelte';
|
||||||
|
|
||||||
|
|
||||||
|
let modal;
|
||||||
|
let alert;
|
||||||
|
|
||||||
|
async function reset() {
|
||||||
|
await invoke('reset_session');
|
||||||
|
$appState.sessionStatus = 'empty';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<button type="button" class="self-end text-sm text-secondary/75 hover:underline focus:ring-accent" on:click={modal.showModal()}>
|
||||||
|
Reset passphrase
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<dialog class="modal" bind:this={modal}>
|
||||||
|
<div class="modal-box space-y-6">
|
||||||
|
<ErrorAlert bind:this={alert} />
|
||||||
|
<h3 class="text-lg font-bold">Delete all credentials?</h3>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<p>Credentials are encrypted with your current passphrase and will be lost if the passphrase is reset.</p>
|
||||||
|
<p>Are you sure you want to reset your passphrase and delete all saved credentials?</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-action">
|
||||||
|
<form method="dialog" class="flex gap-x-4">
|
||||||
|
<button autofocus class="btn btn-outline">Cancel</button>
|
||||||
|
<button class="btn btn-error" on:click|preventDefault={() => alert.run(reset)}>
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
@ -10,4 +10,40 @@ module.exports = {
|
|||||||
plugins: [
|
plugins: [
|
||||||
require('daisyui'),
|
require('daisyui'),
|
||||||
],
|
],
|
||||||
|
daisyui: {
|
||||||
|
themes: [
|
||||||
|
{
|
||||||
|
creddy: {
|
||||||
|
"primary": "#0ea5e9",
|
||||||
|
"secondary": "#fb923c",
|
||||||
|
"accent": "#8b5cf6",
|
||||||
|
"neutral": "#374151",
|
||||||
|
"base-100": "#252e3a",
|
||||||
|
"info": "#66cccc",
|
||||||
|
"success": "#52bf73",
|
||||||
|
"warning": "#d1a900",
|
||||||
|
"error": "#f87171",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"summer-night": {
|
||||||
|
"primary": "#0ea5e9",
|
||||||
|
"secondary": "#0ea5e9",
|
||||||
|
"accent": "#fb923c",
|
||||||
|
"neutral": "#393939",
|
||||||
|
"base-100": "#2d2d2d",
|
||||||
|
"info": "#66cccc",
|
||||||
|
"success": "#22c55e",
|
||||||
|
"warning": "#d1a900",
|
||||||
|
"error": "#f2777a"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"dark",
|
||||||
|
"night",
|
||||||
|
"dracula",
|
||||||
|
"sunset",
|
||||||
|
"dim",
|
||||||
|
"light"
|
||||||
|
]
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user