Browse Source

WIP: Platform-specific service architecture and configuration refactor

This commit introduces a platform-specific service architecture and configuration
system, with the following changes:

- Created platform-specific service implementations for database backup:
  - Web: Uses URL.createObjectURL and download link
  - Mobile: Uses Capacitor Filesystem and Share APIs
  - Base: Abstract DatabaseBackupService class

- Implemented PlatformServiceFactory for dynamic service loading:
  - Singleton pattern for factory management
  - Dynamic imports based on platform environment
  - Error handling for service loading failures

- Refactored Vite configuration:
  - Split into base and platform-specific configs
  - Added environment-based platform detection
  - Improved build optimization settings

- Enhanced error handling:
  - Added type-safe error interfaces
  - Improved error message formatting
  - Better error logging with context

- Updated AccountViewView:
  - Moved profile management to ProfileSection component
  - Improved type safety in error handling
  - Enhanced database backup functionality

Note: This is a work in progress. Some features may need additional testing
and refinement before being production-ready.
db-backup-cross-platform
Matthew Raymer 2 months ago
parent
commit
42d706b1fb
  1. 1
      .env.electron
  2. 1
      .env.mobile
  3. 1
      .env.web
  4. 86
      package-lock.json
  5. 5
      package.json
  6. 257
      src/components/ProfileSection.vue
  7. 22
      src/interfaces/identifier.ts
  8. 1
      src/interfaces/index.ts
  9. 4
      src/libs/crypto/vc/index.ts
  10. 26
      src/services/DatabaseBackupService.ts
  11. 40
      src/services/PlatformServiceFactory.ts
  12. 105
      src/services/ProfileService.ts
  13. 88
      src/services/RateLimitsService.ts
  14. 18
      src/services/platforms/electron/DatabaseBackupService.ts
  15. 31
      src/services/platforms/mobile/DatabaseBackupService.ts
  16. 22
      src/services/platforms/web/DatabaseBackupService.ts
  17. 64
      src/types/capacitor.d.ts
  18. 7
      src/types/index.ts
  19. 96
      src/types/interfaces.ts
  20. 514
      src/views/AccountViewView.vue
  21. 6
      src/views/ProjectsView.vue
  22. 3
      tsconfig.json
  23. 46
      vite.config.base.ts
  24. 28
      vite.config.mobile.ts
  25. 60
      vite.config.ts
  26. 27
      vite.config.web.ts

1
.env.electron

@ -0,0 +1 @@
PLATFORM=electron

1
.env.mobile

@ -0,0 +1 @@
PLATFORM=mobile

1
.env.web

@ -0,0 +1 @@
PLATFORM=web

86
package-lock.json

