Browse Source

refactor: improve code quality and type safety

- Remove v-html usage in EntityIcon component
- Replace unsafe HTML injection with proper Vue component structure
- Add proper type definitions for claims and credentials
- Fix error handling with TimeSafariError type
- Clean up imports and remove unused variables
- Standardize string quotes and formatting
- Add proper type annotations for function parameters
- Improve error handling in feed processing
- Add proper null checks and type guards
- Fix linting warnings in logger utility
- Standardize code style across components
- Add proper JSDoc comments for type safety
- Improve error message handling and display

This commit focuses on improving code quality by removing unsafe practices,
adding proper type safety, and standardizing code style across the codebase.
db-backup-cross-platform
Matthew Raymer 2 months ago
parent
commit
6e2bdc69e9
  1. 427
      package-lock.json
  2. 71
      src/components/EntityIcon.vue
  3. 14
      src/db/index.ts
  4. 83
      src/platforms/capacitor/DatabaseBackupService.ts
  5. 16
      src/services/DatabaseBackupService.ts
  6. 12
      src/services/PlatformServiceFactory.ts
  7. 38
      src/services/RateLimitsService.ts
  8. 2
      src/services/platforms/empty.ts
  9. 8
      src/services/platforms/web/DatabaseBackupService.ts
  10. 100
      src/types/interfaces.ts
  11. 40
      src/utils/logger.ts
  12. 20
      src/views/AccountViewView.vue
  13. 10
      src/views/ClaimCertificateView.vue
  14. 34
      src/views/ClaimReportCertificateView.vue
  15. 110
      src/views/HomeView.vue

427
package-lock.json

