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.
This commit is contained in:
Matthew Raymer
2025-04-04 03:47:01 +00:00
parent b8c3517072
commit 6e2bdc69e9
15 changed files with 604 additions and 381 deletions

427
package-lock.json generated
View File

@@ -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"
"safe-buffer": "^5.2.1"
},
"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"
},
"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"
}

View File

@@ -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>

View File

@@ -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.

View File

@@ -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;
}
}
}
}

View File

@@ -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;

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

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

View File

@@ -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;
}
}

View File

@@ -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>;
}

View File

@@ -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);
}

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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",