@ -12,9 +12,12 @@
"@capacitor/app": "^6.0.0", "@capacitor/app": "^6.0.0",
"@capacitor/cli": "^6.2.0", "@capacitor/cli": "^6.2.0",
"@capacitor/core": "^6.2.0", "@capacitor/core": "^6.2.0",
"@capacitor/filesystem": "^6.0.3",
"@capacitor/ios": "^6.2.0", "@capacitor/ios": "^6.2.0",
"@capacitor/share": "^6.0.3",
"@dicebear/collection": "^5.4.1", "@dicebear/collection": "^5.4.1",
"@dicebear/core": "^5.4.1", "@dicebear/core": "^5.4.1",
"@electron/remote": "^2.1.2",
"@ethersproject/hdnode": "^5.7.0", "@ethersproject/hdnode": "^5.7.0",
"@ethersproject/wallet": "^5.8.0", "@ethersproject/wallet": "^5.8.0",
"@fortawesome/fontawesome-svg-core": "^6.5.1", "@fortawesome/fontawesome-svg-core": "^6.5.1",
@ -89,7 +92,7 @@
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/leaflet": "^1.9.8", "@types/leaflet": "^1.9.8",
"@types/luxon": "^3.4.2", "@types/luxon": "^3.4.2",
"@types/node": "^20.14.11", "@types/node": "^20.17.30",
"@types/node-fetch": "^2.6.12", "@types/node-fetch": "^2.6.12",
"@types/ramda": "^0.29.11", "@types/ramda": "^0.29.11",
"@types/sqlite3": "^3.1.11", "@types/sqlite3": "^3.1.11",
@ -2878,6 +2881,15 @@
"tslib": "^2.1.0" "tslib": "^2.1.0"
} }
}, },
"node_modules/@capacitor/filesystem": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/@capacitor/filesystem/-/filesystem-6.0.3.tgz",
"integrity": "sha512-PdIP/yOGAbG1lq1wbFbSPhXQ9/5lpTpeiok2NneawJOk6UXvy9W7QZXRo7wXAP7J6FdzU7bKfOORRXpOJpgXyw==",
"license": "MIT",
"peerDependencies": {
"@capacitor/core": "^6.0.0"
}
},
"node_modules/@capacitor/ios": { "node_modules/@capacitor/ios": {
"version": "6.2.1", "version": "6.2.1",
"resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-6.2.1.tgz", "resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-6.2.1.tgz",
@ -2887,6 +2899,15 @@
"@capacitor/core": "^6.2.0" "@capacitor/core": "^6.2.0"
} }
}, },
"node_modules/@capacitor/share": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/@capacitor/share/-/share-6.0.3.tgz",
"integrity": "sha512-BkNM73Ix+yxQ7fkni8CrrGcp1kSl7u+YNoPLwWKQ1MuQ5Uav0d+CT8M67ie+3dc4jASmegnzlC6tkTmFcPTLeA==",
"license": "MIT",
"peerDependencies": {
"@capacitor/core": "^6.0.0"
}
},
"node_modules/@cbor-extract/cbor-extract-darwin-arm64": { "node_modules/@cbor-extract/cbor-extract-darwin-arm64": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.2.0.tgz", "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.2.0.tgz",
@ -3881,7 +3902,6 @@
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz",
"integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"debug": "^4.1.1", "debug": "^4.1.1",
@ -3903,7 +3923,6 @@
"version": "8.1.0", "version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"graceful-fs": "^4.2.0", "graceful-fs": "^4.2.0",
@ -3918,7 +3937,6 @@
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
"integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
"dev": true,
"license": "MIT", "license": "MIT",
"optionalDependencies": { "optionalDependencies": {
"graceful-fs": "^4.1.6" "graceful-fs": "^4.1.6"
@ -3928,7 +3946,6 @@
"version": "6.3.1", "version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"license": "ISC", "license": "ISC",
"bin": { "bin": {
"semver": "bin/semver.js" "semver": "bin/semver.js"
@ -3938,7 +3955,6 @@
"version": "0.1.2", "version": "0.1.2",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 4.0.0" "node": ">= 4.0.0"
@ -4069,6 +4085,15 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/@electron/remote": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@electron/remote/-/remote-2.1.2.tgz",
"integrity": "sha512-EPwNx+nhdrTBxyCqXt/pftoQg/ybtWDW3DUWHafejvnB1ZGGfMpv6e15D8KeempocjXe78T7WreyGGb3mlZxdA==",
"license": "MIT",
"peerDependencies": {
"electron": ">= 13.0.0"
}
},
"node_modules/@electron/universal": { "node_modules/@electron/universal": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.1.tgz", "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.1.tgz",
@ -9115,7 +9140,6 @@
"version": "4.6.0", "version": "4.6.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
"integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=10" "node": ">=10"
@ -9390,7 +9414,6 @@
"version": "4.0.6", "version": "4.0.6",
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
"integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"defer-to-connect": "^2.0.0" "defer-to-connect": "^2.0.0"
@ -9803,7 +9826,6 @@
"version": "6.0.3", "version": "6.0.3",
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
"integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/http-cache-semantics": "*", "@types/http-cache-semantics": "*",
@ -9872,7 +9894,6 @@
"version": "4.0.4", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz",
"integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/istanbul-lib-coverage": { "node_modules/@types/istanbul-lib-coverage": {
@ -9929,7 +9950,6 @@
"version": "3.1.4", "version": "3.1.4",
"resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz",
"integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/node": "*" "@types/node": "*"
@ -10055,7 +10075,6 @@
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz",
"integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/node": "*" "@types/node": "*"
@ -10195,7 +10214,6 @@
"version": "2.10.3", "version": "2.10.3",
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
"integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
@ -12667,7 +12685,6 @@
"resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz",
"integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==",
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true "optional": true
}, },
@ -13022,7 +13039,6 @@
"version": "5.0.4", "version": "5.0.4",
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
"integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=10.6.0" "node": ">=10.6.0"
@ -13032,7 +13048,6 @@
"version": "7.0.4", "version": "7.0.4",
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz",
"integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"clone-response": "^1.0.2", "clone-response": "^1.0.2",
@ -13617,7 +13632,6 @@
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz",
"integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"mimic-response": "^1.0.0" "mimic-response": "^1.0.0"
@ -14899,7 +14913,6 @@
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
"integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=10" "node": ">=10"
@ -14909,7 +14922,7 @@
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"dev": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"es-define-property": "^1.0.0", "es-define-property": "^1.0.0",
@ -14936,7 +14949,7 @@
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
"integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
"dev": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"define-data-property": "^1.0.1", "define-data-property": "^1.0.1",
@ -15052,7 +15065,6 @@
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
"integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true "optional": true
}, },
@ -15473,7 +15485,6 @@
"version": "33.4.8", "version": "33.4.8",
"resolved": "https://registry.npmjs.org/electron/-/electron-33.4.8.tgz", "resolved": "https://registry.npmjs.org/electron/-/electron-33.4.8.tgz",
"integrity": "sha512-dy/92HufGG66PslDMlXuK6uhO+70tgiZ4esReTZgDcZ0E67jCJ7S4/et4yZSEjXiT7IyjZTf72QwQbTpANxW4g==", "integrity": "sha512-dy/92HufGG66PslDMlXuK6uhO+70tgiZ4esReTZgDcZ0E67jCJ7S4/et4yZSEjXiT7IyjZTf72QwQbTpANxW4g==",
"dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -15883,7 +15894,6 @@
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
"integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==",
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true "optional": true
}, },
@ -16919,7 +16929,6 @@
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
"integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
"dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"debug": "^4.1.1", "debug": "^4.1.1",
@ -17813,7 +17822,6 @@
"version": "5.2.0", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
"integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"pump": "^3.0.0" "pump": "^3.0.0"
@ -18001,7 +18009,6 @@
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz",
"integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==",
"dev": true,
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
@ -18020,7 +18027,6 @@
"version": "7.0.1", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz",
"integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==",
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
@ -18037,7 +18043,6 @@
"version": "0.13.1", "version": "0.13.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
"integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==",
"dev": true,
"license": "(MIT OR CC0-1.0)", "license": "(MIT OR CC0-1.0)",
"optional": true, "optional": true,
"engines": { "engines": {
@ -18067,7 +18072,7 @@
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
"integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
"dev": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"define-properties": "^1.2.1", "define-properties": "^1.2.1",
@ -18117,7 +18122,6 @@
"version": "11.8.6", "version": "11.8.6",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz",
"integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@sindresorhus/is": "^4.0.0", "@sindresorhus/is": "^4.0.0",
@ -18224,7 +18228,7 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"dev": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"es-define-property": "^1.0.0" "es-define-property": "^1.0.0"
@ -18413,7 +18417,6 @@
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==",
"devOptional": true,
"license": "BSD-2-Clause" "license": "BSD-2-Clause"
}, },
"node_modules/http-errors": { "node_modules/http-errors": {
@ -18463,7 +18466,6 @@
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
"integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"quick-lru": "^5.1.1", "quick-lru": "^5.1.1",
@ -19887,7 +19889,6 @@
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/json-parse-better-errors": { "node_modules/json-parse-better-errors": {
@ -19949,7 +19950,7 @@
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
"dev": true, "devOptional": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/json5": { "node_modules/json5": {
@ -20142,7 +20143,6 @@
"version": "4.5.4", "version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"json-buffer": "3.0.1" "json-buffer": "3.0.1"
@ -20861,7 +20861,6 @@
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
"integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@ -21407,7 +21406,6 @@
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
"integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==",
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
@ -22818,7 +22816,6 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
"integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=4" "node": ">=4"
@ -23514,7 +23511,6 @@
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
"integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=10" "node": ">=10"
@ -23843,7 +23839,7 @@
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
"dev": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@ -24012,7 +24008,6 @@
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
"integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@ -24832,7 +24827,6 @@
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
"devOptional": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=0.4.0" "node": ">=0.4.0"
@ -25272,7 +25266,6 @@
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=10" "node": ">=10"
@ -26508,7 +26501,6 @@
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
"integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/resolve-from": { "node_modules/resolve-from": {
@ -26544,7 +26536,6 @@
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz",
"integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"lowercase-keys": "^2.0.0" "lowercase-keys": "^2.0.0"
@ -26717,7 +26708,6 @@
"version": "2.15.4", "version": "2.15.4",
"resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz",
"integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==",
"dev": true,
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
@ -27002,7 +26992,6 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
"integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==",
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true "optional": true
}, },
@ -28722,7 +28711,6 @@
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz",
"integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==",
"dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"debug": "^4.1.0" "debug": "^4.1.0"

5
package.json

@ -46,9 +46,12 @@
"@capacitor/app": "^6.0.0", "@capacitor/app": "^6.0.0",
"@capacitor/cli": "^6.2.0", "@capacitor/cli": "^6.2.0",
"@capacitor/core": "^6.2.0", "@capacitor/core": "^6.2.0",
"@capacitor/filesystem": "^6.0.3",
"@capacitor/ios": "^6.2.0", "@capacitor/ios": "^6.2.0",
"@capacitor/share": "^6.0.3",
"@dicebear/collection": "^5.4.1", "@dicebear/collection": "^5.4.1",
"@dicebear/core": "^5.4.1", "@dicebear/core": "^5.4.1",
"@electron/remote": "^2.1.2",
"@ethersproject/hdnode": "^5.7.0", "@ethersproject/hdnode": "^5.7.0",
"@ethersproject/wallet": "^5.8.0", "@ethersproject/wallet": "^5.8.0",
"@fortawesome/fontawesome-svg-core": "^6.5.1", "@fortawesome/fontawesome-svg-core": "^6.5.1",
@ -123,7 +126,7 @@
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/leaflet": "^1.9.8", "@types/leaflet": "^1.9.8",
"@types/luxon": "^3.4.2", "@types/luxon": "^3.4.2",
"@types/node": "^20.14.11", "@types/node": "^20.17.30",
"@types/node-fetch": "^2.6.12", "@types/node-fetch": "^2.6.12",
"@types/ramda": "^0.29.11", "@types/ramda": "^0.29.11",
"@types/sqlite3": "^3.1.11", "@types/sqlite3": "^3.1.11",

257
src/components/ProfileSection.vue

@ -0,0 +1,257 @@
/** * @file ProfileSection.vue * @description Component for managing user
profile information * @author Matthew Raymer * @version 1.0.0 */
<template>
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8">
<div v-if="loading" class="text-center mb-2">
<font-awesome
icon="spinner"
class="fa-spin text-slate-400"
></font-awesome>
Loading profile...
</div>
<div v-else class="flex items-center mb-2">
<span class="font-bold">Public Profile</span>
<font-awesome
icon="circle-info"
class="text-slate-400 fa-fw ml-2 cursor-pointer"
@click="showProfileInfo"
/>
</div>
<textarea
v-model="profileDesc"
class="w-full h-32 p-2 border border-slate-300 rounded-md"
placeholder="Write something about yourself for the public..."
:readonly="loading || saving"
:class="{ 'bg-slate-100': loading || saving }"
></textarea>
<div class="flex items-center mb-4" @click="toggleLocation">
<input v-model="includeLocation" type="checkbox" class="mr-2" />
<label for="includeLocation">Include Location</label>
</div>
<div v-if="includeLocation" class="mb-4 aspect-video">
<p class="text-sm mb-2 text-slate-500">
For your security, choose a location nearby but not exactly at your
place.
</p>
<l-map
ref="profileMap"
class="!z-40 rounded-md"
@click="handleMapClick"
@ready="onMapReady"
>
<l-tile-layer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
layer-type="base"
name="OpenStreetMap"
/>
<l-marker
v-if="latitude && longitude"
:lat-lng="[latitude, longitude]"
@click="confirmEraseLocation"
/>
</l-map>
</div>
<div v-if="!loading && !saving">
<div class="flex justify-between items-center">
<button
class="mt-2 px-4 py-2 bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white rounded-md"
:disabled="loading || saving"
:class="{
'opacity-50 cursor-not-allowed': loading || saving,
}"
@click="saveProfile"
>
Save Profile
</button>
<button
class="mt-2 px-4 py-2 bg-gradient-to-b from-red-400 to-red-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white rounded-md"
:disabled="loading || saving"
:class="{
'opacity-50 cursor-not-allowed':
loading || saving || (!profileDesc && !includeLocation),
}"
@click="confirmDeleteProfile"
>
Delete Profile
</button>
</div>
</div>
<div v-else-if="loading">Loading...</div>
<div v-else>Saving...</div>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop, Emit } from "vue-facing-decorator";
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
import { ProfileService } from "../services/ProfileService";
import { logger } from "../utils/logger";
@Component({
components: {
LMap,
LMarker,
LTileLayer,
},
})
export default class ProfileSection extends Vue {
@Prop({ required: true }) activeDid!: string;
@Prop({ required: true }) partnerApiServer!: string;
@Emit("profile-updated") profileUpdated() {}
loading = true;
saving = false;
profileDesc = "";
latitude = 0;
longitude = 0;
includeLocation = false;
zoom = 2;
async mounted() {
await this.loadProfile();
}
async loadProfile() {
try {
const profile = await ProfileService.loadProfile(
this.activeDid,
this.partnerApiServer,
);
if (profile) {
this.profileDesc = profile.description || "";
this.latitude = profile.location?.lat || 0;
this.longitude = profile.location?.lng || 0;
this.includeLocation = !!(this.latitude && this.longitude);
}
} catch (error) {
logger.error("Error loading profile:", error);
this.$notify({
group: "alert",
type: "danger",
title: "Error Loading Profile",
text: "Your server profile is not available.",
});
} finally {
this.loading = false;
}
}
async saveProfile() {
this.saving = true;
try {
await ProfileService.saveProfile(this.activeDid, this.partnerApiServer, {
description: this.profileDesc,
location: this.includeLocation
? {
lat: this.latitude,
lng: this.longitude,
}
: undefined,
});
this.$notify({
group: "alert",
type: "success",
title: "Profile Saved",
text: "Your profile has been updated successfully.",
});
this.profileUpdated();
} catch (error) {
logger.error("Error saving profile:", error);
this.$notify({
group: "alert",
type: "danger",
title: "Error Saving Profile",
text: "There was an error saving your profile.",
});
} finally {
this.saving = false;
}
}
toggleLocation() {
this.includeLocation = !this.includeLocation;
if (!this.includeLocation) {
this.latitude = 0;
this.longitude = 0;
this.zoom = 2;
}
}
handleMapClick(event: { latlng: { lat: number; lng: number } }) {
this.latitude = event.latlng.lat;
this.longitude = event.latlng.lng;
}
onMapReady(map: L.Map) {
const zoom = this.latitude && this.longitude ? 12 : 2;
map.setView([this.latitude, this.longitude], zoom);
}
confirmEraseLocation() {
this.$notify({
group: "modal",
type: "confirm",
title: "Erase Marker",
text: "Are you sure you don't want to mark a location? This will erase the current location.",
onYes: () => {
this.latitude = 0;
this.longitude = 0;
this.zoom = 2;
this.includeLocation = false;
},
});
}
async confirmDeleteProfile() {
this.$notify({
group: "modal",
type: "confirm",
title: "Delete Profile",
text: "Are you sure you want to delete your public profile? This will remove your description and location from the server, and it cannot be undone.",
onYes: this.deleteProfile,
});
}
async deleteProfile() {
this.saving = true;
try {
await ProfileService.deleteProfile(this.activeDid, this.partnerApiServer);
this.profileDesc = "";
this.latitude = 0;
this.longitude = 0;
this.includeLocation = false;
this.$notify({
group: "alert",
type: "success",
title: "Profile Deleted",
text: "Your profile has been deleted successfully.",
});
this.profileUpdated();
} catch (error) {
logger.error("Error deleting profile:", error);
this.$notify({
group: "alert",
type: "danger",
title: "Error Deleting Profile",
text: "There was an error deleting your profile.",
});
} finally {
this.saving = false;
}
}
showProfileInfo() {
this.$notify({
group: "alert",
type: "info",
title: "Public Profile Information",
text: "This data will be published for all to see, so be careful what your write. Your ID will only be shared with people who you allow to see your activity.",
});
}
}
</script>