@ -5217,9 +5217,9 @@
}
},
"node_modules/@expo/cli": {
"version": "0.22.23",
"resolved": "https://registry.npmjs.org/@expo/cli/-/cli-0.22.23.tgz",
"integrity": "sha512-LXFKu2jnk9ClVD+kw0sJCQ89zei01wz2t4EJwc9P7EwYb8gabC8FtPyM/X7NIE5jtrnTLTUtjW5ovxQSBL7pJQ==",
"version": "0.22.24",
"resolved": "https://registry.npmjs.org/@expo/cli/-/cli-0.22.24.tgz",
"integrity": "sha512-lhdenxBC8/x/vL39j79eXE09mOaqNNLmiSDdY/PblnI+UNzGgsQ48hBTYa/MQhd0ioXXVKurZL2941dLKwcxJw==",
"license": "MIT",
"optional": true,
"peer": true,
@ -5237,12 +5237,12 @@
"@expo/osascript": "^2.1.6",
"@expo/package-manager": "^1.7.2",
"@expo/plist": "^0.2.2",
"@expo/prebuild-config": "^8.0.30",
"@expo/prebuild-config": "^8.0.31",
"@expo/rudder-sdk-node": "^1.1.1",
"@expo/spawn-async": "^1.7.2",
"@expo/ws-tunnel": "^1.0.1",
"@expo/xcpretty": "^4.3.0",
"@react-native/dev-middleware": "0.76.8",
"@react-native/dev-middleware": "0.76.9",
"@urql/core": "^5.0.6",
"@urql/exchange-retry": "^1.3.0",
"accepts": "^1.3.8",
@ -6638,9 +6638,9 @@
}
},
"node_modules/@expo/prebuild-config": {
"version": "8.0.30",
"resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-8.0.30.tgz",
"integrity": "sha512-xNHWGh0xLZjxBXwVbDW+TPeexuQ95FZX2ZRrzJkALxhQiwYQswQSFE7CVUFMC2USIKVklCcgfEvtqnguTBQVxQ==",
"version": "8.0.31",
"resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-8.0.31.tgz",
"integrity": "sha512-YTuS5ic9KolD/WA3GqgLcZytHQU1dpitlZ/cbDq8ZqkY+1ae5YWX+GkYEZf2VyECPaWnHYuDGddaTQVw5miTRg==",
"license": "MIT",
"optional": true,
"peer": true,
@ -6650,7 +6650,7 @@
"@expo/config-types": "^52.0.5",
"@expo/image-utils": "^0.6.5",
"@expo/json-file": "^9.0.2",
"@react-native/normalize-colors": "0.76.8",
"@react-native/normalize-colors": "0.76.9",
"debug": "^4.3.1",
"fs-extra": "^9.0.0",
"resolve-from": "^5.0.0",
@ -8046,23 +8046,23 @@
}
},
"node_modules/@react-native/babel-plugin-codegen": {
"version": "0.76.8",
"resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.76.8.tgz",
"integrity": "sha512-84RUEhDZS+q7vPtxKi0iMZLd5/W0VN7NOyqX5f+burV3xMYpUhpF5TDJ2Ysol7dJrvEZHm6ISAriO85++V8YDw==",
"version": "0.76.9",
"resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.76.9.tgz",
"integrity": "sha512-vxL/vtDEIYHfWKm5oTaEmwcnNGsua/i9OjIxBDBFiJDu5i5RU3bpmDiXQm/bJxrJNPRp5lW0I0kpGihVhnMAIQ==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@react-native/codegen": "0.76.8"
"@react-native/codegen": "0.76.9"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@react-native/babel-preset": {
"version": "0.76.8",
"resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.76.8.tgz",
"integrity": "sha512-xrP+r3orRzzxtC2TrfGIP6IYi1f4AiWlnSiWf4zxEdMFzKrYdmxhD0FPtAZb77B0DqFIW5AcBFlm4grfL/VgfA==",
"version": "0.76.9",
"resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.76.9.tgz",
"integrity": "sha512-TbSeCplCM6WhL3hR2MjC/E1a9cRnMLz7i767T7mP90oWkklEjyPxWl+0GGoVGnJ8FC/jLUupg/HvREKjjif6lw==",
"license": "MIT",
"optional": true,
"peer": true,
@ -8108,7 +8108,7 @@
"@babel/plugin-transform-typescript": "^7.25.2",
"@babel/plugin-transform-unicode-regex": "^7.24.7",
"@babel/template": "^7.25.0",
"@react-native/babel-plugin-codegen": "0.76.8",
"@react-native/babel-plugin-codegen": "0.76.9",
"babel-plugin-syntax-hermes-parser": "^0.25.1",
"babel-plugin-transform-flow-enums": "^0.0.2",
"react-refresh": "^0.14.0"
@ -8121,9 +8121,9 @@
}
},
"node_modules/@react-native/codegen": {
"version": "0.76.8",
"resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.76.8.tgz",
"integrity": "sha512-qvKhcYBkRHJFkeWrYm66kEomQOTVXWiHBkZ8VF9oC/71OJkLszpTpVOuPIyyib6fqhjy9l7mHYGYenSpfYI5Ww==",
"version": "0.76.9",
"resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.76.9.tgz",
"integrity": "sha512-AzlCHMTKrAVC2709V4ZGtBXmGVtWTpWm3Ruv5vXcd3/anH4mGucfJ4rjbWKdaYQJMpXa3ytGomQrsIsT/s8kgA==",
"license": "MIT",
"optional": true,
"peer": true,
@ -8260,9 +8260,9 @@
}
},
"node_modules/@react-native/debugger-frontend": {
"version": "0.76.8",
"resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.76.8.tgz",
"integrity": "sha512-kSukBw2C++5ENLUCAp/1uEeiFgiHi/MBa71Wgym3UD5qwu2vOSPOTSKRX7q2Jb676MUzTcrIaJBZ/r2qk25u7Q==",
"version": "0.76.9",
"resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.76.9.tgz",
"integrity": "sha512-0Ru72Bm066xmxFuOXhhvrryxvb57uI79yDSFf+hxRpktkC98NMuRenlJhslMrbJ6WjCu1vOe/9UjWNYyxXTRTA==",
"license": "BSD-3-Clause",
"optional": true,
"peer": true,
@ -8271,15 +8271,15 @@
}
},
"node_modules/@react-native/dev-middleware": {
"version": "0.76.8",
"resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.76.8.tgz",
"integrity": "sha512-KYx7hFME2uYQRCDCqb19ghw51TAdh48PZ5EMpoU2kPA1SKKO9c1bUbpsKRhVZ0bv1QqEX6fjox3c4/WYRozHQA==",
"version": "0.76.9",
"resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.76.9.tgz",
"integrity": "sha512-xkd3C3dRcmZLjFTEAOvC14q3apMLouIvJViCZY/p1EfCMrNND31dgE1dYrLTiI045WAWMt5bD15i6f7dE2/QWA==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@isaacs/ttlcache": "^1.4.1",
"@react-native/debugger-frontend": "0.76.8",
"@react-native/debugger-frontend": "0.76.9",
"chrome-launcher": "^0.15.2",
"chromium-edge-launcher": "^0.2.0",
"connect": "^3.6.5",
@ -8607,9 +8607,9 @@
}
},
"node_modules/@react-native/normalize-colors": {
"version": "0.76.8",
"resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.76.8.tgz",
"integrity": "sha512-FRjRvs7RgsXjkbGSOjYSxhX5V70c0IzA/jy3HXeYpATMwD9fOR1DbveLW497QGsVdCa0vThbJUtR8rIzAfpHQA==",
"version": "0.76.9",
"resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.76.9.tgz",
"integrity": "sha512-TUdMG2JGk72M9d8DYbubdOlrzTYjw+YMe/xOnLU4viDgWRHsCbtRS9x0IAxRjs3amj/7zmK3Atm8jUPvdAc8qw==",
"license": "MIT",
"optional": true,
"peer": true
@ -9791,9 +9791,9 @@
}
},
"node_modules/@types/babel__generator": {
"version": "7.6.8",
"resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz",
"integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==",
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
"integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
"license": "MIT",
"optional": true,
"peer": true,
@ -9977,9 +9977,9 @@
}
},
"node_modules/@types/luxon": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.6.0.tgz",
"integrity": "sha512-RtEj20xRyG7cRp142MkQpV3GRF8Wo2MtDkKLz65MQs7rM1Lh8bz+HtfPXCCJEYpnDFu6VwAq/Iv2Ikyp9Jw/hw==",
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.6.2.tgz",
"integrity": "sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw==",
"dev": true,
"license": "MIT"
},
@ -12383,9 +12383,9 @@
}
},
"node_modules/babel-preset-expo": {
"version": "12.0.10",
"resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-12.0.10.tgz",
"integrity": "sha512-6QE52Bxsp5XRE8t0taKRFTFsmTG0ThQE+PTgCgLY9s8v2Aeh8R+E+riXhSHX6hP+diDmBFBdvLCUTq7kroJb1Q==",
"version": "12.0.11",
"resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-12.0.11.tgz",
"integrity": "sha512-4m6D92nKEieg+7DXa8uSvpr0GjfuRfM/G0t0I/Q5hF8HleEv5ms3z4dJ+p52qXSJsm760tMqLdO93Ywuoi7cCQ==",
"license": "MIT",
"optional": true,
"peer": true,
@ -12396,7 +12396,7 @@
"@babel/plugin-transform-parameters": "^7.22.15",
"@babel/preset-react": "^7.22.15",
"@babel/preset-typescript": "^7.23.0",
"@react-native/babel-preset": "0.76.8",
"@react-native/babel-preset": "0.76.9",
"babel-plugin-react-native-web": "~0.19.13",
"react-refresh": "^0.14.2"
},
@ -12610,9 +12610,9 @@
}
},
"node_modules/bignumber.js": {
"version": "9.1.2",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz",
"integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==",
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.2.0.tgz",
"integrity": "sha512-JocpCSOixzy5XFJi2ub6IMmV/G9i8Lrm2lZvwBv9xPdglmZM0ufDVBbjbrfU/zuLvBfD7Bv2eYxz9i+OHTgkew==",
"license": "MIT",
"engines": {
"node": "*"
@ -12641,54 +12641,42 @@
}
},
"node_modules/bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/bl/-/bl-0.8.2.tgz",
"integrity": "sha512-pfqikmByp+lifZCS0p6j6KreV6kNU6Apzpm2nKOk+94cZb/jvle55+JxWiByUQ0Wo/+XnDXEy5MxxKMb6r0VIw==",
"dev": true,
"license": "MIT",
"dependencies": {
"buffer": "^5.5.0",
"inherits": "^2.0.4",
"readable-stream": "^3.4.0"
"readable-stream": "~1.0.26"
}
},
"node_modules/bl/node_modules/buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"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/bl/node_modules/isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==",
"dev": true,
"license": "MIT"
},
"node_modules/bl/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==",
"version": "1.0.34",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz",
"integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==",
"dev": true,
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
"core-util-is": "~1.0.0",
"inherits": "~2.0.1",
"isarray": "0.0.1",
"string_decoder": "~0.10.x"
}
},
"node_modules/bl/node_modules/string_decoder": {
"version": "0.10.31",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
"integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==",
"dev": true,
"license": "MIT"
},
"node_modules/blakejs": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/blakejs/-/blakejs-1.2.1.tgz",
@ -12871,20 +12859,6 @@
"node": ">= 0.12"
}
},
"node_modules/browserify-sign/node_modules/hash-base": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.5.tgz",
"integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==",
"dev": true,
"license": "MIT",
"dependencies": {
"inherits": "^2.0.4",
"safe-buffer": "^5.2.1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/browserify-sign/node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
@ -14883,20 +14857,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/crypto-browserify/node_modules/hash-base": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.5.tgz",
"integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==",
"dev": true,
"license": "MIT",
"dependencies": {
"inherits": "^2.0.4",
"safe-buffer": "^5.2.1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/crypto-js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
@ -15971,9 +15931,9 @@
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.130",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.130.tgz",
"integrity": "sha512-Ou2u7L9j2XLZbhqzyX0jWDj6gA8D3jIfVzt4rikLf3cGBa0VdReuFimBKS9tQJA4+XpeCxj1NoWlfBXzbMa9IA==",
"version": "1.5.131",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.131.tgz",
"integrity": "sha512-fJFRYXVEJgDCiqFOgRGJm8XR97hZ13tw7FXI9k2yC5hgY+nyzC2tMO8baq1cQR7Ur58iCkASx2zrkZPZUnfzPg==",
"devOptional": true,
"license": "ISC"
},
@ -17024,21 +16984,21 @@
}
},
"node_modules/expo": {
"version": "52.0.42",
"resolved": "https://registry.npmjs.org/expo/-/expo-52.0.42.tgz",
"integrity": "sha512-t+PRYIzzPFAlF99OVJOjZwM1glLhN85XGD6vmeg6uwpADDILl9yw4dfy0DXL4hot5GJkAGaZ+uOHUljV4kC2Bg==",
"version": "52.0.44",
"resolved": "https://registry.npmjs.org/expo/-/expo-52.0.44.tgz",
"integrity": "sha512-qj3+MWxmqLyBaYQ8jDOvVLEgSqNplH3cf+nDhxCo4C1cpTPD1u/HGh1foibtaeuCYLHsE5km1lrcOpRbFJ4luQ==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@babel/runtime": "^7.20.0",
"@expo/cli": "0.22.23",
"@expo/cli": "0.22.24",
"@expo/config": "~10.0.11",
"@expo/config-plugins": "~9.0.17",
"@expo/fingerprint": "0.11.11",
"@expo/metro-config": "0.19.12",
"@expo/vector-icons": "^14.0.0",
"babel-preset-expo": "~12.0.10",
"babel-preset-expo": "~12.0.11",
"expo-asset": "~11.0.5",
"expo-constants": "~17.0.8",
"expo-file-system": "~18.0.12",
@ -18183,6 +18143,16 @@
"xtend": "~4.0.1"
}
},
"node_modules/get-pkg-repo/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/get-pkg-repo/node_modules/yargs": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
@ -18696,31 +18666,16 @@
"license": "ISC"
},
"node_modules/hash-base": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz",
"integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==",
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.5.tgz",
"integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.4",
"readable-stream": "^3.6.0",
"safe-buffer": "^5.2.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/hash-base/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==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
"safe-buffer": "^5.2.1"
},
"engines": {
"node": ">= 6"
"node": ">= 0.10"
}
},
"node_modules/hash.js": {
@ -20859,15 +20814,6 @@
"xtend": "^2.2.0"
}
},
"node_modules/level-filesystem/node_modules/xtend": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-2.2.0.tgz",
"integrity": "sha512-SLt5uylT+4aoXxXuwtQp5ZnMMzhDb1Xkg4pEqc00WUJCQifPfV9Ub1VrNhp9kXkrjZD2I2Hl8WnjP37jzZLPZw==",
"dev": true,
"engines": {
"node": ">=0.4"
}
},
"node_modules/level-fix-range": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/level-fix-range/-/level-fix-range-1.0.2.tgz",
@ -21005,16 +20951,6 @@
"xtend": "~3.0.0"
}
},
"node_modules/levelup/node_modules/bl": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/bl/-/bl-0.8.2.tgz",
"integrity": "sha512-pfqikmByp+lifZCS0p6j6KreV6kNU6Apzpm2nKOk+94cZb/jvle55+JxWiByUQ0Wo/+XnDXEy5MxxKMb6r0VIw==",
"dev": true,
"license": "MIT",
"dependencies": {
"readable-stream": "~1.0.26"
}
},
"node_modules/levelup/node_modules/isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
@ -24775,6 +24711,58 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ora/node_modules/bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"buffer": "^5.5.0",
"inherits": "^2.0.4",
"readable-stream": "^3.4.0"
}
},
"node_modules/ora/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/ora/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/os-tmpdir": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
@ -24950,20 +24938,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/parse-asn1/node_modules/hash-base": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.5.tgz",
"integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==",
"dev": true,
"license": "MIT",
"dependencies": {
"inherits": "^2.0.4",
"safe-buffer": "^5.2.1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/parse-entities": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz",
@ -29314,6 +29288,16 @@
"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",
@ -29859,6 +29843,41 @@
"node": ">=6"
}
},
"node_modules/tar-stream/node_modules/bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
"license": "MIT",
"dependencies": {
"buffer": "^5.5.0",
"inherits": "^2.0.4",
"readable-stream": "^3.4.0"
}
},
"node_modules/tar-stream/node_modules/buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"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/tar-stream/node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
@ -30583,24 +30602,24 @@
"license": "MIT"
},
"node_modules/typeorm": {
"version": "0.3.21",
"resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.21.tgz",
"integrity": "sha512-lh4rUWl1liZGjyPTWpwcK8RNI5x4ekln+/JJOox1wCd7xbucYDOXWD+1cSzTN3L0wbTGxxOtloM5JlxbOxEufA==",
"version": "0.3.22",
"resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.22.tgz",
"integrity": "sha512-P/Tsz3UpJ9+K0oryC0twK5PO27zejLYYwMsE8SISfZc1lVHX+ajigiOyWsKbuXpEFMjD9z7UjLzY3+ElVOMMDA==",
"license": "MIT",
"dependencies": {
"@sqltools/formatter": "^1.2.5",
"ansis": "^3.9.0",
"ansis": "^3.17.0",
"app-root-path": "^3.1.0",
"buffer": "^6.0.3",
"dayjs": "^1.11.9",
"debug": "^4.3.4",
"dotenv": "^16.0.3",
"dayjs": "^1.11.13",
"debug": "^4.4.0",
"dotenv": "^16.4.7",
"glob": "^10.4.5",
"sha.js": "^2.4.11",
"sql-highlight": "^6.0.0",
"tslib": "^2.5.0",
"uuid": "^11.0.5",
"yargs": "^17.6.2"
"tslib": "^2.8.1",
"uuid": "^11.1.0",
"yargs": "^17.7.2"
},
"bin": {
"typeorm": "cli.js",
@ -30614,12 +30633,12 @@
"url": "https://opencollective.com/typeorm"
},
"peerDependencies": {
"@google-cloud/spanner": "^5.18.0",
"@google-cloud/spanner": "^5.18.0 || ^6.0.0 || ^7.0.0",
"@sap/hana-client": "^2.12.25",
"better-sqlite3": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0",
"hdb-pool": "^0.1.6",
"ioredis": "^5.0.4",
"mongodb": "^5.8.0",
"mongodb": "^5.8.0 || ^6.0.0",
"mssql": "^9.1.1 || ^10.0.1 || ^11.0.1",
"mysql2": "^2.2.5 || ^3.0.1",
"oracledb": "^6.3.0",
@ -30631,7 +30650,7 @@
"sql.js": "^1.4.0",
"sqlite3": "^5.0.3",
"ts-node": "^10.7.0",
"typeorm-aurora-data-api-driver": "^2.0.0"
"typeorm-aurora-data-api-driver": "^2.0.0 || ^3.0.0"
},
"peerDependenciesMeta": {
"@google-cloud/spanner": {
@ -30687,6 +30706,23 @@
}
}
},
"node_modules/typeorm/node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/typeorm/node_modules/glob": {
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
@ -30722,6 +30758,18 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/typeorm/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/typeorm/node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/typeorm/node_modules/uuid": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
@ -31210,9 +31258,9 @@
}
},
"node_modules/vite": {
"version": "5.4.16",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.16.tgz",
"integrity": "sha512-Y5gnfp4NemVfgOTDQAunSD4346fal44L9mszGGY/e+qxsRT5y1sMlS/8tiQ8AFAp+MFgYNSINdfEchJiPm41vQ==",
"version": "5.4.17",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.17.tgz",
"integrity": "sha512-5+VqZryDj4wgCs55o9Lp+p8GE78TLVg0lasCH5xFZ4jacZjtqZa6JUw9/p0WeAojaOfncSM6v77InkFPGnvPvg==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -32417,11 +32465,10 @@
}
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-2.2.0.tgz",
"integrity": "sha512-SLt5uylT+4aoXxXuwtQp5ZnMMzhDb1Xkg4pEqc00WUJCQifPfV9Ub1VrNhp9kXkrjZD2I2Hl8WnjP37jzZLPZw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.4"
}

