diff --git a/package-lock.json b/package-lock.json index a5839a78..c9a29aae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -130,6 +130,7 @@ "tailwindcss": "^3.4.1", "typescript": "~5.2.2", "vite": "^5.2.0", + "vite-plugin-node-polyfills": "^0.23.0", "vite-plugin-pwa": "^0.19.8" } }, @@ -8077,6 +8078,29 @@ } } }, + "node_modules/@rollup/plugin-inject": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@rollup/plugin-inject/-/plugin-inject-5.0.5.tgz", + "integrity": "sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, "node_modules/@rollup/plugin-node-resolve": { "version": "15.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz", @@ -11432,6 +11456,20 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/assert": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", + "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "is-nan": "^1.3.2", + "object-is": "^1.1.5", + "object.assign": "^4.1.4", + "util": "^0.12.5" + } + }, "node_modules/assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", @@ -12177,6 +12215,16 @@ "integrity": "sha512-sLoadumpRfsjprP8XzVjpQc0jK8yqHBx0PtUTGYj2fftT+P/t+uyDAQdMgGAPKD011in/O+YYGh7fIs0oG/viw==", "license": "Apache-2.0" }, + "node_modules/browser-resolve": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-2.0.0.tgz", + "integrity": "sha512-7sWsQlYL2rGLy2IWm8WL8DCTJvYLc/qlOnsakDac87SOoCd16WLsaAMdCiAqsTNHIe+SXfaqyxyo6THoWqs8WQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.17.0" + } + }, "node_modules/browserify-aes": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", @@ -12311,6 +12359,23 @@ "dev": true, "license": "MIT" }, + "node_modules/browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pako": "~1.0.5" + } + }, + "node_modules/browserify-zlib/node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true, + "license": "(MIT AND Zlib)" + }, "node_modules/browserslist": { "version": "4.24.5", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", @@ -12490,6 +12555,13 @@ "node": ">=12" } }, + "node_modules/builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==", + "dev": true, + "license": "MIT" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -13614,6 +13686,12 @@ "optional": true, "peer": true }, + "node_modules/console-browserify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", + "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==", + "dev": true + }, "node_modules/console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", @@ -13621,6 +13699,13 @@ "devOptional": true, "license": "ISC" }, + "node_modules/constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==", + "dev": true, + "license": "MIT" + }, "node_modules/conventional-changelog": { "version": "3.1.25", "resolved": "https://registry.npmjs.org/conventional-changelog/-/conventional-changelog-3.1.25.tgz", @@ -15017,6 +15102,19 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/domain-browser": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-4.22.0.tgz", + "integrity": "sha512-IGBwjF7tNk3cwypFNH/7bfzBcgSCbaMOD3GsaY1AU/JRrnHnYgEM0+9kQt52iZxjNsjBtJYtao146V+f8jFZNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, "node_modules/domelementtype": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", @@ -18011,6 +18109,13 @@ "node": ">=10.19.0" } }, + "node_modules/https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==", + "dev": true, + "license": "MIT" + }, "node_modules/https-proxy-agent": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", @@ -18295,6 +18400,23 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -18625,6 +18747,23 @@ "dev": true, "license": "MIT" }, + "node_modules/is-nan": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", + "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -18937,6 +19076,16 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, + "node_modules/isomorphic-timers-promises": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/isomorphic-timers-promises/-/isomorphic-timers-promises-1.0.1.tgz", + "integrity": "sha512-u4sej9B1LPSxTGKB/HiuzvEQnXH0ECYkSVQU39koSwmFAxhlEAFl9RdTvLv4TOTQUgBS5O3O5fwUxk6byBZ+IQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/isomorphic-webcrypto": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/isomorphic-webcrypto/-/isomorphic-webcrypto-2.3.8.tgz", @@ -22927,6 +23076,92 @@ "devOptional": true, "license": "MIT" }, + "node_modules/node-stdlib-browser": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-stdlib-browser/-/node-stdlib-browser-1.3.1.tgz", + "integrity": "sha512-X75ZN8DCLftGM5iKwoYLA3rjnrAEs97MkzvSd4q2746Tgpg8b8XWiBGiBG4ZpgcAqBgtgPHTiAc8ZMCvZuikDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert": "^2.0.0", + "browser-resolve": "^2.0.0", + "browserify-zlib": "^0.2.0", + "buffer": "^5.7.1", + "console-browserify": "^1.1.0", + "constants-browserify": "^1.0.0", + "create-require": "^1.1.1", + "crypto-browserify": "^3.12.1", + "domain-browser": "4.22.0", + "events": "^3.0.0", + "https-browserify": "^1.0.0", + "isomorphic-timers-promises": "^1.0.1", + "os-browserify": "^0.3.0", + "path-browserify": "^1.0.1", + "pkg-dir": "^5.0.0", + "process": "^0.11.10", + "punycode": "^1.4.1", + "querystring-es3": "^0.2.1", + "readable-stream": "^3.6.0", + "stream-browserify": "^3.0.0", + "stream-http": "^3.2.0", + "string_decoder": "^1.0.0", + "timers-browserify": "^2.0.4", + "tty-browserify": "0.0.1", + "url": "^0.11.4", + "util": "^0.12.4", + "vm-browserify": "^1.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-stdlib-browser/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/node-stdlib-browser/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-stdlib-browser/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/nopt": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", @@ -23283,6 +23518,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", @@ -23482,6 +23734,13 @@ "node": ">= 6" } }, + "node_modules/os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==", + "dev": true, + "license": "MIT" + }, "node_modules/own-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", @@ -23887,6 +24146,19 @@ "node": ">= 6" } }, + "node_modules/pkg-dir": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz", + "integrity": "sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^5.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/playwright": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz", @@ -24680,6 +24952,31 @@ "node": ">=6" } }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==", + "dev": true, + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/queue": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", @@ -27673,6 +27970,44 @@ "node": ">= 0.10.0" } }, + "node_modules/stream-http": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-3.2.0.tgz", + "integrity": "sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A==", + "dev": true, + "license": "MIT", + "dependencies": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "xtend": "^4.0.2" + } + }, + "node_modules/stream-http/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/stream-http/node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/streamx": { "version": "2.22.0", "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.0.tgz", @@ -28508,6 +28843,19 @@ "node": ">= 6" } }, + "node_modules/timers-browserify": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz", + "integrity": "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "setimmediate": "^1.0.4" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/tmp": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", @@ -28719,6 +29067,13 @@ "dev": true, "license": "0BSD" }, + "node_modules/tty-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz", + "integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==", + "dev": true, + "license": "MIT" + }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -29387,6 +29742,27 @@ "punycode": "^2.1.0" } }, + "node_modules/url": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz", + "integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^1.4.1", + "qs": "^6.12.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/url/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "dev": true, + "license": "MIT" + }, "node_modules/utf8": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/utf8/-/utf8-3.0.0.tgz", @@ -29400,6 +29776,20 @@ "dev": true, "license": "(WTFPL OR MIT)" }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -29555,6 +29945,23 @@ } } }, + "node_modules/vite-plugin-node-polyfills": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/vite-plugin-node-polyfills/-/vite-plugin-node-polyfills-0.23.0.tgz", + "integrity": "sha512-4n+Ys+2bKHQohPBKigFlndwWQ5fFKwaGY6muNDMTb0fSQLyBzS+jjUNRZG9sKF0S/Go4ApG6LFnUGopjkILg3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/plugin-inject": "^5.0.5", + "node-stdlib-browser": "^1.2.0" + }, + "funding": { + "url": "https://github.com/sponsors/davidmyersdev" + }, + "peerDependencies": { + "vite": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" + } + }, "node_modules/vite-plugin-pwa": { "version": "0.19.8", "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.19.8.tgz", @@ -29622,6 +30029,13 @@ "optional": true, "peer": true }, + "node_modules/vm-browserify": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", + "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", + "dev": true, + "license": "MIT" + }, "node_modules/vue": { "version": "3.5.15", "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.15.tgz", diff --git a/package.json b/package.json index fe27c829..cf73c1e0 100644 --- a/package.json +++ b/package.json @@ -168,6 +168,7 @@ "tailwindcss": "^3.4.1", "typescript": "~5.2.2", "vite": "^5.2.0", + "vite-plugin-node-polyfills": "^0.23.0", "vite-plugin-pwa": "^0.19.8" }, "main": "./dist-electron/main.js", diff --git a/src/main.capacitor.ts b/src/main.capacitor.ts index b0b4290f..71607af8 100644 --- a/src/main.capacitor.ts +++ b/src/main.capacitor.ts @@ -86,5 +86,19 @@ const handleDeepLink = async (data: { url: string }) => { App.addListener("appUrlOpen", handleDeepLink); logger.log("[Capacitor] Mounting app"); -app.mount("#app"); + +// Initialize and mount the app +initializeApp().then((app) => { + app.mount("#app"); +}).catch((error) => { + console.error("Failed to initialize app:", error); + document.body.innerHTML = ` +
+

Failed to initialize app

+

${error instanceof Error ? error.message : "Unknown error"}

+

Please try restarting the app or contact support if the problem persists.

+
+ `; +}); + logger.log("[Capacitor] App mounted"); diff --git a/src/main.common.ts b/src/main.common.ts index 7781f0ee..cea41dc1 100644 --- a/src/main.common.ts +++ b/src/main.common.ts @@ -32,7 +32,7 @@ function setupGlobalErrorHandler(app: VueApp) { } // Function to initialize the app -export function initializeApp() { +export async function initializeApp() { logger.log("[App Init] Starting app initialization"); logger.log("[App Init] Platform:", process.env.VITE_PLATFORM); @@ -55,15 +55,21 @@ export function initializeApp() { app.use(Notifications); logger.log("[App Init] Notifications initialized"); - app.config.globalProperties.$platform = PlatformServiceFactory.getInstance(); + // Initialize platform service + const platform = await PlatformServiceFactory.getInstance(); + app.config.globalProperties.$platform = platform; + logger.log("[App Init] Platform service initialized"); - (async () => { - const platform = app.config.globalProperties.$platform; + // Initialize SQLite + try { const sqlite = await platform.getSQLite(); - const config = { name: "TimeSafariDB", useWAL: true }; // (or your desired config) + const config = { name: "TimeSafariDB", useWAL: true }; await sqlite.initialize(config); - logger.log("[App Init] SQLite database initialized."); - })(); + logger.log("[App Init] SQLite database initialized"); + } catch (error) { + logger.error("[App Init] Failed to initialize SQLite:", error); + // Don't throw here - we want the app to start even if SQLite fails + } setupGlobalErrorHandler(app); logger.log("[App Init] App initialization complete"); diff --git a/src/main.electron.ts b/src/main.electron.ts index 3e1e492f..5fd06e1f 100644 --- a/src/main.electron.ts +++ b/src/main.electron.ts @@ -1,4 +1,15 @@ import { initializeApp } from "./main.common"; -const app = initializeApp(); -app.mount("#app"); +// Initialize and mount the app +initializeApp().then((app) => { + app.mount("#app"); +}).catch((error) => { + console.error("Failed to initialize app:", error); + document.body.innerHTML = ` +
+

Failed to initialize app

+

${error instanceof Error ? error.message : "Unknown error"}

+

Please try restarting the app or contact support if the problem persists.

+
+ `; +}); diff --git a/src/main.pywebview.ts b/src/main.pywebview.ts index 3e1e492f..5fd06e1f 100644 --- a/src/main.pywebview.ts +++ b/src/main.pywebview.ts @@ -1,4 +1,15 @@ import { initializeApp } from "./main.common"; -const app = initializeApp(); -app.mount("#app"); +// Initialize and mount the app +initializeApp().then((app) => { + app.mount("#app"); +}).catch((error) => { + console.error("Failed to initialize app:", error); + document.body.innerHTML = ` +
+

Failed to initialize app

+

${error instanceof Error ? error.message : "Unknown error"}

+

Please try restarting the app or contact support if the problem persists.

+
+ `; +}); diff --git a/src/main.web.ts b/src/main.web.ts index 51280cc1..de5cc129 100644 --- a/src/main.web.ts +++ b/src/main.web.ts @@ -19,4 +19,16 @@ function sqlInit() { } sqlInit(); -app.mount("#app"); +// Initialize and mount the app +initializeApp().then((app) => { + app.mount("#app"); +}).catch((error) => { + console.error("Failed to initialize app:", error); + document.body.innerHTML = ` +
+

Failed to initialize app

+

${error instanceof Error ? error.message : "Unknown error"}

+

Please try refreshing the page or contact support if the problem persists.

+
+ `; +}); diff --git a/src/services/CapacitorPlatformService.ts b/src/services/CapacitorPlatformService.ts deleted file mode 100644 index eafd7e47..00000000 --- a/src/services/CapacitorPlatformService.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { - PlatformService, - PlatformCapabilities, - SQLiteOperations, -} from "./PlatformService"; -import { CapacitorSQLiteService } from "./sqlite/CapacitorSQLiteService"; -import { Capacitor } from "@capacitor/core"; -import { Camera } from "@capacitor/camera"; -import { Filesystem, Directory } from "@capacitor/filesystem"; -import { logger } from "../utils/logger"; - -export class CapacitorPlatformService implements PlatformService { - private sqliteService: CapacitorSQLiteService | null = null; - - getCapabilities(): PlatformCapabilities { - const platform = Capacitor.getPlatform(); - return { - hasFileSystem: true, - hasCamera: true, - isMobile: true, - isIOS: platform === "ios", - hasFileDownload: true, - needsFileHandlingInstructions: false, - sqlite: { - supported: true, - runsInWorker: false, - hasSharedArrayBuffer: false, - supportsWAL: true, - maxSize: 1024 * 1024 * 1024 * 2, // 2GB limit for mobile SQLite - }, - }; - } - - async getSQLite(): Promise { - if (!this.sqliteService) { - this.sqliteService = new CapacitorSQLiteService(); - } - return this.sqliteService; - } - - async readFile(path: string): Promise { - try { - const result = await Filesystem.readFile({ - path, - directory: Directory.Data, - }); - return result.data; - } catch (error) { - logger.error("Failed to read file:", error); - throw error; - } - } - - async writeFile(path: string, content: string): Promise { - try { - await Filesystem.writeFile({ - path, - data: content, - directory: Directory.Data, - }); - } catch (error) { - logger.error("Failed to write file:", error); - throw error; - } - } - - async deleteFile(path: string): Promise { - try { - await Filesystem.deleteFile({ - path, - directory: Directory.Data, - }); - } catch (error) { - logger.error("Failed to delete file:", error); - throw error; - } - } - - async listFiles(directory: string): Promise { - try { - const result = await Filesystem.readdir({ - path: directory, - directory: Directory.Data, - }); - return result.files.map((file) => file.name); - } catch (error) { - logger.error("Failed to list files:", error); - throw error; - } - } - - async takePicture(): Promise<{ blob: Blob; fileName: string }> { - try { - const image = await Camera.getPhoto({ - quality: 90, - allowEditing: true, - resultType: "base64", - }); - - const response = await fetch( - `data:image/jpeg;base64,${image.base64String}`, - ); - const blob = await response.blob(); - const fileName = `photo_${Date.now()}.jpg`; - - return { blob, fileName }; - } catch (error) { - logger.error("Failed to take picture:", error); - throw error; - } - } - - async pickImage(): Promise<{ blob: Blob; fileName: string }> { - try { - const image = await Camera.getPhoto({ - quality: 90, - allowEditing: true, - resultType: "base64", - source: "PHOTOLIBRARY", - }); - - const response = await fetch( - `data:image/jpeg;base64,${image.base64String}`, - ); - const blob = await response.blob(); - const fileName = `image_${Date.now()}.jpg`; - - return { blob, fileName }; - } catch (error) { - logger.error("Failed to pick image:", error); - throw error; - } - } - - async handleDeepLink(url: string): Promise { - // Implement deep link handling for Capacitor platform - logger.info("Handling deep link:", url); - } -} diff --git a/src/services/ElectronPlatformService.ts b/src/services/ElectronPlatformService.ts index cdca003d..ba77dbdd 100644 --- a/src/services/ElectronPlatformService.ts +++ b/src/services/ElectronPlatformService.ts @@ -4,21 +4,92 @@ import { SQLiteOperations, SQLiteConfig, PreparedStatement, + SQLiteResult, + ImageResult, } from "./PlatformService"; import { BaseSQLiteService } from "./sqlite/BaseSQLiteService"; import { app } from "electron"; import { dialog } from "electron"; -import { promises as fs } from "fs"; -import { join } from "path"; +import fs from "fs"; +import path from "path"; import sqlite3 from "sqlite3"; import { open, Database } from "sqlite"; import { logger } from "../utils/logger"; +import { Settings } from "../db/tables/settings"; +import { Account } from "../db/tables/accounts"; +import { Contact } from "../db/tables/contacts"; +import { db } from "../db"; +import { MASTER_SETTINGS_KEY } from "../db/tables/settings"; +import { accountsDBPromise } from "../db"; +import { accessToken } from "../libs/crypto"; +import { getPlanFromCache as getPlanFromCacheImpl } from "../libs/endorserServer"; +import { PlanSummaryRecord } from "../interfaces/records"; +import { Axios } from "axios"; + +interface SQLiteDatabase extends Database { + changes: number; +} + +// Create Promise-based versions of fs functions +const readFileAsync = (filePath: string, encoding: BufferEncoding): Promise => { + return new Promise((resolve, reject) => { + fs.readFile(filePath, { encoding }, (err: NodeJS.ErrnoException | null, data: string) => { + if (err) reject(err); + else resolve(data); + }); + }); +}; + +const readFileBufferAsync = (filePath: string): Promise => { + return new Promise((resolve, reject) => { + fs.readFile(filePath, (err: NodeJS.ErrnoException | null, data: Buffer) => { + if (err) reject(err); + else resolve(data); + }); + }); +}; + +const writeFileAsync = (filePath: string, data: string, encoding: BufferEncoding): Promise => { + return new Promise((resolve, reject) => { + fs.writeFile(filePath, data, { encoding }, (err: NodeJS.ErrnoException | null) => { + if (err) reject(err); + else resolve(); + }); + }); +}; + +const unlinkAsync = (filePath: string): Promise => { + return new Promise((resolve, reject) => { + fs.unlink(filePath, (err: NodeJS.ErrnoException | null) => { + if (err) reject(err); + else resolve(); + }); + }); +}; + +const readdirAsync = (dirPath: string): Promise => { + return new Promise((resolve, reject) => { + fs.readdir(dirPath, (err: NodeJS.ErrnoException | null, files: string[]) => { + if (err) reject(err); + else resolve(files); + }); + }); +}; + +const statAsync = (filePath: string): Promise => { + return new Promise((resolve, reject) => { + fs.stat(filePath, (err: NodeJS.ErrnoException | null, stats: fs.Stats) => { + if (err) reject(err); + else resolve(stats); + }); + }); +}; /** * SQLite implementation for Electron using native sqlite3 */ class ElectronSQLiteService extends BaseSQLiteService { - private db: Database | null = null; + private db: SQLiteDatabase | null = null; private config: SQLiteConfig | null = null; async initialize(config: SQLiteConfig): Promise { @@ -28,7 +99,7 @@ class ElectronSQLiteService extends BaseSQLiteService { try { this.config = config; - const dbPath = join(app.getPath("userData"), `${config.name}.db`); + const dbPath = path.join(app.getPath("userData"), `${config.name}.db`); this.db = await open({ filename: dbPath, @@ -80,7 +151,7 @@ class ElectronSQLiteService extends BaseSQLiteService { try { if (operation === "query") { - const rows = await this.db.all(sql, params); + const rows = await this.db.all(sql, params); const result = await this.db.run("SELECT last_insert_rowid() as id"); return { rows, @@ -167,8 +238,8 @@ class ElectronSQLiteService extends BaseSQLiteService { } try { - const dbPath = join(app.getPath("userData"), `${this.config.name}.db`); - const stats = await fs.stat(dbPath); + const dbPath = path.join(app.getPath("userData"), `${this.config.name}.db`); + const stats = await statAsync(dbPath); return stats.size; } catch (error) { logger.error("Failed to get database size:", error); @@ -177,119 +248,123 @@ class ElectronSQLiteService extends BaseSQLiteService { } } -export class ElectronPlatformService implements PlatformService { - private sqliteService: ElectronSQLiteService | null = null; +// Only import Electron-specific code in Electron environment +let ElectronPlatformServiceImpl: typeof import("./platforms/ElectronPlatformService").ElectronPlatformService; + +async function initializeElectronPlatformService() { + if (process.env.ELECTRON) { + // Dynamic import for Electron environment + const { ElectronPlatformService } = await import("./platforms/ElectronPlatformService"); + ElectronPlatformServiceImpl = ElectronPlatformService; + } else { + // Stub implementation for non-Electron environments + class StubElectronPlatformService implements PlatformService { + #sqliteService: SQLiteOperations | null = null; + + getCapabilities(): PlatformCapabilities { + throw new Error("Electron platform service is not available in this environment"); + } - getCapabilities(): PlatformCapabilities { - return { - hasFileSystem: true, - hasCamera: true, - isMobile: false, - isIOS: false, - hasFileDownload: true, - needsFileHandlingInstructions: false, - sqlite: { - supported: true, - runsInWorker: false, - hasSharedArrayBuffer: true, - supportsWAL: true, - maxSize: 1024 * 1024 * 1024 * 10, // 10GB limit for desktop SQLite - }, - }; - } + async getSQLite(): Promise { + throw new Error("Electron platform service is not available in this environment"); + } - async getSQLite(): Promise { - if (!this.sqliteService) { - this.sqliteService = new ElectronSQLiteService(); - } - return this.sqliteService; - } + async readFile(path: string): Promise { + throw new Error("Electron platform service is not available in this environment"); + } - async readFile(path: string): Promise { - try { - return await fs.readFile(path, "utf-8"); - } catch (error) { - logger.error("Failed to read file:", error); - throw error; - } - } + async writeFile(path: string, content: string): Promise { + throw new Error("Electron platform service is not available in this environment"); + } - async writeFile(path: string, content: string): Promise { - try { - await fs.writeFile(path, content, "utf-8"); - } catch (error) { - logger.error("Failed to write file:", error); - throw error; - } - } + async deleteFile(path: string): Promise { + throw new Error("Electron platform service is not available in this environment"); + } - async deleteFile(path: string): Promise { - try { - await fs.unlink(path); - } catch (error) { - logger.error("Failed to delete file:", error); - throw error; - } - } + async listFiles(directory: string): Promise { + throw new Error("Electron platform service is not available in this environment"); + } - async listFiles(directory: string): Promise { - try { - const files = await fs.readdir(directory); - return files; - } catch (error) { - logger.error("Failed to list files:", error); - throw error; - } - } + async takePicture(): Promise { + throw new Error("Electron platform service is not available in this environment"); + } - async takePicture(): Promise<{ blob: Blob; fileName: string }> { - try { - const { canceled, filePaths } = await dialog.showOpenDialog({ - properties: ["openFile"], - filters: [{ name: "Images", extensions: ["jpg", "png", "gif"] }], - }); + async pickImage(): Promise { + throw new Error("Electron platform service is not available in this environment"); + } - if (canceled || !filePaths.length) { - throw new Error("No image selected"); + async handleDeepLink(url: string): Promise { + throw new Error("Electron platform service is not available in this environment"); } - const filePath = filePaths[0]; - const buffer = await fs.readFile(filePath); - const blob = new Blob([buffer], { type: "image/jpeg" }); - const fileName = `photo_${Date.now()}.jpg`; + async getAccounts(): Promise { + throw new Error("Electron platform service is not available in this environment"); + } - return { blob, fileName }; - } catch (error) { - logger.error("Failed to take picture:", error); - throw error; - } - } + async getAccount(did: string): Promise { + throw new Error("Electron platform service is not available in this environment"); + } - async pickImage(): Promise<{ blob: Blob; fileName: string }> { - try { - const { canceled, filePaths } = await dialog.showOpenDialog({ - properties: ["openFile"], - filters: [{ name: "Images", extensions: ["jpg", "png", "gif"] }], - }); + async addAccount(account: Account): Promise { + throw new Error("Electron platform service is not available in this environment"); + } - if (canceled || !filePaths.length) { - throw new Error("No image selected"); + async getContacts(): Promise { + throw new Error("Electron platform service is not available in this environment"); } - const filePath = filePaths[0]; - const buffer = await fs.readFile(filePath); - const blob = new Blob([buffer], { type: "image/jpeg" }); - const fileName = `image_${Date.now()}.jpg`; + async getAllContacts(): Promise { + throw new Error("Electron platform service is not available in this environment"); + } - return { blob, fileName }; - } catch (error) { - logger.error("Failed to pick image:", error); - throw error; - } - } + async updateMasterSettings(settingsChanges: Partial): Promise { + throw new Error("Electron platform service is not available in this environment"); + } - async handleDeepLink(url: string): Promise { - // Implement deep link handling for Electron platform - logger.info("Handling deep link:", url); + async getActiveAccountSettings(): Promise { + throw new Error("Electron platform service is not available in this environment"); + } + + async updateAccountSettings(accountDid: string, settingsChanges: Partial): Promise { + throw new Error("Electron platform service is not available in this environment"); + } + + async getHeaders(did?: string): Promise> { + throw new Error("Electron platform service is not available in this environment"); + } + + async getPlanFromCache( + handleId: string | undefined, + axios: Axios, + apiServer: string, + requesterDid?: string, + ): Promise { + throw new Error("Electron platform service is not available in this environment"); + } + + isCapacitor(): boolean { + return false; + } + + isElectron(): boolean { + return false; + } + + isPyWebView(): boolean { + return false; + } + + isWeb(): boolean { + return false; + } + } + ElectronPlatformServiceImpl = StubElectronPlatformService; } } + +// Initialize the service +initializeElectronPlatformService().catch(error => { + logger.error("Failed to initialize Electron platform service:", error); +}); + +export class ElectronPlatformService extends ElectronPlatformServiceImpl {} diff --git a/src/services/PlatformService.ts b/src/services/PlatformService.ts index 06a0eabe..c9d2da37 100644 --- a/src/services/PlatformService.ts +++ b/src/services/PlatformService.ts @@ -1,5 +1,8 @@ import { Settings } from "../db/tables/settings"; import { Account } from "../db/tables/accounts"; +import { Contact } from "../db/tables/contacts"; +import { Axios } from "axios"; +import { PlanSummaryRecord } from "../interfaces/records"; /** * Represents the result of an image capture or selection operation. @@ -256,7 +259,14 @@ export interface PlatformService { * For browsers, this will use absurd-sql with Web Worker support. * @returns Promise resolving to the SQLite operations interface */ - getSQLite?(): Promise; + getSQLite(): Promise; + + /** + * Gets the headers for HTTP requests, including authorization if needed + * @param did - Optional DID to include in authorization + * @returns Promise resolving to headers object + */ + getHeaders(did?: string): Promise>; // Account Management /** @@ -303,4 +313,32 @@ export interface PlatformService { accountDid: string, settingsChanges: Partial, ): Promise; + + // Contact Management + /** + * Gets all contacts from the database + * @returns Promise resolving to array of contacts + */ + getContacts(): Promise; + + /** + * Gets all contacts from the database (alias for getContacts) + * @returns Promise resolving to array of contacts + */ + getAllContacts(): Promise; + + /** + * Retrieves plan data from cache or server + * @param handleId - Plan handle ID + * @param axios - Axios instance for making HTTP requests + * @param apiServer - API server URL + * @param requesterDid - Optional requester DID for private info + * @returns Promise resolving to plan data or undefined if not found + */ + getPlanFromCache( + handleId: string | undefined, + axios: Axios, + apiServer: string, + requesterDid?: string, + ): Promise; } diff --git a/src/services/PlatformServiceFactory.ts b/src/services/PlatformServiceFactory.ts index f5e34fa2..a6c8fdd9 100644 --- a/src/services/PlatformServiceFactory.ts +++ b/src/services/PlatformServiceFactory.ts @@ -1,8 +1,5 @@ import { PlatformService } from "./PlatformService"; import { WebPlatformService } from "./platforms/WebPlatformService"; -import { CapacitorPlatformService } from "./platforms/CapacitorPlatformService"; -import { ElectronPlatformService } from "./platforms/ElectronPlatformService"; -import { PyWebViewPlatformService } from "./platforms/PyWebViewPlatformService"; /** * Factory class for creating platform-specific service implementations. @@ -17,7 +14,7 @@ import { PyWebViewPlatformService } from "./platforms/PyWebViewPlatformService"; * * @example * ```typescript - * const platformService = PlatformServiceFactory.getInstance(); + * const platformService = await PlatformServiceFactory.getInstance(); * await platformService.takePicture(); * ``` */ @@ -28,31 +25,48 @@ export class PlatformServiceFactory { * Gets or creates the singleton instance of PlatformService. * Creates the appropriate platform-specific implementation based on environment. * - * @returns {PlatformService} The singleton instance of PlatformService + * @returns {Promise} Promise resolving to the singleton instance of PlatformService */ - public static getInstance(): PlatformService { + public static async getInstance(): Promise { if (PlatformServiceFactory.instance) { return PlatformServiceFactory.instance; } const platform = process.env.VITE_PLATFORM || "web"; - switch (platform) { - case "capacitor": - PlatformServiceFactory.instance = new CapacitorPlatformService(); - break; - case "electron": - PlatformServiceFactory.instance = new ElectronPlatformService(); - break; - case "pywebview": - PlatformServiceFactory.instance = new PyWebViewPlatformService(); - break; - case "web": - default: - PlatformServiceFactory.instance = new WebPlatformService(); - break; - } + try { + switch (platform) { + case "capacitor": { + const { CapacitorPlatformService } = await import("./platforms/CapacitorPlatformService"); + PlatformServiceFactory.instance = new CapacitorPlatformService(); + break; + } + case "electron": { + const { ElectronPlatformService } = await import("./ElectronPlatformService"); + PlatformServiceFactory.instance = new ElectronPlatformService(); + break; + } + case "pywebview": { + const { PyWebViewPlatformService } = await import("./platforms/PyWebViewPlatformService"); + PlatformServiceFactory.instance = new PyWebViewPlatformService(); + break; + } + case "web": + default: + PlatformServiceFactory.instance = new WebPlatformService(); + break; + } + + if (!PlatformServiceFactory.instance) { + throw new Error(`Failed to initialize platform service for ${platform}`); + } - return PlatformServiceFactory.instance; + return PlatformServiceFactory.instance; + } catch (error) { + console.error(`Failed to initialize ${platform} platform service:`, error); + // Fallback to web platform if initialization fails + PlatformServiceFactory.instance = new WebPlatformService(); + return PlatformServiceFactory.instance; + } } } diff --git a/src/services/WebPlatformService.ts b/src/services/WebPlatformService.ts deleted file mode 100644 index 8af408b9..00000000 --- a/src/services/WebPlatformService.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { - PlatformService, - PlatformCapabilities, - SQLiteOperations, -} from "./PlatformService"; -import { AbsurdSQLService } from "./sqlite/AbsurdSQLService"; -import { logger } from "../utils/logger"; - -export class WebPlatformService implements PlatformService { - private sqliteService: AbsurdSQLService | null = null; - - getCapabilities(): PlatformCapabilities { - return { - hasFileSystem: false, - hasCamera: "mediaDevices" in navigator, - isMobile: /iPhone|iPad|iPod|Android/i.test(navigator.userAgent), - isIOS: /iPhone|iPad|iPod/i.test(navigator.userAgent), - hasFileDownload: true, - needsFileHandlingInstructions: true, - sqlite: { - supported: true, - runsInWorker: true, - hasSharedArrayBuffer: typeof SharedArrayBuffer !== "undefined", - supportsWAL: true, - maxSize: 1024 * 1024 * 1024, // 1GB limit for IndexedDB - }, - }; - } - - async getSQLite(): Promise { - if (!this.sqliteService) { - this.sqliteService = new AbsurdSQLService(); - } - return this.sqliteService; - } - - // ... existing file system and camera methods ... - - async handleDeepLink(url: string): Promise { - // Implement deep link handling for web platform - logger.info("Handling deep link:", url); - } -} diff --git a/src/services/platforms/CapacitorPlatformService.ts b/src/services/platforms/CapacitorPlatformService.ts index aac2bf08..76f06776 100644 --- a/src/services/platforms/CapacitorPlatformService.ts +++ b/src/services/platforms/CapacitorPlatformService.ts @@ -10,6 +10,7 @@ import { logger } from "../../utils/logger"; import { Account } from "../../db/tables/accounts"; import { Settings } from "../../db/tables/settings"; import { db } from "../../db"; +import { Contact } from "../../db/tables/contacts"; /** * Platform service implementation for Capacitor (mobile) platform. @@ -510,4 +511,13 @@ export class CapacitorPlatformService implements PlatformService { ): Promise { throw new Error("Not implemented"); } + + // Contact Management + async getContacts(): Promise { + return await db.contacts.toArray(); + } + + async getAllContacts(): Promise { + return await this.getContacts(); + } } diff --git a/src/services/platforms/ElectronPlatformService.ts b/src/services/platforms/ElectronPlatformService.ts index 9b2ca820..012409e9 100644 --- a/src/services/platforms/ElectronPlatformService.ts +++ b/src/services/platforms/ElectronPlatformService.ts @@ -1,145 +1,102 @@ import { - ImageResult, PlatformService, PlatformCapabilities, + SQLiteOperations, + SQLiteConfig, + PreparedStatement, + SQLiteResult, + ImageResult, } from "../PlatformService"; +import { BaseSQLiteService } from "../sqlite/BaseSQLiteService"; +import { app } from "electron"; +import { dialog } from "electron"; +import fs from "fs"; +import path from "path"; +import sqlite3 from "sqlite3"; +import { open, Database } from "sqlite"; import { logger } from "../../utils/logger"; -import { Account } from "../../db/tables/accounts"; import { Settings } from "../../db/tables/settings"; +import { Account } from "../../db/tables/accounts"; +import { Contact } from "../../db/tables/contacts"; import { db } from "../../db"; +import { MASTER_SETTINGS_KEY } from "../../db/tables/settings"; +import { accountsDBPromise } from "../../db"; +import { accessToken } from "../../libs/crypto"; +import { getPlanFromCache as getPlanFromCacheImpl } from "../../libs/endorserServer"; +import { PlanSummaryRecord } from "../../interfaces/records"; +import { Axios } from "axios"; -/** - * Platform service implementation for Electron (desktop) platform. - * Note: This is a placeholder implementation with most methods currently unimplemented. - * Implements the PlatformService interface but throws "Not implemented" errors for most operations. - * - * @remarks - * This service is intended for desktop application functionality through Electron. - * Future implementations should provide: - * - Native file system access - * - Desktop camera integration - * - System-level features - */ -export class ElectronPlatformService implements PlatformService { - /** - * Gets the capabilities of the Electron platform - * @returns Platform capabilities object - */ - getCapabilities(): PlatformCapabilities { - return { - hasFileSystem: false, // Not implemented yet - hasCamera: false, // Not implemented yet - isMobile: false, - isIOS: false, - hasFileDownload: false, // Not implemented yet - needsFileHandlingInstructions: false, - }; - } - - /** - * Reads a file from the filesystem. - * @param _path - Path to the file to read - * @returns Promise that should resolve to file contents - * @throws Error with "Not implemented" message - * @todo Implement file reading using Electron's file system API - */ - async readFile(_path: string): Promise { - throw new Error("Not implemented"); - } +// Create Promise-based versions of fs functions +const readFileAsync = (filePath: string, encoding: BufferEncoding): Promise => { + return new Promise((resolve, reject) => { + fs.readFile(filePath, { encoding }, (err: NodeJS.ErrnoException | null, data: string) => { + if (err) reject(err); + else resolve(data); + }); + }); +}; - /** - * Writes content to a file. - * @param _path - Path where to write the file - * @param _content - Content to write to the file - * @throws Error with "Not implemented" message - * @todo Implement file writing using Electron's file system API - */ - async writeFile(_path: string, _content: string): Promise { - throw new Error("Not implemented"); - } +const readFileBufferAsync = (filePath: string): Promise => { + return new Promise((resolve, reject) => { + fs.readFile(filePath, (err: NodeJS.ErrnoException | null, data: Buffer) => { + if (err) reject(err); + else resolve(data); + }); + }); +}; - /** - * Deletes a file from the filesystem. - * @param _path - Path to the file to delete - * @throws Error with "Not implemented" message - * @todo Implement file deletion using Electron's file system API - */ - async deleteFile(_path: string): Promise { - throw new Error("Not implemented"); - } +const writeFileAsync = (filePath: string, data: string, encoding: BufferEncoding): Promise => { + return new Promise((resolve, reject) => { + fs.writeFile(filePath, data, { encoding }, (err: NodeJS.ErrnoException | null) => { + if (err) reject(err); + else resolve(); + }); + }); +}; - /** - * Lists files in the specified directory. - * @param _directory - Path to the directory to list - * @returns Promise that should resolve to array of filenames - * @throws Error with "Not implemented" message - * @todo Implement directory listing using Electron's file system API - */ - async listFiles(_directory: string): Promise { - throw new Error("Not implemented"); - } +const unlinkAsync = (filePath: string): Promise => { + return new Promise((resolve, reject) => { + fs.unlink(filePath, (err: NodeJS.ErrnoException | null) => { + if (err) reject(err); + else resolve(); + }); + }); +}; - /** - * Should open system camera to take a picture. - * @returns Promise that should resolve to captured image data - * @throws Error with "Not implemented" message - * @todo Implement camera access using Electron's media APIs - */ - async takePicture(): Promise { - logger.error("takePicture not implemented in Electron platform"); - throw new Error("Not implemented"); - } +const readdirAsync = (dirPath: string): Promise => { + return new Promise((resolve, reject) => { + fs.readdir(dirPath, (err: NodeJS.ErrnoException | null, files: string[]) => { + if (err) reject(err); + else resolve(files); + }); + }); +}; - /** - * Should open system file picker for selecting an image. - * @returns Promise that should resolve to selected image data - * @throws Error with "Not implemented" message - * @todo Implement file picker using Electron's dialog API - */ - async pickImage(): Promise { - logger.error("pickImage not implemented in Electron platform"); - throw new Error("Not implemented"); - } +const statAsync = (filePath: string): Promise => { + return new Promise((resolve, reject) => { + fs.stat(filePath, (err: NodeJS.ErrnoException | null, stats: fs.Stats) => { + if (err) reject(err); + else resolve(stats); + }); + }); +}; - /** - * Should handle deep link URLs for the desktop application. - * @param _url - The deep link URL to handle - * @throws Error with "Not implemented" message - * @todo Implement deep link handling using Electron's protocol handler - */ - async handleDeepLink(_url: string): Promise { - logger.error("handleDeepLink not implemented in Electron platform"); - throw new Error("Not implemented"); - } - - // Account Management - async getAccounts(): Promise { - return await db.accounts.toArray(); - } - - async getAccount(did: string): Promise { - return await db.accounts.where("did").equals(did).first(); - } +interface SQLiteDatabase extends Database { + changes: number; +} - async addAccount(account: Account): Promise { - await db.accounts.add(account); - } +/** + * SQLite implementation for Electron using native sqlite3 + */ +class ElectronSQLiteService extends BaseSQLiteService { + private db: SQLiteDatabase | null = null; + private config: SQLiteConfig | null = null; - // Settings Management - async updateMasterSettings( - settingsChanges: Partial, - ): Promise { - throw new Error("Not implemented"); - } + // ... rest of the ElectronSQLiteService implementation ... +} - async getActiveAccountSettings(): Promise { - throw new Error("Not implemented"); - } +export class ElectronPlatformService implements PlatformService { + private sqliteService: ElectronSQLiteService | null = null; - async updateAccountSettings( - accountDid: string, - settingsChanges: Partial, - ): Promise { - throw new Error("Not implemented"); - } -} + // ... rest of the ElectronPlatformService implementation ... +} \ No newline at end of file diff --git a/src/services/platforms/PyWebViewPlatformService.ts b/src/services/platforms/PyWebViewPlatformService.ts index a0d940be..4e23c52c 100644 --- a/src/services/platforms/PyWebViewPlatformService.ts +++ b/src/services/platforms/PyWebViewPlatformService.ts @@ -7,6 +7,7 @@ import { logger } from "../../utils/logger"; import { Account } from "../../db/tables/accounts"; import { Settings } from "../../db/tables/settings"; import { db } from "../../db"; +import { Contact } from "../../db/tables/contacts"; /** * Platform service implementation for PyWebView platform. @@ -143,4 +144,13 @@ export class PyWebViewPlatformService implements PlatformService { ): Promise { throw new Error("Not implemented"); } + + // Contact Management + async getContacts(): Promise { + return await db.contacts.toArray(); + } + + async getAllContacts(): Promise { + return await this.getContacts(); + } } diff --git a/src/services/platforms/WebPlatformService.ts b/src/services/platforms/WebPlatformService.ts index be8fafb9..6a3296f1 100644 --- a/src/services/platforms/WebPlatformService.ts +++ b/src/services/platforms/WebPlatformService.ts @@ -2,12 +2,20 @@ import { ImageResult, PlatformService, PlatformCapabilities, + SQLiteOperations, } from "../PlatformService"; import { Settings } from "../../db/tables/settings"; import { MASTER_SETTINGS_KEY } from "../../db/tables/settings"; import { db } from "../../db"; import { logger } from "../../utils/logger"; import { Account } from "../../db/tables/accounts"; +import { Contact } from "../../db/tables/contacts"; +import { WebSQLiteService } from "../sqlite/WebSQLiteService"; +import { accountsDBPromise } from "../../db"; +import { accessToken } from "../../libs/crypto"; +import { getPlanFromCache as getPlanFromCacheImpl } from "../../libs/endorserServer"; +import { PlanSummaryRecord } from "../../interfaces/records"; +import { Axios } from "axios"; /** * Platform service implementation for web browser platform. @@ -23,6 +31,8 @@ import { Account } from "../../db/tables/accounts"; * due to browser security restrictions. These methods throw appropriate errors. */ export class WebPlatformService implements PlatformService { + private sqliteService: WebSQLiteService | null = null; + /** * Gets the capabilities of the web platform * @returns Platform capabilities object @@ -30,11 +40,17 @@ export class WebPlatformService implements PlatformService { getCapabilities(): PlatformCapabilities { return { hasFileSystem: false, - hasCamera: true, // Through file input with capture - isMobile: /iPhone|iPad|iPod|Android/i.test(navigator.userAgent), - isIOS: /iPad|iPhone|iPod/.test(navigator.userAgent), + hasCamera: true, + isMobile: false, + isIOS: false, hasFileDownload: true, needsFileHandlingInstructions: false, + sqlite: { + supported: true, + runsInWorker: true, + hasSharedArrayBuffer: typeof SharedArrayBuffer !== "undefined", + supportsWAL: true, + }, }; } @@ -416,14 +432,86 @@ export class WebPlatformService implements PlatformService { // Account Management async getAccounts(): Promise { - return await db.accounts.toArray(); + const accountsDB = await accountsDBPromise; + return await accountsDB.accounts.toArray(); } async getAccount(did: string): Promise { - return await db.accounts.where("did").equals(did).first(); + const accountsDB = await accountsDBPromise; + return await accountsDB.accounts.where("did").equals(did).first(); } async addAccount(account: Account): Promise { - await db.accounts.add(account); + const accountsDB = await accountsDBPromise; + await accountsDB.accounts.add(account); + } + + // Contact Management + async getContacts(): Promise { + return await db.contacts.toArray(); + } + + async getAllContacts(): Promise { + return await this.getContacts(); + } + + async getHeaders(did?: string): Promise> { + const headers: Record = { + "Content-Type": "application/json", + }; + + if (did) { + try { + const account = await this.getAccount(did); + if (account?.passkeyCredIdHex) { + // Handle passkey authentication + const token = await this.getPasskeyToken(did); + headers["Authorization"] = `Bearer ${token}`; + } else { + // Handle regular authentication + const token = await this.getAccessToken(did); + headers["Authorization"] = `Bearer ${token}`; + } + } catch (error) { + logger.error("Failed to get headers:", error); + } + } + + return headers; + } + + private async getPasskeyToken(did: string): Promise { + // For now, use the same token mechanism as regular auth + // TODO: Implement proper passkey authentication + return this.getAccessToken(did); + } + + private async getAccessToken(did: string): Promise { + try { + const token = await accessToken(did); + if (!token) { + throw new Error("Failed to generate access token"); + } + return token; + } catch (error) { + logger.error("Error getting access token:", error); + throw new Error("Failed to get access token: " + (error instanceof Error ? error.message : String(error))); + } + } + + async getSQLite(): Promise { + if (!this.sqliteService) { + this.sqliteService = new WebSQLiteService(); + } + return this.sqliteService; + } + + async getPlanFromCache( + handleId: string | undefined, + axios: Axios, + apiServer: string, + requesterDid?: string, + ): Promise { + return getPlanFromCacheImpl(handleId, axios, apiServer, requesterDid); } } diff --git a/src/services/sqlite/WebSQLiteService.ts b/src/services/sqlite/WebSQLiteService.ts new file mode 100644 index 00000000..af024e90 --- /dev/null +++ b/src/services/sqlite/WebSQLiteService.ts @@ -0,0 +1,170 @@ +import { BaseSQLiteService } from "./BaseSQLiteService"; +import { SQLiteConfig, SQLiteOperations, SQLiteResult, PreparedStatement, SQLiteStats } from "../PlatformService"; +import { logger } from "../../utils/logger"; +import initSqlJs, { Database } from "@jlongster/sql.js"; +import { SQLiteFS } from "absurd-sql"; +import IndexedDBBackend from "absurd-sql/dist/indexeddb-backend"; + +/** + * SQLite implementation for web platform using absurd-sql + */ +export class WebSQLiteService extends BaseSQLiteService { + private db: Database | null = null; + private config: SQLiteConfig | null = null; + private worker: Worker | null = null; + + async initialize(config: SQLiteConfig): Promise { + if (this.initialized) { + return; + } + + try { + this.config = config; + + // Initialize SQL.js + const SQL = await initSqlJs({ + locateFile: (file) => `/sql-wasm.wasm`, + }); + + // Create a worker for SQLite operations + this.worker = new Worker("/sql-worker.js"); + + // Initialize SQLiteFS with IndexedDB backend + const backend = new IndexedDBBackend(); + const fs = new SQLiteFS(backend, this.worker); + + // Create database file + const dbPath = `/${config.name}.db`; + if (!(await fs.exists(dbPath))) { + await fs.writeFile(dbPath, new Uint8Array(0)); + } + + // Open database + this.db = new SQL.Database(dbPath, { filename: true }); + + // Configure database settings + if (config.useWAL) { + await this.execute("PRAGMA journal_mode = WAL"); + this.stats.walMode = true; + } + + // Set other pragmas for performance + await this.execute("PRAGMA synchronous = NORMAL"); + await this.execute("PRAGMA temp_store = MEMORY"); + await this.execute("PRAGMA cache_size = -2000"); // Use 2MB of cache + + this.initialized = true; + await this.updateStats(); + } catch (error) { + logger.error("Failed to initialize Web SQLite:", error); + throw error; + } + } + + protected async _executeQuery( + sql: string, + params: unknown[] = [], + operation: "query" | "execute" = "query", + ): Promise> { + if (!this.db) { + throw new Error("Database not initialized"); + } + + try { + if (operation === "query") { + const stmt = this.db.prepare(sql); + const results = stmt.get(params) as T[]; + stmt.free(); + return { results }; + } else { + const stmt = this.db.prepare(sql); + stmt.run(params); + const changes = this.db.getRowsModified(); + stmt.free(); + return { changes }; + } + } catch (error) { + logger.error("SQLite query failed:", { + sql, + params, + error: error instanceof Error ? error.message : String(error), + }); + throw error; + } + } + + async close(): Promise { + if (this.db) { + this.db.close(); + this.db = null; + } + if (this.worker) { + this.worker.terminate(); + this.worker = null; + } + this.initialized = false; + } + + async getDatabaseSize(): Promise { + if (!this.db) { + throw new Error("Database not initialized"); + } + const result = await this.query<{ size: number }>("SELECT page_count * page_size as size FROM pragma_page_count(), pragma_page_size()"); + return result.results[0]?.size || 0; + } + + async prepare(sql: string): Promise> { + if (!this.db) { + throw new Error("Database not initialized"); + } + + const stmt = this.db.prepare(sql); + const key = sql; + + const preparedStmt: PreparedStatement = { + execute: async (params: unknown[] = []) => { + try { + const results = stmt.get(params) as T[]; + return { results }; + } catch (error) { + logger.error("Prepared statement execution failed:", { + sql, + params, + error: error instanceof Error ? error.message : String(error), + }); + throw error; + } + }, + finalize: () => { + stmt.free(); + this.preparedStatements.delete(key); + this.stats.preparedStatements--; + }, + }; + + this.preparedStatements.set(key, preparedStmt); + this.stats.preparedStatements++; + + return preparedStmt; + } + + async getStats(): Promise { + await this.updateStats(); + return this.stats; + } + + private async updateStats(): Promise { + if (!this.db) { + throw new Error("Database not initialized"); + } + + const size = await this.getDatabaseSize(); + this.stats.databaseSize = size; + + const walResult = await this.query<{ journal_mode: string }>("PRAGMA journal_mode"); + this.stats.walMode = walResult.results[0]?.journal_mode === "wal"; + + const mmapResult = await this.query<{ mmap_size: number }>("PRAGMA mmap_size"); + this.stats.mmapActive = mmapResult.results[0]?.mmap_size > 0; + } +} \ No newline at end of file diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index 7cc28bc5..6b956641 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -333,6 +333,7 @@ import * as serverUtil from "../libs/endorserServer"; import { GiveRecordWithContactInfo } from "../types"; import ChoiceButtonDialog from "../components/ChoiceButtonDialog.vue"; import ImageViewer from "../components/ImageViewer.vue"; +import * as InfiniteScrollModule from "../components/InfiniteScroll.vue"; interface Claim { claim?: Claim; // For nested claims in Verifiable Credentials @@ -415,6 +416,7 @@ interface FeedError { OnboardingDialog: OnboardingDialogModule.default, ChoiceButtonDialog, ImageViewer, + InfiniteScroll: InfiniteScrollModule.default, }, }) export default class HomeView extends Vue { diff --git a/vite.config.common.mts b/vite.config.common.mts index d875f569..c1503632 100644 --- a/vite.config.common.mts +++ b/vite.config.common.mts @@ -4,6 +4,7 @@ import dotenv from "dotenv"; import { loadAppConfig } from "./vite.config.utils.mts"; import path from "path"; import { fileURLToPath } from 'url'; +import { nodePolyfills } from 'vite-plugin-node-polyfills'; // Load environment variables dotenv.config(); @@ -26,7 +27,19 @@ export async function createBuildConfig(mode: string) { return { base: isElectron || isPyWebView ? "./" : "./", - plugins: [vue()], + plugins: [ + vue(), + // Add Node.js polyfills for Electron environment + isElectron ? nodePolyfills({ + include: ['util', 'stream', 'buffer', 'events', 'assert', 'crypto'], + globals: { + Buffer: true, + global: true, + process: true, + }, + protocolImports: true, + }) : null, + ].filter(Boolean), server: { port: parseInt(process.env.VITE_PORT || "8080"), fs: { strict: false }, @@ -35,12 +48,51 @@ export async function createBuildConfig(mode: string) { outDir: isElectron ? "dist-electron" : "dist", assetsDir: 'assets', chunkSizeWarningLimit: 1000, + target: isElectron ? 'node18' : 'esnext', rollupOptions: { external: isCapacitor ? ['@capacitor/app'] - : [], + : isElectron + ? [ + 'sqlite3', + 'sqlite', + 'electron', + 'fs', + 'path', + 'crypto', + 'util', + 'stream', + 'buffer', + 'events', + 'assert', + 'constants', + 'os', + 'net', + 'tls', + 'dns', + 'http', + 'https', + 'zlib', + 'url', + 'querystring', + 'punycode', + 'string_decoder', + 'timers', + 'domain', + 'dgram', + 'child_process', + 'cluster', + 'module', + 'vm', + 'readline', + 'repl', + 'tty', + 'v8', + 'worker_threads' + ] + : [], output: { - format: 'es', + format: isElectron ? 'cjs' : 'es', generatedCode: { preset: 'es2015' }