22
src/interfaces/identifier.ts

@ -0,0 +1,22 @@
/**
* Interface for a Decentralized Identifier (DID)
* @author Matthew Raymer
*/
import { KeyMeta } from "../libs/crypto/vc";
export interface IKey {
id: string;
type: string;
controller: string;
ethereumAddress: string;
publicKeyHex: string;
privateKeyHex: string;
meta?: KeyMeta;
}
export interface IIdentifier {
did: string;
keys: IKey[];
services: any[];
}

1
src/interfaces/index.ts

@ -5,3 +5,4 @@ export * from "./limits";
export * from "./records"; export * from "./records";
export * from "./user"; export * from "./user";
export * from "./deepLinks"; export * from "./deepLinks";
export * from "./identifier";

4
src/libs/crypto/vc/index.ts

@ -38,6 +38,10 @@ export interface KeyMeta {
* The Webauthn credential ID in hex, if this is from a passkey * The Webauthn credential ID in hex, if this is from a passkey
*/ */
passkeyCredIdHex?: string; passkeyCredIdHex?: string;
/**
* The derivation path for the key
*/
derivationPath?: string;
} }
const ethLocalResolver = new Resolver({ ethr: didEthLocalResolver }); const ethLocalResolver = new Resolver({ ethr: didEthLocalResolver });

26
src/services/DatabaseBackupService.ts

@ -0,0 +1,26 @@
/**
* @file DatabaseBackupService.ts
* @description Base service class for handling database backup operations
* @author Matthew Raymer
* @version 1.0.0
*/
import { PlatformServiceFactory } from "./PlatformServiceFactory";
export class DatabaseBackupService {
protected async handleBackup(): Promise<void> {
throw new Error(
"handleBackup must be implemented by platform-specific service",
);
}
public static async createAndShareBackup(
base64Data: string,
arrayBuffer: ArrayBuffer,
blob: Blob,
): Promise<void> {
const factory = PlatformServiceFactory.getInstance();
const service = await factory.createDatabaseBackupService();
await service.handleBackup(base64Data, arrayBuffer, blob);
}
}