71
src/components/EntityIcon.vue

@ -9,12 +9,26 @@
:height="iconSize"
@click="handleClick"
/>
<div
v-else
v-html="identiconSvg"
class="cursor-pointer"
@click="handleClick"
/>
<div v-else class="cursor-pointer" @click="handleClick">
<img
v-if="!identifier"
:src="blankSquareUrl"
class="rounded"
:width="iconSize"
:height="iconSize"
/>
<svg
v-else
:width="iconSize"
:height="iconSize"
viewBox="0 0 100 100"
xmlns="http://www.w3.org/2000/svg"
>
<g v-for="(path, index) in avatarPaths" :key="index">
<path :d="path" />
</g>
</svg>
</div>
</div>
</template>
@ -32,7 +46,9 @@ export default class EntityIcon extends Vue {
@Prop iconSize = 0;
@Prop profileImageUrl = ""; // overridden by contact.profileImageUrl
private identiconSvg = "";
private avatarPaths: string[] = [];
private blankSquareUrl =
import.meta.env.VITE_BASE_URL + "assets/blank-square.svg";
get imageUrl(): string {
return this.contact?.profileImageUrl || this.profileImageUrl;
@ -42,38 +58,43 @@ export default class EntityIcon extends Vue {
return !!this.imageUrl;
}
get identifier(): string | undefined {
return this.contact?.did || this.entityId;
}
handleClick() {
try {
// Emit a simple event without passing the event object
this.$emit('click');
this.$emit("click");
} catch (error) {
logger.error('Error handling click event:', error);
logger.error("Error handling click event:", error);
}
}
generateIdenticon(): string {
const identifier = this.contact?.did || this.entityId;
if (!identifier) {
const baseUrl = import.meta.env.VITE_BASE_URL || '/';
return `<img src="${baseUrl}assets/blank-square.svg" class="rounded" width="${this.iconSize}" height="${this.iconSize}" />`;
}
// https://api.dicebear.com/8.x/avataaars/svg?seed=
// ... does not render things with the same seed as this library.
// "did:ethr:0x222BB77E6Ff3774d34c751f3c1260866357B677b" yields a girl with flowers in her hair and a lightning earring
// ... which looks similar to '' at the dicebear site but which is different.
generateAvatarPaths(): string[] {
if (!this.identifier) return [];
const options: StyleOptions<object> = {
seed: identifier || "",
seed: this.identifier,
size: this.iconSize,
};
const avatar = createAvatar(avataaars, options);
return avatar.toString();
const svgString = avatar.toString();
// Extract paths from SVG string
const parser = new DOMParser();
const doc = parser.parseFromString(svgString, "image/svg+xml");
const paths = Array.from(doc.querySelectorAll("path")).map(
(path) => path.getAttribute("d") || "",
);
return paths;
}
mounted() {
this.identiconSvg = this.generateIdenticon();
logger.log('EntityIcon mounted, profileImageUrl:', this.profileImageUrl);
logger.log('EntityIcon mounted, entityId:', this.entityId);
logger.log('EntityIcon mounted, iconSize:', this.iconSize);
this.avatarPaths = this.generateAvatarPaths();
logger.log("EntityIcon mounted, profileImageUrl:", this.profileImageUrl);
logger.log("EntityIcon mounted, entityId:", this.entityId);
logger.log("EntityIcon mounted, iconSize:", this.iconSize);
}
}
</script>

14
src/db/index.ts

@ -1,7 +1,8 @@
import BaseDexie, { Table } from "dexie";
import { encrypted, Encryption } from "@pvermeer/dexie-encrypted-addon";
import { exportDB } from "dexie-export-import";
import { exportDB, ExportOptions } from "dexie-export-import";
import * as R from "ramda";
import Dexie from "dexie";
import { Account, AccountsSchema } from "./tables/accounts";
import { Contact, ContactSchema } from "./tables/contacts";
@ -27,9 +28,14 @@ type NonsensitiveTables = {
};
// Using 'unknown' instead of 'any' for stricter typing and to avoid TypeScript warnings
export type SecretDexie<T extends unknown = SecretTable> = BaseDexie & T & { export: (options?: any) => Promise<Blob> };
export type SensitiveDexie<T extends unknown = SensitiveTables> = BaseDexie & T & { export: (options?: any) => Promise<Blob> };
export type NonsensitiveDexie<T extends unknown = NonsensitiveTables> = BaseDexie & T & { export: (options?: any) => Promise<Blob> };
export type SecretDexie<T extends Record<string, Dexie.Table> = SecretTable> =
BaseDexie & T & { export: (options?: ExportOptions) => Promise<Blob> };
export type SensitiveDexie<
T extends Record<string, Dexie.Table> = SensitiveTables,
> = BaseDexie & T & { export: (options?: ExportOptions) => Promise<Blob> };
export type NonsensitiveDexie<
T extends Record<string, Dexie.Table> = NonsensitiveTables,
> = BaseDexie & T & { export: (options?: ExportOptions) => Promise<Blob> };
//// Initialize the DBs, starting with the sensitive ones.

83
src/platforms/capacitor/DatabaseBackupService.ts

@ -1,69 +1,82 @@
/**
* @file DatabaseBackupService.ts
* @description Capacitor-specific implementation of DatabaseBackupService
* @description Capacitor-specific implementation of database backup service
*
* This implementation handles database backup operations specifically for Capacitor
* platforms (Android/iOS). It uses the Filesystem and Share plugins to save and
* share the backup file.
* This service handles database backup operations on Capacitor platforms (Android/iOS)
* using the Filesystem and Share plugins. It creates a temporary backup file,
* writes the backup data to it, and shares the file using the platform's share sheet.
*/
import { DatabaseBackupService as BaseDatabaseBackupService } from "../../services/DatabaseBackupService";
import { Filesystem, Directory } from "@capacitor/filesystem";
import { Share } from "@capacitor/share";
import { DatabaseBackupService as BaseDatabaseBackupService } from "../../services/DatabaseBackupService";
import { log, error } from "../../utils/logger";
export class DatabaseBackupService extends BaseDatabaseBackupService {
/**
* Handles the backup process for Capacitor platforms
*
*
* @param base64Data - Backup data in base64 format
* @param arrayBuffer - Backup data as ArrayBuffer
* @param blob - Backup data as Blob
*/
protected async handleBackup(
base64Data: string,
arrayBuffer: ArrayBuffer,
blob: Blob
_arrayBuffer: ArrayBuffer,
_blob: Blob,
): Promise<void> {
try {
log("Starting Capacitor backup process");
// Create a temporary file
const fileName = `timesafari-backup-${new Date().toISOString()}.json`;
const filePath = `backups/${fileName}`;
log("Writing backup file");
const result = await Filesystem.writeFile({
path: filePath,
log("Starting backup process for Capacitor platform");
// Create a timestamped backup file name
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
const backupFileName = `timesafari-backup-${timestamp}.json`;
const backupFilePath = `backups/${backupFileName}`;
log("Creating backup file:", {
fileName: backupFileName,
path: backupFilePath,
});
// Write the backup file
const writeResult = (await Filesystem.writeFile({
path: backupFilePath,
data: base64Data,
directory: Directory.Cache,
recursive: true
});
log("Getting file path");
const fileInfo = await Filesystem.stat({
path: filePath,
directory: Directory.Cache
});
recursive: true,
})) as unknown as { uri: string };
if (!writeResult.uri) {
throw new Error("Failed to write backup file: No URI returned");
}
log("Backup file written successfully:", { uri: writeResult.uri });
// Share the backup file
log("Sharing backup file");
await Share.share({
title: "TimeSafari Backup",
text: "Your TimeSafari backup file",
url: fileInfo.uri,
dialogTitle: "Share TimeSafari Backup"
url: writeResult.uri,
dialogTitle: "Share TimeSafari Backup",
});
log("Backup shared successfully");
// Clean up the temporary file
await Filesystem.deleteFile({
path: filePath,
directory: Directory.Cache
});
try {
await Filesystem.deleteFile({
path: backupFilePath,
directory: Directory.Cache,
});
log("Temporary backup file cleaned up");
} catch (cleanupError) {
error("Failed to clean up temporary backup file:", cleanupError);
// Don't throw here as the backup was successful
}
} catch (err) {
error("Error during Capacitor backup:", err);
error("Error during backup process:", err);
throw err;
}
}
}
}

16
src/services/DatabaseBackupService.ts

@ -65,23 +65,23 @@ export class DatabaseBackupService {
public static async createAndShareBackup(
base64Data: string,
arrayBuffer: ArrayBuffer,
blob: Blob
blob: Blob,
): Promise<void> {
try {
log('Creating platform-specific backup service');
log("Creating platform-specific backup service");
const backupService = await this.getPlatformSpecificBackupService();
log('Backup service created successfully');
log("Backup service created successfully");
log('Executing platform-specific backup');
log("Executing platform-specific backup");
await backupService.handleBackup(base64Data, arrayBuffer, blob);
log('Backup completed successfully');
log("Backup completed successfully");
} catch (err) {
error('Error during backup creation:', err);
error("Error during backup creation:", err);
if (err instanceof Error) {
error('Error details:', {
error("Error details:", {
name: err.name,
message: err.message,
stack: err.stack
stack: err.stack,
});
}
throw err;

12
src/services/PlatformServiceFactory.ts

@ -131,10 +131,12 @@ export class PlatformServiceFactory {
public async createDatabaseBackupService(): Promise<DatabaseBackupService> {
// List of supported platforms for web builds
const webSupportedPlatforms = ["web", "mobile"];
// Return stub implementation for unsupported platforms
if (!webSupportedPlatforms.includes(this.platform)) {
logger.log(`Using stub implementation for unsupported platform: ${this.platform}`);
logger.log(
`Using stub implementation for unsupported platform: ${this.platform}`,
);
return new StubDatabaseBackupService();
}
@ -145,15 +147,15 @@ export class PlatformServiceFactory {
/* @vite-ignore */
`./platforms/${this.platform}/DatabaseBackupService.ts`
);
logger.log('Platform service loaded successfully');
logger.log("Platform service loaded successfully");
return new module.DatabaseBackupService();
} catch (error) {
logger.error(
`[TimeSafari] Failed to load platform-specific service for ${this.platform}:`,
error
error,
);
// Fallback to stub implementation on error
logger.log('Falling back to stub implementation');
logger.log("Falling back to stub implementation");
return new StubDatabaseBackupService();
}
}

38
src/services/RateLimitsService.ts

@ -17,31 +17,45 @@ export class RateLimitsService {
* @param did - The user's DID
* @returns Promise<EndorserRateLimits>
*/
static async fetchRateLimits(apiServer: string, did: string): Promise<EndorserRateLimits> {
logger.log('Fetching rate limits for DID:', did);
logger.log('Using API server:', apiServer);
static async fetchRateLimits(
apiServer: string,
did: string,
): Promise<EndorserRateLimits> {
logger.log("Fetching rate limits for DID:", did);
logger.log("Using API server:", apiServer);
try {
const headers = await getHeaders(did);
const response = await axios.get(`${apiServer}/api/v2/rate-limits/${did}`, { headers });
logger.log('Rate limits response:', response.data);
const response = await axios.get(
`${apiServer}/api/v2/rate-limits/${did}`,
{ headers },
);
logger.log("Rate limits response:", response.data);
return response.data;
} catch (error) {
if (axios.isAxiosError(error) && (error.response?.status === 400 || error.response?.status === 404)) {
const errorData = error.response.data as { error?: { message?: string, code?: string } };
if (errorData.error?.code === 'UNREGISTERED_USER' || error.response?.status === 404) {
logger.log('User is not registered, returning default limits');
if (
axios.isAxiosError(error) &&
(error.response?.status === 400 || error.response?.status === 404)
) {
const errorData = error.response.data as {
error?: { message?: string; code?: string };
};
if (
errorData.error?.code === "UNREGISTERED_USER" ||
error.response?.status === 404
) {
logger.log("User is not registered, returning default limits");
return {
doneClaimsThisWeek: "0",
maxClaimsPerWeek: "0",
nextWeekBeginDateTime: new Date().toISOString(),
doneRegistrationsThisMonth: "0",
maxRegistrationsPerMonth: "0",
nextMonthBeginDateTime: new Date().toISOString()
nextMonthBeginDateTime: new Date().toISOString(),
};
}
}
logger.error('Error fetching rate limits:', error);
logger.error("Error fetching rate limits:", error);
throw error;
}
}

2
src/services/platforms/empty.ts

@ -17,4 +17,4 @@ export default class StubDatabaseBackupService extends DatabaseBackupService {
}
}
export { StubDatabaseBackupService as DatabaseBackupService };
export { StubDatabaseBackupService as DatabaseBackupService };

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

@ -12,10 +12,10 @@ export default class WebDatabaseBackupService extends DatabaseBackupService {
protected async handleBackup(
_base64Data: string,
_arrayBuffer: ArrayBuffer,
blob: Blob
blob: Blob,
): Promise<void> {
try {
log('Starting web platform backup');
log("Starting web platform backup");
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
@ -24,9 +24,9 @@ export default class WebDatabaseBackupService extends DatabaseBackupService {
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
log('Web platform backup completed');
log("Web platform backup completed");
} catch (err) {
error('Error during web platform backup:', err);
error("Error during web platform backup:", err);
throw err;
}
}

100
src/types/interfaces.ts

@ -1,29 +1,29 @@
/**
* @file interfaces.ts
* @description Core type declarations for the TimeSafari application
*
*
* This module defines the core interfaces and types used throughout the application.
* It serves as the central location for type definitions that are shared across
* multiple components and services.
*
*
* Architecture:
* 1. DID (Decentralized Identifier) Types:
* - IIdentifier: Core DID structure
* - IKey: Cryptographic key information
* - IService: Service endpoints and capabilities
*
*
* 2. Verifiable Credential Types:
* - GenericCredWrapper: Base wrapper for all credentials
* - GiveVerifiableCredential: Gift-related credentials
* - OfferVerifiableCredential: Offer-related credentials
* - RegisterVerifiableCredential: Registration credentials
*
*
* 3. Service Types:
* - EndorserService: Claims and endorsements
* - PushNotificationService: Web push notifications
* - ProfileService: User profiles
* - BackupService: Data backup
*
*
* @see src/interfaces/identifier.ts
* @see src/interfaces/claims.ts
* @see src/interfaces/limits.ts
@ -33,11 +33,11 @@ import { GiveVerifiableCredential } from "../interfaces";
/**
* Interface for a Decentralized Identifier (DID)
*
*
* This interface defines the structure of a DID, which is a unique identifier
* that can be used to look up a DID document containing information associated
* with the DID, such as public keys and service endpoints.
*
*
* @example
* ```typescript
* const identifier: IIdentifier = {
@ -102,7 +102,29 @@ export interface IIdentifier {
/**
* Optional metadata about the key
*/
meta?: any;
meta?: {
/**
* HD wallet derivation path
* @example "m/44'/60'/0'/0/0"
*/
derivationPath?: string;
/**
* Key usage or purpose
* @example "signing", "encryption"
*/
usage?: string;
/**
* Key creation timestamp
*/
createdAt?: number;
/**
* Additional key metadata
*/
[key: string]: unknown;
};
}>;
/**
@ -132,15 +154,41 @@ export interface IIdentifier {
*/
description?: string;
}>;
/**
* Optional metadata about the identifier
*/
meta?: {
/**
* DID method-specific metadata
* @example { network: "mainnet", chainId: 1 } for ethr
*/
method?: Record<string, unknown>;
/**
* Identifier creation timestamp
*/
createdAt?: number;
/**
* Last update timestamp
*/
updatedAt?: number;
/**
* Additional identifier metadata
*/
[key: string]: unknown;
};
}
/**
* Interface for a cryptographic key
*
*
* This interface defines the structure of a cryptographic key used in the
* DID system. It includes both public and private key information, along
* with metadata about the key's purpose and derivation.
*
*
* @example
* ```typescript
* const key: IKey = {
@ -203,6 +251,17 @@ export interface IKey {
*/
derivationPath?: string;
/**
* Key usage or purpose
* @example "signing", "encryption"
*/
usage?: string;
/**
* Key creation timestamp
*/
createdAt?: number;
/**
* Additional key metadata
*/
@ -212,11 +271,11 @@ export interface IKey {
/**
* Interface for a service endpoint
*
*
* This interface defines the structure of a service endpoint that can be
* associated with a DID. Services provide additional functionality and
* endpoints for DID operations.
*
*
* @example
* ```typescript
* const service: IService = {
@ -352,3 +411,20 @@ export interface GiveRecordWithContactInfo {
recipientProjectName?: string;
image?: string;
}
export interface TimeSafariError extends Error {
/**
* User-friendly error message
*/
userMessage?: string;
/**
* Error code for programmatic handling
*/
code?: string;
/**
* Additional error context
*/
context?: Record<string, unknown>;
}

40
src/utils/logger.ts

@ -20,7 +20,7 @@ function safeStringify(obj: unknown) {
}
function formatMessage(message: string, ...args: unknown[]): string {
const prefix = '[TimeSafari]';
const prefix = "[TimeSafari]";
const argsString = args.length > 0 ? " - " + safeStringify(args) : "";
return `${prefix} ${message}${argsString}`;
}
@ -29,6 +29,7 @@ export const logger = {
log: (message: string, ...args: unknown[]) => {
if (process.env.NODE_ENV !== "production") {
const formattedMessage = formatMessage(message, ...args);
// eslint-disable-next-line no-console
console.log(formattedMessage);
logToDb(message + (args.length > 0 ? " - " + safeStringify(args) : ""));
}
@ -36,38 +37,45 @@ export const logger = {
warn: (message: string, ...args: unknown[]) => {
if (process.env.NODE_ENV !== "production") {
const formattedMessage = formatMessage(message, ...args);
// eslint-disable-next-line no-console
console.warn(formattedMessage);
logToDb(message + (args.length > 0 ? " - " + safeStringify(args) : ""));
}
},
error: (message: string, ...args: unknown[]) => {
const formattedMessage = formatMessage(message, ...args);
// eslint-disable-next-line no-console
console.error(formattedMessage);
logToDb(message + (args.length > 0 ? " - " + safeStringify(args) : ""));
},
};
export function log(...args: any[]) {
const message = formatMessage(args[0], ...args.slice(1));
console.log(message);
export function log(message: string, ...args: unknown[]): void {
const formattedMessage = formatMessage(message, ...args);
// eslint-disable-next-line no-console
console.log(formattedMessage);
}
export function error(...args: any[]) {
const message = formatMessage(args[0], ...args.slice(1));
console.error(message);
export function error(message: string, ...args: unknown[]): void {
const formattedMessage = formatMessage(message, ...args);
// eslint-disable-next-line no-console
console.error(formattedMessage);
}
export function warn(...args: any[]) {
const message = formatMessage(args[0], ...args.slice(1));
console.warn(message);
export function warn(message: string, ...args: unknown[]): void {
const formattedMessage = formatMessage(message, ...args);
// eslint-disable-next-line no-console
console.warn(formattedMessage);
}
export function info(...args: any[]) {
const message = formatMessage(args[0], ...args.slice(1));
console.info(message);
export function info(message: string, ...args: unknown[]): void {
const formattedMessage = formatMessage(message, ...args);
// eslint-disable-next-line no-console
console.info(formattedMessage);
}
export function debug(...args: any[]) {
const message = formatMessage(args[0], ...args.slice(1));
console.debug(message);
export function debug(message: string, ...args: unknown[]): void {
const formattedMessage = formatMessage(message, ...args);
// eslint-disable-next-line no-console
console.debug(formattedMessage);
}

20
src/views/AccountViewView.vue

@ -836,7 +836,6 @@ import "leaflet/dist/leaflet.css";
import { AxiosError } from "axios";
import { Buffer } from "buffer/";
import Dexie from "dexie";
import { exportDB } from "dexie-export-import";
import * as R from "ramda";
import type { IIdentifier, UserProfile } from "@/types/interfaces";
import { ref } from "vue";
@ -889,7 +888,6 @@ import { DatabaseBackupService } from "../services/DatabaseBackupService";
import { ProfileService } from "../services/ProfileService";
import { RateLimitsService } from "../services/RateLimitsService";
import ProfileSection from "../components/ProfileSection.vue";
import { log, error } from '../utils/logger';
const inputImportFileNameRef = ref<Blob>();
@ -1404,12 +1402,12 @@ export default class AccountViewView extends Vue {
async exportDatabase() {
try {
logger.log("Starting database export process");
const db = await this.getDatabase();
logger.log("Database instance:", {
name: db.name,
version: db.verno,
tables: db.tables.map((t: { name: string }) => t.name)
tables: db.tables.map((t: { name: string }) => t.name),
});
if (!db.export) {
@ -1421,7 +1419,7 @@ export default class AccountViewView extends Vue {
const blob = await db.export();
logger.log("Blob created:", {
type: blob.type,
size: blob.size
size: blob.size,
});
logger.log("Converting blob to base64");
@ -1445,8 +1443,16 @@ export default class AccountViewView extends Vue {
}
}
async createAndShareBackup(base64Data: string, arrayBuffer: ArrayBuffer, blob: Blob) {
await DatabaseBackupService.createAndShareBackup(base64Data, arrayBuffer, blob);
async createAndShareBackup(
base64Data: string,
arrayBuffer: ArrayBuffer,
blob: Blob,
) {
await DatabaseBackupService.createAndShareBackup(
base64Data,
arrayBuffer,
blob,
);
}
async uploadImportFile(event: Event) {

10
src/views/ClaimCertificateView.vue

@ -24,8 +24,8 @@
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { nextTick } from "vue";
import QRCodeVue from 'qrcode.vue';
import { APP_SERVER, NotificationIface } from "../constants/app";
import QRCodeVue from "qrcode.vue";
import { NotificationIface } from "../constants/app";
import { db, retrieveSettingsForActiveAccount } from "../db/index";
import * as serverUtil from "../libs/endorserServer";
import { GenericCredWrapper, GenericVerifiableCredential } from "../interfaces";
@ -277,11 +277,11 @@ export default class ClaimCertificateView extends Vue {
private async generateQRCode() {
if (!this.qrCodeRef) return;
const canvas = await this.qrCodeRef.toCanvas();
const ctx = canvas.getContext('2d');
const ctx = canvas.getContext("2d");
if (!ctx) return;
// Draw the QR code on the claim canvas
const CANVAS_WIDTH = 1100;
const CANVAS_HEIGHT = 850;

34
src/views/ClaimReportCertificateView.vue

@ -20,9 +20,9 @@
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { nextTick } from "vue";
import QRCodeVue from 'qrcode.vue';
import QRCodeVue from "qrcode.vue";
import { APP_SERVER, NotificationIface } from "../constants/app";
import { NotificationIface } from "../constants/app";
import { db, retrieveSettingsForActiveAccount } from "../db/index";
import * as endorserServer from "../libs/endorserServer";
import { GenericCredWrapper, GenericVerifiableCredential } from "../interfaces";
@ -84,9 +84,7 @@ export default class ClaimReportCertificateView extends Vue {
}
}
async drawCanvas(
claimData: GenericCredWrapper<GenericVerifiableCredential>,
) {
async drawCanvas(claimData: GenericCredWrapper<GenericVerifiableCredential>) {
await db.open();
const allContacts = await db.contacts.toArray();
@ -99,7 +97,13 @@ export default class ClaimReportCertificateView extends Vue {
backgroundImage.src = "/img/background/cert-frame-2.jpg";
backgroundImage.onload = async () => {
// Draw the background image
ctx.drawImage(backgroundImage, 0, 0, this.CANVAS_WIDTH, this.CANVAS_HEIGHT);
ctx.drawImage(
backgroundImage,
0,
0,
this.CANVAS_WIDTH,
this.CANVAS_HEIGHT,
);
// Set font and styles
ctx.fillStyle = "black";
@ -164,12 +168,20 @@ export default class ClaimReportCertificateView extends Vue {
claimData.issuer,
allContacts,
);
ctx.fillText(issuerText, this.CANVAS_WIDTH * 0.3, this.CANVAS_HEIGHT * 0.6);
ctx.fillText(
issuerText,
this.CANVAS_WIDTH * 0.3,
this.CANVAS_HEIGHT * 0.6,
);
}
// Draw claim ID
ctx.font = "14px Arial";
ctx.fillText(this.claimId, this.CANVAS_WIDTH * 0.3, this.CANVAS_HEIGHT * 0.7);
ctx.fillText(
this.claimId,
this.CANVAS_WIDTH * 0.3,
this.CANVAS_HEIGHT * 0.7,
);
ctx.fillText(
"via EndorserSearch.com",
this.CANVAS_WIDTH * 0.3,
@ -185,11 +197,11 @@ export default class ClaimReportCertificateView extends Vue {
private async generateQRCode() {
if (!this.qrCodeRef) return;
const canvas = await this.qrCodeRef.toCanvas();
const ctx = canvas.getContext('2d');
const ctx = canvas.getContext("2d");
if (!ctx) return;
// Draw the QR code on the report canvas
ctx.drawImage(canvas, this.CANVAS_WIDTH * 0.6, this.CANVAS_HEIGHT * 0.55);
}

110
src/views/HomeView.vue

@ -345,10 +345,13 @@ import {
GiverReceiverInputInfo,
OnboardPage,
} from "../libs/util";
import { GiveSummaryRecord } from "../interfaces";
import { GiveSummaryRecord, PlanSummaryRecord } from "../interfaces/records";
import * as serverUtil from "../libs/endorserServer";
import { logger } from "../utils/logger";
import { GiveRecordWithContactInfo } from "types";
import { TimeSafariError } from "../types/interfaces";
import { GiveVerifiableCredential } from "../interfaces/claims";
import { GenericVerifiableCredential } from "../interfaces/common";
/**
* HomeView Component
@ -451,20 +454,20 @@ export default class HomeView extends Vue {
const initPromises = [
this.initializeIdentity(),
this.loadSettings(),
this.loadContacts()
this.loadContacts(),
];
await Promise.all(initPromises);
// Sequential operations that depend on the above
await this.checkRegistrationStatus();
await this.loadFeedData();
// Non-critical operations that can run after UI is ready
this.loadNewOffers().catch(err => {
this.loadNewOffers().catch((err) => {
logger.error("Error loading new offers:", err);
});
this.checkOnboarding().catch(err => {
this.checkOnboarding().catch((err) => {
logger.error("Error checking onboarding:", err);
});
} catch (err: unknown) {
@ -487,7 +490,7 @@ export default class HomeView extends Vue {
try {
// Load DIDs first as it's critical
this.allMyDids = await retrieveAccountDids();
if (this.allMyDids.length === 0) {
this.isCreatingIdentifier = true;
const newDid = await generateSaveAndActivateIdentity();
@ -498,7 +501,7 @@ export default class HomeView extends Vue {
// Load settings and contacts in parallel
const [settings, contacts] = await Promise.all([
retrieveSettingsForActiveAccount(),
db.contacts.toArray()
db.contacts.toArray(),
]);
// Update state with loaded data
@ -511,36 +514,43 @@ export default class HomeView extends Vue {
this.isFeedFilteredByNearby = !!settings.filterFeedByNearby;
this.isRegistered = !!settings.isRegistered;
this.lastAckedOfferToUserJwtId = settings.lastAckedOfferToUserJwtId;
this.lastAckedOfferToUserProjectsJwtId = settings.lastAckedOfferToUserProjectsJwtId;
this.lastAckedOfferToUserProjectsJwtId =
settings.lastAckedOfferToUserProjectsJwtId;
this.searchBoxes = settings.searchBoxes || [];
this.showShortcutBvc = !!settings.showShortcutBvc;
this.isAnyFeedFilterOn = checkIsAnyFeedFilterOn(settings);
// Start non-critical operations
if (!settings.finishedOnboarding) {
(this.$refs.onboardingDialog as OnboardingDialog).open(OnboardPage.Home);
(this.$refs.onboardingDialog as OnboardingDialog).open(
OnboardPage.Home,
);
}
// Check registration status in background
if (!this.isRegistered && this.activeDid) {
this.checkRegistrationStatus().catch(err => {
this.checkRegistrationStatus().catch((err: TimeSafariError) => {
logger.error("Error checking registration status:", err);
});
}
// Start feed update in background
this.updateAllFeed().catch(err => {
this.updateAllFeed().catch((err: TimeSafariError) => {
logger.error("Error updating feed:", err);
});
} catch (err: any) {
logConsoleAndDb("Error retrieving settings or feed: " + err, true);
} catch (err) {
const error = err as TimeSafariError;
logConsoleAndDb(
"Error retrieving settings or feed: " + error.message,
true,
);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: (err as { userMessage?: string })?.userMessage ||
text:
error.userMessage ||
"There was an error retrieving your settings or the latest activity.",
},
5000,
@ -835,8 +845,9 @@ export default class HomeView extends Vue {
await this.processFeedResults(results.data);
await this.updateFeedLastViewedId(results.data);
}
} catch (e) {
this.handleFeedError(e);
} catch (err) {
const error = err as TimeSafariError;
this.handleFeedError(error);
}
if (this.feedData.length === 0 && !endOfResults) {
@ -878,10 +889,10 @@ export default class HomeView extends Vue {
if (processedRecord) {
this.feedData.push(processedRecord);
}
})
}),
);
// Allow UI to update between chunks
await new Promise(resolve => setTimeout(resolve, 0));
await new Promise((resolve) => setTimeout(resolve, 0));
}
this.feedPreviousOldestId = records[records.length - 1].jwtId;
}
@ -983,9 +994,11 @@ export default class HomeView extends Vue {
* @param claim The claim object containing giver information
* @returns The giver's DID
*/
private extractGiverDid(claim: any) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return claim.agent?.identifier || (claim.agent as any)?.did;
private extractGiverDid(claim: GiveVerifiableCredential): string {
if (!claim.agent?.identifier) {
throw new Error("Agent identifier is missing in claim");
}
return claim.agent.identifier;
}
/**
@ -994,9 +1007,11 @@ export default class HomeView extends Vue {
* @internal
* Called by processRecord()
*/
private extractRecipientDid(claim: any) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return claim.recipient?.identifier || (claim.recipient as any)?.did;
private extractRecipientDid(claim: GiveVerifiableCredential): string {
if (!claim.recipient?.identifier) {
throw new Error("Recipient identifier is missing in claim");
}
return claim.recipient.identifier;
}
/**
@ -1052,7 +1067,7 @@ export default class HomeView extends Vue {
*/
private shouldIncludeRecord(
record: GiveSummaryRecord,
fulfillsPlan: any,
fulfillsPlan: PlanSummaryRecord | null | undefined,
): boolean {
if (!this.isAnyFeedFilterOn) {
return true;
@ -1086,7 +1101,7 @@ export default class HomeView extends Vue {
* @internal
* Called by processRecord()
*/
private extractProvider(claim: any) {
private extractProvider(claim: GiveVerifiableCredential) {
return Array.isArray(claim.provider) ? claim.provider[0] : claim.provider;
}
@ -1096,7 +1111,9 @@ export default class HomeView extends Vue {
* @internal
* Called by processRecord()
*/
private async getProvidedByPlan(provider: any) {
private async getProvidedByPlan(
provider: GenericVerifiableCredential | null,
) {
return await getPlanFromCache(
provider?.identifier as string,
this.axios,
@ -1134,12 +1151,12 @@ export default class HomeView extends Vue {
*/
private createFeedRecord(
record: GiveSummaryRecord,
claim: any,
claim: GiveVerifiableCredential,
giverDid: string,
recipientDid: string,
provider: any,
fulfillsPlan: any,
providedByPlan: any,
provider: GenericVerifiableCredential | null,
fulfillsPlan: PlanSummaryRecord | null | undefined,
providedByPlan: PlanSummaryRecord | null | undefined,
): GiveRecordWithContactInfo {
return {
...record,
@ -1198,14 +1215,14 @@ export default class HomeView extends Vue {
* @internal
* Called by updateAllFeed()
*/
private handleFeedError(e: any) {
logger.error("Error with feed load:", e);
private handleFeedError(error: TimeSafariError) {
logger.error("Error with feed load:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Feed Error",
text: e.userMessage || "There was an error retrieving feed data.",
text: error.userMessage || "There was an error retrieving feed data.",
},
-1,
);
@ -1217,7 +1234,7 @@ export default class HomeView extends Vue {
* @internal
* Called by updateAllFeed()
* @param endorserApiServer API server URL
* @param beforeId OptioCalled by updateAllFeed()nal ID to fetch earlier results
* @param beforeId Optional ID to fetch earlier results
* @returns claims in reverse chronological order
*/
async retrieveGives(endorserApiServer: string, beforeId?: string) {
@ -1606,10 +1623,11 @@ export default class HomeView extends Vue {
* @param eventOrUrl Either an Event object or a direct image URL string
*/
private async openImageViewer(eventOrUrl: Event | string) {
const imageUrl = typeof eventOrUrl === 'string'
? eventOrUrl
: (eventOrUrl.target as HTMLElement).getAttribute('src') || '';
const imageUrl =
typeof eventOrUrl === "string"
? eventOrUrl
: (eventOrUrl.target as HTMLElement).getAttribute("src") || "";
if (!imageUrl) return;
// Check cache first
@ -1624,16 +1642,16 @@ export default class HomeView extends Vue {
// If not in cache, load it
try {
const response = await fetch(imageUrl);
if (!response.ok) throw new Error('Failed to load image');
if (!response.ok) throw new Error("Failed to load image");
const blob = await response.blob();
await this.cacheImageData(imageUrl, blob);
this.selectedImageData = blob;
this.selectedImage = imageUrl;
this.isImageViewerOpen = true;
} catch (error) {
logger.error('Error loading image:', error);
logger.error("Error loading image:", error);
this.$notify(
{
group: "alert",

Loading…
Cancel
Save