40
src/services/PlatformServiceFactory.ts

@ -0,0 +1,40 @@
/**
* @file PlatformServiceFactory.ts
* @description Factory for creating platform-specific service implementations
* @author Matthew Raymer
* @version 1.0.0
*/
import { DatabaseBackupService } from "./DatabaseBackupService";
export class PlatformServiceFactory {
private static instance: PlatformServiceFactory;
private platform: string;
private constructor() {
this.platform = import.meta.env.VITE_PLATFORM || "web";
}
public static getInstance(): PlatformServiceFactory {
if (!PlatformServiceFactory.instance) {
PlatformServiceFactory.instance = new PlatformServiceFactory();
}
return PlatformServiceFactory.instance;
}
public async createDatabaseBackupService(): Promise<DatabaseBackupService> {
try {
// Use Vite's dynamic import for platform-specific implementation
const { default: PlatformService } = await import(
`./platforms/${this.platform}/DatabaseBackupService`
);
return new PlatformService();
} catch (error) {
console.error(
`Failed to load platform-specific service for ${this.platform}:`,
error,
);
throw error;
}
}
}

105
src/services/ProfileService.ts

@ -0,0 +1,105 @@
/**
* @file ProfileService.ts
* @description Service class for handling user profile operations
* @author Matthew Raymer
* @version 1.0.0
*/
import { logger } from "../utils/logger";
import { getHeaders } from "../libs/endorserServer";
import type { UserProfile } from "@/types/interfaces";
export class ProfileService {
/**
* Saves a user profile to the server
* @param activeDid - The user's active DID
* @param partnerApiServer - The partner API server URL
* @param profile - The profile data to save
* @returns Promise<void>
*/
static async saveProfile(
activeDid: string,
partnerApiServer: string,
profile: Partial<UserProfile>,
): Promise<void> {
try {
const headers = await getHeaders(activeDid);
const response = await fetch(
`${partnerApiServer}/api/partner/userProfile`,
{
method: "POST",
headers,
body: JSON.stringify(profile),
},
);
if (!response.ok) {
throw new Error(`Failed to save profile: ${response.statusText}`);
}
} catch (error) {
logger.error("Error saving profile:", error);
throw error;
}
}
/**
* Deletes a user profile from the server
* @param activeDid - The user's active DID
* @param partnerApiServer - The partner API server URL
* @returns Promise<void>
*/
static async deleteProfile(
activeDid: string,
partnerApiServer: string,
): Promise<void> {
try {
const headers = await getHeaders(activeDid);
const response = await fetch(
`${partnerApiServer}/api/partner/userProfile`,
{
method: "DELETE",
headers,
},
);
if (!response.ok) {
throw new Error(`Failed to delete profile: ${response.statusText}`);
}
} catch (error) {
logger.error("Error deleting profile:", error);
throw error;
}
}
/**
* Loads a user profile from the server
* @param activeDid - The user's active DID
* @param partnerApiServer - The partner API server URL
* @returns Promise<UserProfile | null>
*/
static async loadProfile(
activeDid: string,
partnerApiServer: string,
): Promise<UserProfile | null> {
try {
const headers = await getHeaders(activeDid);
const response = await fetch(
`${partnerApiServer}/api/partner/userProfileForIssuer/${activeDid}`,
{ headers },
);
if (response.status === 404) {
return null;
}
if (!response.ok) {
throw new Error(`Failed to load profile: ${response.statusText}`);
}
return await response.json();
} catch (error) {
logger.error("Error loading profile:", error);
throw error;
}
}
}

88
src/services/RateLimitsService.ts

@ -0,0 +1,88 @@
/**
* @file RateLimitsService.ts
* @description Service class for handling rate limit operations
* @author Matthew Raymer
* @version 1.0.0
*/
import { logger } from "../utils/logger";
import { getHeaders } from "../libs/endorserServer";
import type { EndorserRateLimits, ImageRateLimits } from "../interfaces/limits";
export class RateLimitsService {
/**
* Fetches rate limits for a given DID
* @param apiServer - The API server URL
* @param activeDid - The user's active DID
* @returns Promise<EndorserRateLimits>
*/
static async fetchRateLimits(
apiServer: string,
activeDid: string,
): Promise<EndorserRateLimits> {
try {
const headers = await getHeaders(activeDid);
const response = await fetch(
`${apiServer}/api/endorser/rateLimits/${activeDid}`,
{ headers },
);
if (!response.ok) {
throw new Error(`Failed to fetch rate limits: ${response.statusText}`);
}
return await response.json();
} catch (error) {
logger.error("Error fetching rate limits:", error);
throw error;
}
}
/**
* Fetches image rate limits for a given DID
* @param apiServer - The API server URL
* @param activeDid - The user's active DID
* @returns Promise<ImageRateLimits>
*/
static async fetchImageRateLimits(
apiServer: string,
activeDid: string,
): Promise<ImageRateLimits> {
try {
const headers = await getHeaders(activeDid);
const response = await fetch(
`${apiServer}/api/endorser/imageRateLimits/${activeDid}`,
{ headers },
);
if (!response.ok) {
throw new Error(
`Failed to fetch image rate limits: ${response.statusText}`,
);
}
return await response.json();
} catch (error) {
logger.error("Error fetching image rate limits:", error);
throw error;
}
}
/**
* Formats rate limit error messages
* @param error - The error object
* @returns string
*/
static formatRateLimitError(error: unknown): string {
if (error instanceof Error) {
return error.message;
}
if (typeof error === "object" && error !== null) {
const err = error as {
response?: { data?: { error?: { message?: string } } };
};
return err.response?.data?.error?.message || "An unknown error occurred";
}
return "An unknown error occurred";
}
}

18
src/services/platforms/electron/DatabaseBackupService.ts

@ -0,0 +1,18 @@
import { DatabaseBackupService } from "../../DatabaseBackupService";
import { dialog } from "electron";
import * as fs from "fs";
import * as path from "path";
export default class ElectronDatabaseBackupService extends DatabaseBackupService {
protected async handleBackup(base64Data: string): Promise<void> {
const { filePath } = await dialog.showSaveDialog({
title: "Save Database Backup",
defaultPath: path.join(process.env.HOME || "", "database-backup.json"),
filters: [{ name: "JSON", extensions: ["json"] }],
});
if (filePath) {
fs.writeFileSync(filePath, base64Data, "base64");
}
}
}

31
src/services/platforms/mobile/DatabaseBackupService.ts

@ -0,0 +1,31 @@
/**
* @file DatabaseBackupService.ts
* @description Mobile-specific implementation of the DatabaseBackupService
* @author Matthew Raymer
* @version 1.0.0
*/
import { DatabaseBackupService } from "../../DatabaseBackupService";
import { Filesystem } from "@capacitor/filesystem";
import { Share } from "@capacitor/share";
export default class MobileDatabaseBackupService extends DatabaseBackupService {
protected async handleBackup(base64Data: string): Promise<void> {
// Mobile platform handling
const fileName = `database-backup-${new Date().toISOString()}.json`;
const path = `backups/${fileName}`;
await Filesystem.writeFile({
path,
data: base64Data,
directory: "CACHE",
recursive: true,
});
await Share.share({
title: "Database Backup",
text: "Here's your database backup",
url: path,
});
}
}

22
src/services/platforms/web/DatabaseBackupService.ts

@ -0,0 +1,22 @@
/**
* @file DatabaseBackupService.ts
* @description Web-specific implementation of the DatabaseBackupService
* @author Matthew Raymer
* @version 1.0.0
*/
import { DatabaseBackupService } from "../../DatabaseBackupService";
export default class WebDatabaseBackupService extends DatabaseBackupService {
protected async handleBackup(blob: Blob): Promise<void> {
// Web platform handling
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `database-backup-${new Date().toISOString()}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
}

64
src/types/capacitor.d.ts

@ -0,0 +1,64 @@
/**
* Type declarations for Capacitor modules used in the application.
* @author Matthew Raymer
*/
declare module "@capacitor/filesystem" {
export interface FileWriteOptions {
path: string;
data: string;
directory?: string;
encoding?: string;
recursive?: boolean;
}
export interface FileReadResult {
data: string;
}
export interface FileDeleteOptions {
path: string;
directory?: string;
}
export interface FilesystemDirectory {
Cache: "CACHE";
Documents: "DOCUMENTS";
Data: "DATA";
External: "EXTERNAL";
ExternalStorage: "EXTERNAL_STORAGE";
}
export interface Filesystem {
writeFile(options: FileWriteOptions): Promise<void>;
readFile(options: {
path: string;
directory?: string;
}): Promise<FileReadResult>;
deleteFile(options: FileDeleteOptions): Promise<void>;
}
export const Filesystem: Filesystem;
export const Directory: FilesystemDirectory;
export const Encoding: {
UTF8: "utf8";
ASCII: "ascii";
UTF16: "utf16";
};
}
declare module "@capacitor/share" {
export interface ShareOptions {
title?: string;
text?: string;
url?: string;
dialogTitle?: string;
files?: string[];
}
export interface Share {
share(options: ShareOptions): Promise<void>;
}
export const Share: Share;
}

7
src/types/index.ts

@ -1,3 +1,10 @@
/**
* Index file for all type declarations.
* @author Matthew Raymer
*/
export * from "./interfaces";
import { GiveSummaryRecord, GiveVerifiableCredential } from "interfaces"; import { GiveSummaryRecord, GiveVerifiableCredential } from "interfaces";
export interface GiveRecordWithContactInfo extends GiveSummaryRecord { export interface GiveRecordWithContactInfo extends GiveSummaryRecord {

96
src/types/interfaces.ts

@ -0,0 +1,96 @@
/**
* Type declarations for custom interfaces used in the application.
* @author Matthew Raymer
*/
import { GiveVerifiableCredential } from "../interfaces";
export interface IIdentifier {
did: string;
provider: string;
keys: Array<{
kid: string;
kms: string;
type: string;
publicKeyHex: string;
meta?: any;
}>;
services: Array<{
id: string;
type: string;
serviceEndpoint: string;
description?: string;
}>;
}
export interface ExportProgress {
status: "preparing" | "exporting" | "complete" | "error";
message?: string;
error?: Error;
}
export interface UserProfile {
/** User's profile description */
description: string;
/** User's location information */
location?: {
/** Latitude coordinate */
lat: number;
/** Longitude coordinate */
lng: number;
};
/** User's given name */
givenName?: string;
/** User's family name */
familyName?: string;
}
export interface ErrorResponse {
error: string;
message: string;
statusCode: number;
}
export interface LeafletMouseEvent {
latlng: {
lat: number;
lng: number;
};
}
export interface GiveRecordWithContactInfo {
type?: string;
agentDid: string;
amount: number;
amountConfirmed: number;
description: string;
fullClaim: GiveVerifiableCredential;
fulfillsHandleId: string;
fulfillsPlanHandleId?: string;
fulfillsType?: string;
handleId: string;
issuedAt: string;
issuerDid: string;
jwtId: string;
providerPlanHandleId?: string;
recipientDid: string;
unit: string;
giver: {
known: boolean;
displayName: string;
profileImageUrl?: string;
};
issuer: {
known: boolean;
displayName: string;
profileImageUrl?: string;
};
receiver: {
known: boolean;
displayName: string;
profileImageUrl?: string;
};
providerPlanName?: string;
recipientProjectName?: string;
image?: string;
}

514
src/views/AccountViewView.vue

@ -57,12 +57,7 @@
> >
<button <button
class="inline-block text-md uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md" class="inline-block text-md uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
@click=" @click="showNameDialog"
() =>
($refs.userNameDialog as UserNameDialog).open(
(name) => (givenName = name),
)
"
> >
Set Your Name Set Your Name
</button> </button>
@ -83,7 +78,7 @@
/> />
</span> </span>
<div v-else class="text-center"> <div v-else class="text-center">
<div class @click="openImageDialog()"> <div @click="openImageDialog()">
<font-awesome <font-awesome
icon="image-portrait" icon="image-portrait"
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-2 rounded-l" class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-2 rounded-l"
@ -266,100 +261,12 @@
</div> </div>
<!-- User Profile --> <!-- User Profile -->
<div <ProfileSection
v-if="isRegistered" v-if="isRegistered"
class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8" :active-did="activeDid"
> :partner-api-server="partnerApiServer"
<div v-if="loadingProfile" class="text-center mb-2"> @profile-updated="handleProfileUpdate"
<font-awesome
icon="spinner"
class="fa-spin text-slate-400"
></font-awesome>
Loading profile...
</div>
<div v-else class="flex items-center mb-2">
<span class="font-bold">Public Profile</span>
<font-awesome
icon="circle-info"
class="text-slate-400 fa-fw ml-2 cursor-pointer"
@click="showProfileInfo"
/>
</div>
<textarea
v-model="userProfileDesc"
class="w-full h-32 p-2 border border-slate-300 rounded-md"
placeholder="Write something about yourself for the public..."
:readonly="loadingProfile || savingProfile"
:class="{ 'bg-slate-100': loadingProfile || savingProfile }"
></textarea>
<div class="flex items-center mb-4" @click="toggleUserProfileLocation">
<input
v-model="includeUserProfileLocation"
type="checkbox"
class="mr-2"
/>
<label for="includeUserProfileLocation">Include Location</label>
</div>
<div v-if="includeUserProfileLocation" class="mb-4 aspect-video">
<p class="text-sm mb-2 text-slate-500">
For your security, choose a location nearby but not exactly at your
place.
</p>
<l-map
ref="profileMap"
class="!z-40 rounded-md"
@click="
(event: LeafletMouseEvent) => {
userProfileLatitude = event.latlng.lat;
userProfileLongitude = event.latlng.lng;
}
"
@ready="onMapReady"
>
<l-tile-layer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
layer-type="base"
name="OpenStreetMap"
/>
<l-marker
v-if="userProfileLatitude && userProfileLongitude"
:lat-lng="[userProfileLatitude, userProfileLongitude]"
@click="confirmEraseLatLong()"
/> />
</l-map>
</div>
<div v-if="!loadingProfile && !savingProfile">
<div class="flex justify-between items-center">
<button
class="mt-2 px-4 py-2 bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white rounded-md"
:disabled="loadingProfile || savingProfile"
:class="{
'opacity-50 cursor-not-allowed': loadingProfile || savingProfile,
}"
@click="saveProfile"
>
Save Profile
</button>
<button
class="mt-2 px-4 py-2 bg-gradient-to-b from-red-400 to-red-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white rounded-md"
:disabled="loadingProfile || savingProfile"
:class="{
'opacity-50 cursor-not-allowed':
loadingProfile ||
savingProfile ||
(!userProfileDesc && !includeUserProfileLocation),
}"
@click="confirmDeleteProfile"
>
Delete Profile
</button>
</div>
</div>
<div v-else-if="loadingProfile">Loading...</div>
<div v-else>Saving...</div>
</div>
<div <div
v-if="activeDid" v-if="activeDid"
@ -930,15 +837,20 @@ import { AxiosError } from "axios";
import { Buffer } from "buffer/"; import { Buffer } from "buffer/";
import Dexie from "dexie"; import Dexie from "dexie";
import "dexie-export-import"; import "dexie-export-import";
import { ImportProgress } from "dexie-export-import";
import { LeafletMouseEvent } from "leaflet";
import * as R from "ramda"; import * as R from "ramda";
import { IIdentifier } from "@veramo/core"; import type { IIdentifier, UserProfile } from "@/types/interfaces";
import { ref } from "vue"; import { ref } from "vue";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { RouteLocationNormalizedLoaded, Router } from "vue-router"; import { RouteLocationNormalizedLoaded, Router } from "vue-router";
import { useClipboard } from "@vueuse/core"; import { useClipboard } from "@vueuse/core";
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet"; import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
import type { EndorserRateLimits, ImageRateLimits } from "../interfaces/limits";
import {
clearPasskeyToken,
errorStringForLog,
getHeaders,
tokenExpiryTimeDescription,
} from "../libs/endorserServer";
import EntityIcon from "../components/EntityIcon.vue"; import EntityIcon from "../components/EntityIcon.vue";
import ImageMethodDialog from "../components/ImageMethodDialog.vue"; import ImageMethodDialog from "../components/ImageMethodDialog.vue";
@ -966,32 +878,51 @@ import {
DEFAULT_PASSKEY_EXPIRATION_MINUTES, DEFAULT_PASSKEY_EXPIRATION_MINUTES,
MASTER_SETTINGS_KEY, MASTER_SETTINGS_KEY,
} from "../db/tables/settings"; } from "../db/tables/settings";
import {
clearPasskeyToken,
EndorserRateLimits,
ErrorResponse,
errorStringForLog,
fetchEndorserRateLimits,
fetchImageRateLimits,
getHeaders,
ImageRateLimits,
tokenExpiryTimeDescription,
} from "../libs/endorserServer";
import { import {
DAILY_CHECK_TITLE, DAILY_CHECK_TITLE,
DIRECT_PUSH_TITLE, DIRECT_PUSH_TITLE,
retrieveAccountMetadata, retrieveAccountMetadata,
} from "../libs/util"; } from "../libs/util";
import { UserProfile } from "@/libs/partnerServer";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
import { ExportProgress as DexieExportProgress } from "dexie-export-import";
import { DatabaseBackupService } from "../services/DatabaseBackupService";
import { ProfileService } from "../services/ProfileService";
import { RateLimitsService } from "../services/RateLimitsService";
import ProfileSection from "../components/ProfileSection.vue";
const inputImportFileNameRef = ref<Blob>(); const inputImportFileNameRef = ref<Blob>();
// Update the error type definitions
interface ErrorDetail {
message?: string;
[key: string]: unknown;
}
interface ApiErrorResponse {
error: string | ErrorDetail;
[key: string]: unknown;
}
interface ApiError {
status?: number;
response?: {
data?: ApiErrorResponse;
};
message?: string;
}
interface AxiosErrorDetail {
status: number;
response?: {
data?: ApiErrorResponse;
};
message?: string;
}
@Component({ @Component({
components: { components: {
EntityIcon, EntityIcon,
ImageMethodDialog, ImageMethodDialog,
LeafletMouseEvent,
LMap, LMap,
LMarker, LMarker,
LTileLayer, LTileLayer,
@ -999,6 +930,7 @@ const inputImportFileNameRef = ref<Blob>();
QuickNav, QuickNav,
TopMessage, TopMessage,
UserNameDialog, UserNameDialog,
ProfileSection,
}, },
}) })
export default class AccountViewView extends Vue { export default class AccountViewView extends Vue {
@ -1091,11 +1023,11 @@ export default class AccountViewView extends Vue {
this.includeUserProfileLocation = true; this.includeUserProfileLocation = true;
} }
} else { } else {
// won't get here because axios throws an error instead throw new Error("Unable to load profile.");
throw Error("Unable to load profile.");
} }
} catch (error) { } catch (error: unknown) {
if (error.status === 404) { const typedError = error as { status?: number };
if (typedError.status === 404) {
// this is ok: the profile is not yet created // this is ok: the profile is not yet created
} else { } else {
logConsoleAndDb( logConsoleAndDb(
@ -1259,7 +1191,7 @@ export default class AccountViewView extends Vue {
}); });
} }
readableDate(timeStr: string) { readableDate(timeStr?: string): string {
return timeStr ? timeStr.substring(0, timeStr.indexOf("T")) : "?"; return timeStr ? timeStr.substring(0, timeStr.indexOf("T")) : "?";
} }
@ -1440,109 +1372,75 @@ export default class AccountViewView extends Vue {
} }
/** /**
* Asynchronously exports the database into a downloadable JSON file. * Exports the database to a JSON file, handling platform-specific requirements.
* *
* @throws Will notify the user if there is an export error. * @internal
*/ * @callGraph
public async exportDatabase() { * - Called by: template click handler
try { * - Calls: Filesystem.writeFile(), Share.share(), URL.createObjectURL()
// Generate the blob from the database
const blob = await this.generateDatabaseBlob();
// Create a temporary URL for the blob
this.downloadUrl = this.createBlobURL(blob);
// Trigger the download
this.downloadDatabaseBackup(this.downloadUrl);
// Notify the user that the download has started
this.notifyDownloadStarted();
// Revoke the temporary URL -- after a pause to avoid DuckDuckGo download failure
setTimeout(() => URL.revokeObjectURL(this.downloadUrl), 1000);
} catch (error) {
this.handleExportError(error);
}
}
/**
* Generates a blob object representing the database.
* *
* @returns {Promise<Blob>} The generated blob object. * @chain
*/ * 1. Generate database blob
private async generateDatabaseBlob(): Promise<Blob> { * 2. Convert to base64 for mobile platforms
return await db.export({ prettyJson: true }); * 3. Handle platform-specific export:
} * - Mobile: Use Filesystem API and Share API
* - Web: Use URL.createObjectURL and download link
/** * - Electron: Use dialog and fs
* Creates a temporary URL for a blob object.
* *
* @param {Blob} blob - The blob object. * @requires
* @returns {string} The temporary URL for the blob. * - db: Dexie database instance
*/ * - Filesystem API (for mobile)
private createBlobURL(blob: Blob): string { * - Share API (for mobile)
return URL.createObjectURL(blob); * - fs module (for Electron)
} * - dialog module (for Electron)
/**
* Triggers the download of the database backup.
* *
* @param {string} url - The temporary URL for the blob. * @modifies
* - downloadLinkRef: Sets href and triggers download
* - State: Updates UI feedback
*/ */
private downloadDatabaseBackup(url: string) { async exportDatabase() {
const downloadAnchor = this.$refs.downloadLink as HTMLAnchorElement; try {
downloadAnchor.href = url; // Generate database blob
downloadAnchor.download = `${db.name}-backup.json`; const blob = await (Dexie as any).export(db, {
downloadAnchor.click(); // doesn't work for some browsers, eg. DuckDuckGo prettyJson: true,
} });
public computedStartDownloadLinkClassNames() { // Convert blob to base64 for mobile platforms
return { const arrayBuffer = await blob.arrayBuffer();
hidden: this.downloadUrl, const base64Data = Buffer.from(arrayBuffer).toString("base64");
};
}
public computedDownloadLinkClassNames() { await DatabaseBackupService.createAndShareBackup(
return { base64Data,
hidden: !this.downloadUrl, arrayBuffer,
}; blob,
} );
/**
* Notifies the user that the download has started.
*/
private notifyDownloadStarted() {
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "success", type: "success",
title: "Download Started", title: "Export Complete",
text: "See your downloads directory for the backup. It is in the Dexie format.", text: "Your database has been exported successfully.",
}, },
-1, 5000,
); );
} catch (error: unknown) {
if (error instanceof Error) {
const errorMessage = error.message;
this.limitsMessage = errorMessage || "Bad server response.";
logger.error("Got bad response retrieving limits:", error);
} else {
this.limitsMessage = "Got an error retrieving limits.";
logger.error("Got some error retrieving limits:", error);
}
} }
/**
* Handles errors during the database export process.
*
* @param {Error} error - The error object.
*/
private handleExportError(error: unknown) {
logger.error("Export Error:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Export Error",
text: "There was an error exporting the data.",
},
3000,
);
} }
async uploadImportFile(event: Event) { async uploadImportFile(event: Event) {
inputImportFileNameRef.value = (event.target as EventTarget).files[0]; const target = event.target as HTMLInputElement;
if (target.files) {
inputImportFileNameRef.value = target.files[0];
}
} }
showContactImport() { showContactImport() {
@ -1614,17 +1512,16 @@ export default class AccountViewView extends Vue {
reader.readAsText(inputImportFileNameRef.value as Blob); reader.readAsText(inputImportFileNameRef.value as Blob);
} }
private progressCallback(progress: ImportProgress) { private progressCallback(progress: DexieExportProgress) {
logger.log( logger.log(
`Import progress: ${progress.completedRows} of ${progress.totalRows} rows completed.`, `Export progress: ${progress.completedTables} of ${progress.totalTables} tables completed.`,
); );
if (progress.done) { if (progress.done) {
// console.log(`Imported ${progress.completedTables} tables.`);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "success", type: "success",
title: "Import Complete", title: "Export Complete",
text: "", text: "",
}, },
5000, 5000,
@ -1654,48 +1551,18 @@ export default class AccountViewView extends Vue {
this.limitsMessage = ""; this.limitsMessage = "";
try { try {
const resp = await fetchEndorserRateLimits( const response = await RateLimitsService.fetchRateLimits(
this.apiServer, this.apiServer,
this.axios,
did, did,
); );
if (resp.status === 200) { this.endorserLimits = response;
this.endorserLimits = resp.data; } catch (error: unknown) {
if (!this.isRegistered) { this.limitsMessage = RateLimitsService.formatRateLimitError(error);
// the user was not known to be registered, but now they are (because we got no error) so let's record it logger.error("Error fetching rate limits:", error);
try { } finally {
await updateAccountSettings(did, { isRegistered: true });
this.isRegistered = true;
} catch (err) {
logger.error("Got an error updating settings:", err);
this.$notify(
{
group: "alert",
type: "danger",
title: "Update Error",
text: "Unable to update your settings. Check claim limits again.",
},
5000,
);
}
}
try {
const imageResp = await fetchImageRateLimits(this.axios, did);
if (imageResp.status === 200) {
this.imageLimits = imageResp.data;
} else {
this.limitsMessage = "You don't have access to upload images.";
}
} catch {
this.limitsMessage = "You cannot upload images.";
}
}
} catch (error) {
this.handleRateLimitsError(error);
}
this.loadingLimits = false; this.loadingLimits = false;
} }
}
/** /**
* Handles errors that occur while fetching rate limits. * Handles errors that occur while fetching rate limits.
@ -1704,25 +1571,61 @@ export default class AccountViewView extends Vue {
*/ */
private handleRateLimitsError(error: unknown) { private handleRateLimitsError(error: unknown) {
if (error instanceof AxiosError) { if (error instanceof AxiosError) {
if (error.status == 400 || error.status == 404) { const axiosError = error as AxiosErrorDetail;
// no worries: they probably just aren't registered and don't have any limits if (axiosError.status === 400 || axiosError.status === 404) {
logger.log( logger.log(
"Got 400 or 404 response retrieving limits which probably means they're not registered:", "Got 400 or 404 response retrieving limits which probably means they're not registered:",
error, error,
); );
this.limitsMessage = "No limits were found, so no actions are allowed."; this.limitsMessage = "No limits were found, so no actions are allowed.";
} else { } else {
const data = error.response?.data as ErrorResponse; const data = axiosError.response?.data as ApiErrorResponse;
this.limitsMessage = const errorMessage =
(data?.error?.message as string) || "Bad server response."; typeof data?.error === "string"
? data.error
: (data?.error as ErrorDetail)?.message || "Bad server response.";
this.limitsMessage = errorMessage;
logger.error("Got bad response retrieving limits:", error); logger.error("Got bad response retrieving limits:", error);
} }
} else if (this.isApiError(error)) {
this.limitsMessage = this.getErrorMessage(error);
logger.error("Got API error retrieving limits:", error);
} else { } else {
this.limitsMessage = "Got an error retrieving limits."; this.limitsMessage = "Got an error retrieving limits.";
logger.error("Got some error retrieving limits:", error); logger.error("Got some error retrieving limits:", error);
} }
} }
private isApiError(error: unknown): error is ApiError {
return (
typeof error === "object" &&
error !== null &&
"status" in error &&
"response" in error
);
}
private getErrorMessage(error: ApiError): string {
if (error.response?.data) {
const data = error.response.data;
if (typeof data.error === "string") {
return data.error;
}
return (data.error as ErrorDetail)?.message || "Bad server response.";
}
return error.message || "An unexpected error occurred";
}
private handleError(error: unknown): string {
if (this.isApiError(error)) {
return this.getErrorMessage(error);
}
if (error instanceof Error) {
return error.message;
}
return "An unknown error occurred";
}
async onClickSaveApiServer() { async onClickSaveApiServer() {
await db.open(); await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, { await db.settings.update(MASTER_SETTINGS_KEY, {
@ -1877,30 +1780,16 @@ export default class AccountViewView extends Vue {
async saveProfile() { async saveProfile() {
this.savingProfile = true; this.savingProfile = true;
try { try {
const headers = await getHeaders(this.activeDid); await ProfileService.saveProfile(this.activeDid, this.partnerApiServer, {
const payload: UserProfile = {
description: this.userProfileDesc, description: this.userProfileDesc,
}; location: this.includeUserProfileLocation
if (this.userProfileLatitude && this.userProfileLongitude) { ? {
payload.locLat = this.userProfileLatitude; lat: this.userProfileLatitude,
payload.locLon = this.userProfileLongitude; lng: this.userProfileLongitude,
} else if (this.includeUserProfileLocation) {
this.$notify(
{
group: "alert",
type: "toast",
title: "",
text: "No profile location is saved.",
},
3000,
);
} }
const response = await this.axios.post( : undefined,
this.partnerApiServer + "/api/partner/userProfile", });
payload,
{ headers },
);
if (response.status === 201) {
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@ -1910,17 +1799,9 @@ export default class AccountViewView extends Vue {
}, },
3000, 3000,
); );
} else { } catch (error: unknown) {
// won't get here because axios throws an error on non-success const errorMessage = this.handleAxiosError(error);
throw Error("Profile not saved");
}
} catch (error) {
logConsoleAndDb("Error saving profile: " + errorStringForLog(error)); logConsoleAndDb("Error saving profile: " + errorStringForLog(error));
const errorMessage: string =
error.response?.data?.error?.message ||
error.response?.data?.error ||
error.message ||
"There was an error saving your profile.";
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@ -1982,12 +1863,7 @@ export default class AccountViewView extends Vue {
async deleteProfile() { async deleteProfile() {
this.savingProfile = true; this.savingProfile = true;
try { try {
const headers = await getHeaders(this.activeDid); await ProfileService.deleteProfile(this.activeDid, this.partnerApiServer);
const response = await this.axios.delete(
this.partnerApiServer + "/api/partner/userProfile",
{ headers },
);
if (response.status === 204) {
this.userProfileDesc = ""; this.userProfileDesc = "";
this.userProfileLatitude = 0; this.userProfileLatitude = 0;
this.userProfileLongitude = 0; this.userProfileLongitude = 0;
@ -2001,16 +1877,9 @@ export default class AccountViewView extends Vue {
}, },
3000, 3000,
); );
} else { } catch (error: unknown) {
throw Error("Profile not deleted"); const errorMessage = this.handleAxiosError(error);
}
} catch (error) {
logConsoleAndDb("Error deleting profile: " + errorStringForLog(error)); logConsoleAndDb("Error deleting profile: " + errorStringForLog(error));
const errorMessage: string =
error.response?.data?.error?.message ||
error.response?.data?.error ||
error.message ||
"There was an error deleting your profile.";
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@ -2024,5 +1893,54 @@ export default class AccountViewView extends Vue {
this.savingProfile = false; this.savingProfile = false;
} }
} }
notifyDownloadStarted() {
this.$notify(
{
group: "alert",
type: "success",
title: "Download Started",
text: "See your downloads directory for the backup. It is in the Dexie format.",
},
-1,
);
}
showNameDialog() {
(this.$refs.userNameDialog as UserNameDialog).open((name?: string) => {
if (name) {
this.givenName = name;
}
});
}
computedStartDownloadLinkClassNames(): string {
return "block w-full text-center text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md";
}
computedDownloadLinkClassNames(): string {
return "block w-full text-center text-md bg-gradient-to-b from-green-500 to-green-800 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-6";
}
// Update error handling for type safety
private handleAxiosError(error: unknown): string {
if (error instanceof Error) {
return error.message;
}
if (typeof error === "object" && error !== null) {
const err = error as {
response?: { data?: { error?: { message?: string } } };
};
return err.response?.data?.error?.message || "An unknown error occurred";
}
return "An unknown error occurred";
}
handleProfileUpdate(updatedProfile: UserProfile) {
this.userProfileDesc = updatedProfile.description;
this.userProfileLatitude = updatedProfile.location?.lat || 0;
this.userProfileLongitude = updatedProfile.location?.lng || 0;
this.includeUserProfileLocation = !!updatedProfile.location;
}
} }
</script> </script>

6
src/views/ProjectsView.vue

@ -279,11 +279,7 @@ import ProjectIcon from "../components/ProjectIcon.vue";
import TopMessage from "../components/TopMessage.vue"; import TopMessage from "../components/TopMessage.vue";
import UserNameDialog from "../components/UserNameDialog.vue"; import UserNameDialog from "../components/UserNameDialog.vue";
import { Contact } from "../db/tables/contacts"; import { Contact } from "../db/tables/contacts";
import { import { didInfo, getHeaders, getPlanFromCache } from "../libs/endorserServer";
didInfo,
getHeaders,
getPlanFromCache,
} from "../libs/endorserServer";
import { OfferSummaryRecord, PlanData } from "../interfaces/records"; import { OfferSummaryRecord, PlanData } from "../interfaces/records";
import * as libsUtil from "../libs/util"; import * as libsUtil from "../libs/util";
import { OnboardPage } from "../libs/util"; import { OnboardPage } from "../libs/util";

3
tsconfig.json

@ -18,7 +18,8 @@
"@/db/*": ["db/*"], "@/db/*": ["db/*"],
"@/libs/*": ["libs/*"], "@/libs/*": ["libs/*"],
"@/constants/*": ["constants/*"], "@/constants/*": ["constants/*"],
"@/store/*": ["store/*"] "@/store/*": ["store/*"],
"@/types/*": ["types/*"]
}, },
"lib": ["ES2020", "dom", "dom.iterable"], // Include typings for ES2020 and DOM APIs "lib": ["ES2020", "dom", "dom.iterable"], // Include typings for ES2020 and DOM APIs
}, },

46
vite.config.base.ts

@ -0,0 +1,46 @@
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import path from "path";
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'nostr-tools': path.resolve(__dirname, 'node_modules/nostr-tools'),
'nostr-tools/nip06': path.resolve(__dirname, 'node_modules/nostr-tools/nip06'),
'nostr-tools/core': path.resolve(__dirname, 'node_modules/nostr-tools/core'),
stream: 'stream-browserify',
util: 'util',
crypto: 'crypto-browserify'
},
mainFields: ['module', 'jsnext:main', 'jsnext', 'main'],
},
optimizeDeps: {
include: ['nostr-tools', 'nostr-tools/nip06', 'nostr-tools/core'],
esbuildOptions: {
define: {
global: 'globalThis'
}
}
},
build: {
sourcemap: true,
target: 'esnext',
chunkSizeWarningLimit: 1000,
commonjsOptions: {
include: [/node_modules/],
transformMixedEsModules: true
},
rollupOptions: {
external: ['stream', 'util', 'crypto'],
output: {
globals: {
stream: 'stream',
util: 'util',
crypto: 'crypto'
}
}
}
}
});

28
vite.config.mobile.ts

@ -0,0 +1,28 @@
import { defineConfig, loadEnv } from "vite";
import baseConfig from "./vite.config.base";
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '');
return {
...baseConfig,
define: {
'import.meta.env.VITE_PLATFORM': JSON.stringify('mobile'),
},
build: {
...baseConfig.build,
outDir: 'dist/mobile',
rollupOptions: {
...baseConfig.build.rollupOptions,
output: {
...baseConfig.build.rollupOptions.output,
manualChunks: {
// Mobile-specific chunk splitting
vendor: ['vue', 'vue-router', 'pinia'],
capacitor: ['@capacitor/core', '@capacitor/filesystem', '@capacitor/share'],
}
}
}
}
};
});

60
vite.config.ts

@ -1,46 +1,18 @@
import { defineConfig } from "vite"; import { defineConfig, loadEnv } from "vite";
import vue from "@vitejs/plugin-vue"; import baseConfig from "./vite.config.base";
import path from "path";
export default defineConfig({ // https://vitejs.dev/config/
plugins: [vue()], export default defineConfig(({ mode }) => {
resolve: { // Load env file based on `mode` in the current working directory.
alias: { // Set the third parameter to '' to load all env regardless of the `VITE_` prefix.
'@': path.resolve(__dirname, './src'), const env = loadEnv(mode, process.cwd(), '');
'nostr-tools': path.resolve(__dirname, 'node_modules/nostr-tools'), const platform = env.PLATFORM || 'web';
'nostr-tools/nip06': path.resolve(__dirname, 'node_modules/nostr-tools/nip06'),
'nostr-tools/core': path.resolve(__dirname, 'node_modules/nostr-tools/core'), // Load platform-specific config
stream: 'stream-browserify', const platformConfig = require(`./vite.config.${platform}`).default;
util: 'util',
crypto: 'crypto-browserify' return {
}, ...baseConfig,
mainFields: ['module', 'jsnext:main', 'jsnext', 'main'], ...platformConfig,
}, };
optimizeDeps: {
include: ['nostr-tools', 'nostr-tools/nip06', 'nostr-tools/core'],
esbuildOptions: {
define: {
global: 'globalThis'
}
}
},
build: {
sourcemap: true,
target: 'esnext',
chunkSizeWarningLimit: 1000,
commonjsOptions: {
include: [/node_modules/],
transformMixedEsModules: true
},
rollupOptions: {
external: ['stream', 'util', 'crypto'],
output: {
globals: {
stream: 'stream',
util: 'util',
crypto: 'crypto'
}
}
}
}
}); });

27
vite.config.web.ts

@ -0,0 +1,27 @@
import { defineConfig, loadEnv } from "vite";
import baseConfig from "./vite.config.base";
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '');
return {
...baseConfig,
define: {
'import.meta.env.VITE_PLATFORM': JSON.stringify('web'),
},
build: {
...baseConfig.build,
outDir: 'dist/web',
rollupOptions: {
...baseConfig.build.rollupOptions,
output: {
...baseConfig.build.rollupOptions.output,
manualChunks: {
// Web-specific chunk splitting
vendor: ['vue', 'vue-router', 'pinia'],
}
}
}
}
};
});
Loading…
Cancel
Save