Compare commits
19 Commits
dev_pr_tes
...
e33ea62e35
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e33ea62e35 | ||
|
|
96d95a50fc | ||
|
|
c1ce804d14 | ||
|
|
0aab9a838b | ||
|
|
6822638d47 | ||
|
|
5c1172a912 | ||
|
|
b5373c2d9a | ||
|
|
3ffcc60ee8 | ||
|
|
50e78c84c9 | ||
|
|
8e6c621f7b | ||
|
|
7b8511f080 | ||
|
|
f6fa19d0b2 | ||
|
|
536513ab3f | ||
|
|
240a7eea8b | ||
| d64a6557a8 | |||
|
|
22dc3abf65 | ||
|
|
ed1f6fc49d | ||
|
|
218b2e8b29 | ||
| 56bb72aab8 |
6
App.tsx
@@ -4,8 +4,10 @@
|
||||
* Main application component with authentication routing.
|
||||
* Shows loading screen while restoring auth state.
|
||||
*/
|
||||
import './src/polyfills';
|
||||
|
||||
import React from 'react';
|
||||
import { Buffer } from 'buffer';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import { NavigationContainer } from '@react-navigation/native';
|
||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||
@@ -15,6 +17,10 @@ import AuthNavigator from './src/navigation/AuthNavigator';
|
||||
import { AuthProvider, useAuth } from './src/context/AuthContext';
|
||||
import { colors } from './src/theme/colors';
|
||||
|
||||
if (typeof globalThis !== 'undefined' && !globalThis.Buffer) {
|
||||
globalThis.Buffer = Buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loading screen shown while restoring auth state
|
||||
*/
|
||||
|
||||
6
app.json
@@ -19,14 +19,10 @@
|
||||
"bundleIdentifier": "com.sentinel.app"
|
||||
},
|
||||
"android": {
|
||||
"adaptiveIcon": {
|
||||
"foregroundImage": "./assets/adaptive-icon.png",
|
||||
"backgroundColor": "#459E9E"
|
||||
},
|
||||
"package": "com.sentinel.app"
|
||||
},
|
||||
"web": {
|
||||
"favicon": "./assets/favicon.png",
|
||||
"favicon": "./assets/icon.png",
|
||||
"bundler": "metro"
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 86 B After Width: | Height: | Size: 70 B |
|
Before Width: | Height: | Size: 105 B After Width: | Height: | Size: 70 B |
BIN
assets/icon.png
|
Before Width: | Height: | Size: 52 B After Width: | Height: | Size: 70 B |
BIN
assets/images/icon.png
Normal file
|
After Width: | Height: | Size: 70 B |
|
Before Width: | Height: | Size: 108 B After Width: | Height: | Size: 70 B |
15
metro.config.js
Normal file
@@ -0,0 +1,15 @@
|
||||
const { getDefaultConfig } = require('expo/metro-config');
|
||||
const path = require('path');
|
||||
|
||||
const config = getDefaultConfig(__dirname);
|
||||
|
||||
config.resolver.extraNodeModules = {
|
||||
...config.resolver.extraNodeModules,
|
||||
crypto: path.resolve(__dirname, 'src/utils/crypto_polyfill.ts'),
|
||||
stream: require.resolve('readable-stream'),
|
||||
vm: require.resolve('vm-browserify'),
|
||||
async_hooks: path.resolve(__dirname, 'src/utils/async_hooks_mock.ts'),
|
||||
'node:async_hooks': path.resolve(__dirname, 'src/utils/async_hooks_mock.ts'),
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
566
package-lock.json
generated
@@ -10,13 +10,20 @@
|
||||
"dependencies": {
|
||||
"@expo/metro-runtime": "~4.0.1",
|
||||
"@expo/vector-icons": "~14.0.4",
|
||||
"@langchain/core": "^1.1.18",
|
||||
"@langchain/langgraph": "^1.1.3",
|
||||
"@noble/ciphers": "^1.3.0",
|
||||
"@noble/hashes": "^1.8.0",
|
||||
"@react-native-async-storage/async-storage": "^2.2.0",
|
||||
"@react-navigation/bottom-tabs": "^6.6.1",
|
||||
"@react-navigation/native": "^6.1.18",
|
||||
"@react-navigation/native-stack": "^6.11.0",
|
||||
"bip39": "^3.1.0",
|
||||
"buffer": "^6.0.3",
|
||||
"expo": "~52.0.0",
|
||||
"expo-asset": "~11.0.5",
|
||||
"expo-constants": "~17.0.8",
|
||||
"expo-crypto": "~14.0.2",
|
||||
"expo-font": "~13.0.4",
|
||||
"expo-haptics": "~14.0.0",
|
||||
"expo-image-picker": "^17.0.10",
|
||||
@@ -30,7 +37,9 @@
|
||||
"react-native-safe-area-context": "4.12.0",
|
||||
"react-native-screens": "~4.4.0",
|
||||
"react-native-view-shot": "^3.8.0",
|
||||
"react-native-web": "~0.19.13"
|
||||
"react-native-web": "~0.19.13",
|
||||
"readable-stream": "^4.7.0",
|
||||
"vm-browserify": "^1.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.25.2",
|
||||
@@ -80,7 +89,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz",
|
||||
"integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.28.6",
|
||||
"@babel/generator": "^7.28.6",
|
||||
@@ -484,6 +492,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz",
|
||||
"integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1",
|
||||
"@babel/traverse": "^7.28.5"
|
||||
@@ -500,6 +509,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz",
|
||||
"integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
},
|
||||
@@ -515,6 +525,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz",
|
||||
"integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
},
|
||||
@@ -530,6 +541,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz",
|
||||
"integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1",
|
||||
"@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
|
||||
@@ -547,6 +559,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.6.tgz",
|
||||
"integrity": "sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.28.6",
|
||||
"@babel/traverse": "^7.28.6"
|
||||
@@ -647,6 +660,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz",
|
||||
"integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
},
|
||||
@@ -767,6 +781,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz",
|
||||
"integrity": "sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.28.6"
|
||||
},
|
||||
@@ -953,6 +968,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz",
|
||||
"integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-create-regexp-features-plugin": "^7.18.6",
|
||||
"@babel/helper-plugin-utils": "^7.18.6"
|
||||
@@ -1018,6 +1034,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz",
|
||||
"integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
},
|
||||
@@ -1064,6 +1081,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.6.tgz",
|
||||
"integrity": "sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-create-class-features-plugin": "^7.28.6",
|
||||
"@babel/helper-plugin-utils": "^7.28.6"
|
||||
@@ -1132,6 +1150,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.28.6.tgz",
|
||||
"integrity": "sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-create-regexp-features-plugin": "^7.28.5",
|
||||
"@babel/helper-plugin-utils": "^7.28.6"
|
||||
@@ -1148,6 +1167,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz",
|
||||
"integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
},
|
||||
@@ -1163,6 +1183,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.28.6.tgz",
|
||||
"integrity": "sha512-5suVoXjC14lUN6ZL9OLKIHCNVWCrqGqlmEp/ixdXjvgnEl/kauLvvMO/Xw9NyMc95Joj1AeLVPVMvibBgSoFlA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-create-regexp-features-plugin": "^7.28.5",
|
||||
"@babel/helper-plugin-utils": "^7.28.6"
|
||||
@@ -1179,6 +1200,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz",
|
||||
"integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
},
|
||||
@@ -1194,6 +1216,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.6.tgz",
|
||||
"integrity": "sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.28.6",
|
||||
"@babel/plugin-transform-destructuring": "^7.28.5"
|
||||
@@ -1210,6 +1233,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.6.tgz",
|
||||
"integrity": "sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.28.6"
|
||||
},
|
||||
@@ -1289,6 +1313,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.28.6.tgz",
|
||||
"integrity": "sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.28.6"
|
||||
},
|
||||
@@ -1334,6 +1359,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz",
|
||||
"integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
},
|
||||
@@ -1349,6 +1375,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz",
|
||||
"integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-module-transforms": "^7.27.1",
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
@@ -1381,6 +1408,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.28.5.tgz",
|
||||
"integrity": "sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-module-transforms": "^7.28.3",
|
||||
"@babel/helper-plugin-utils": "^7.27.1",
|
||||
@@ -1399,6 +1427,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz",
|
||||
"integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-module-transforms": "^7.27.1",
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
@@ -1431,6 +1460,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz",
|
||||
"integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
},
|
||||
@@ -1495,6 +1525,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz",
|
||||
"integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1",
|
||||
"@babel/helper-replace-supers": "^7.27.1"
|
||||
@@ -1590,6 +1621,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz",
|
||||
"integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
},
|
||||
@@ -1715,6 +1747,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.28.6.tgz",
|
||||
"integrity": "sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-create-regexp-features-plugin": "^7.28.5",
|
||||
"@babel/helper-plugin-utils": "^7.28.6"
|
||||
@@ -1731,6 +1764,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz",
|
||||
"integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
},
|
||||
@@ -1827,6 +1861,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz",
|
||||
"integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
},
|
||||
@@ -1861,6 +1896,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz",
|
||||
"integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
},
|
||||
@@ -1876,6 +1912,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.28.6.tgz",
|
||||
"integrity": "sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-create-regexp-features-plugin": "^7.28.5",
|
||||
"@babel/helper-plugin-utils": "^7.28.6"
|
||||
@@ -1908,6 +1945,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.28.6.tgz",
|
||||
"integrity": "sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-create-regexp-features-plugin": "^7.28.5",
|
||||
"@babel/helper-plugin-utils": "^7.28.6"
|
||||
@@ -2026,6 +2064,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz",
|
||||
"integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.0.0",
|
||||
"@babel/types": "^7.4.4",
|
||||
@@ -2166,6 +2205,12 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@cfworker/json-schema": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/@cfworker/json-schema/-/json-schema-4.1.1.tgz",
|
||||
"integrity": "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@egjs/hammerjs": {
|
||||
"version": "2.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz",
|
||||
@@ -2643,7 +2688,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@expo/metro-runtime/-/metro-runtime-4.0.1.tgz",
|
||||
"integrity": "sha512-CRpbLvdJ1T42S+lrYa1iZp1KfDeBp4oeZOK3hdpiS5n0vR0nhD6sC1gGF0sTboCTp64tLteikz5Y3j53dvgOIw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"react-native": "*"
|
||||
}
|
||||
@@ -3180,6 +3224,228 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/core": {
|
||||
"version": "1.1.18",
|
||||
"resolved": "https://registry.npmmirror.com/@langchain/core/-/core-1.1.18.tgz",
|
||||
"integrity": "sha512-vwzbtHUSZaJONBA1n9uQedZPfyFFZ6XzTggTpR28n8tiIg7e1NC/5dvGW/lGtR1Du1VwV9DvDHA5/bOrLe6cVg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@cfworker/json-schema": "^4.0.2",
|
||||
"ansi-styles": "^5.0.0",
|
||||
"camelcase": "6",
|
||||
"decamelize": "1.2.0",
|
||||
"js-tiktoken": "^1.0.12",
|
||||
"langsmith": ">=0.4.0 <1.0.0",
|
||||
"mustache": "^4.2.0",
|
||||
"p-queue": "^6.6.2",
|
||||
"uuid": "^10.0.0",
|
||||
"zod": "^3.25.76 || ^4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/core/node_modules/ansi-styles": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-5.2.0.tgz",
|
||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/core/node_modules/camelcase": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/camelcase/-/camelcase-6.3.0.tgz",
|
||||
"integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/core/node_modules/uuid": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/uuid/-/uuid-10.0.0.tgz",
|
||||
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/langgraph": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/@langchain/langgraph/-/langgraph-1.1.3.tgz",
|
||||
"integrity": "sha512-o/cEWeocDDSpyBI2MfX07LkNG4LzdRKxwcgUcbR4PyRzhxxCkeIZRCCYkXVQoDbdKqAczJa0D7+yjU9rmA5iHQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@langchain/langgraph-checkpoint": "^1.0.0",
|
||||
"@langchain/langgraph-sdk": "~1.5.5",
|
||||
"@standard-schema/spec": "1.1.0",
|
||||
"uuid": "^10.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@langchain/core": "^1.0.1",
|
||||
"zod": "^3.25.32 || ^4.2.0",
|
||||
"zod-to-json-schema": "^3.x"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"zod-to-json-schema": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/langgraph-checkpoint": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/@langchain/langgraph-checkpoint/-/langgraph-checkpoint-1.0.0.tgz",
|
||||
"integrity": "sha512-xrclBGvNCXDmi0Nz28t3vjpxSH6UYx6w5XAXSiiB1WEdc2xD2iY/a913I3x3a31XpInUW/GGfXXfePfaghV54A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"uuid": "^10.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@langchain/core": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/langgraph-checkpoint/node_modules/uuid": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/uuid/-/uuid-10.0.0.tgz",
|
||||
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/langgraph-sdk": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmmirror.com/@langchain/langgraph-sdk/-/langgraph-sdk-1.5.5.tgz",
|
||||
"integrity": "sha512-SyiAs6TVXPWlt/8cI9pj/43nbIvclY3ytKqUFbL5MplCUnItetEyqvH87EncxyVF5D7iJKRZRfSVYBMmOZbjbQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-queue": "^9.0.1",
|
||||
"p-retry": "^7.1.1",
|
||||
"uuid": "^13.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@langchain/core": "^1.1.15",
|
||||
"react": "^18 || ^19",
|
||||
"react-dom": "^18 || ^19"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@langchain/core": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/langgraph-sdk/node_modules/eventemitter3": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-5.0.4.tgz",
|
||||
"integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@langchain/langgraph-sdk/node_modules/p-queue": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/p-queue/-/p-queue-9.1.0.tgz",
|
||||
"integrity": "sha512-O/ZPaXuQV29uSLbxWBGGZO1mCQXV2BLIwUr59JUU9SoH76mnYvtms7aafH/isNSNGwuEfP6W/4xD0/TJXxrizw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eventemitter3": "^5.0.1",
|
||||
"p-timeout": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/langgraph-sdk/node_modules/p-timeout": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/p-timeout/-/p-timeout-7.0.1.tgz",
|
||||
"integrity": "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/langgraph-sdk/node_modules/uuid": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/uuid/-/uuid-13.0.0.tgz",
|
||||
"integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist-node/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/langgraph/node_modules/uuid": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/uuid/-/uuid-10.0.0.tgz",
|
||||
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/ciphers": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/@noble/ciphers/-/ciphers-1.3.0.tgz",
|
||||
"integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^14.21.3 || >=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/hashes": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmmirror.com/@noble/hashes/-/hashes-1.8.0.tgz",
|
||||
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^14.21.3 || >=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
@@ -3688,7 +3954,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-6.1.18.tgz",
|
||||
"integrity": "sha512-mIT9MiL/vMm4eirLcmw2h6h/Nm5FICtnYSdohq4vTLA2FF/6PNhByM7s8ffqoVfE5L0uAa6Xda1B7oddolUiGg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@react-navigation/core": "^6.4.17",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
@@ -3759,6 +4024,12 @@
|
||||
"@sinonjs/commons": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@standard-schema/spec": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||
@@ -3870,7 +4141,6 @@
|
||||
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^3.2.2"
|
||||
@@ -3882,6 +4152,12 @@
|
||||
"integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/uuid": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/@types/uuid/-/uuid-10.0.0.tgz",
|
||||
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/yargs": {
|
||||
"version": "17.0.35",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz",
|
||||
@@ -4420,6 +4696,15 @@
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/bip39": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bip39/-/bip39-3.1.0.tgz",
|
||||
"integrity": "sha512-c9kiwdk45Do5GL0vJMe7tS95VjCii65mYAH7DfWl3uW8AVzXKQVUm64i3hzVybBDMp9r7j9iNxR85+ul8MdN/A==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@noble/hashes": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bplist-creator": {
|
||||
"version": "0.0.7",
|
||||
"resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.0.7.tgz",
|
||||
@@ -4482,7 +4767,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@@ -4507,9 +4791,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -4527,7 +4811,7 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.1.13"
|
||||
"ieee754": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-alloc": {
|
||||
@@ -5033,6 +5317,15 @@
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/console-table-printer": {
|
||||
"version": "2.15.0",
|
||||
"resolved": "https://registry.npmmirror.com/console-table-printer/-/console-table-printer-2.15.0.tgz",
|
||||
"integrity": "sha512-SrhBq4hYVjLCkBVOWaTzceJalvn5K1Zq5aQA6wXC/cYjI3frKWNPEMK3sZsJfNNQApvCQmgBcc13ZKmFj8qExw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"simple-wcswidth": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/convert-source-map": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||
@@ -5172,6 +5465,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decamelize": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/decamelize/-/decamelize-1.2.0.tgz",
|
||||
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/decode-uri-component": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
|
||||
@@ -5507,6 +5809,7 @@
|
||||
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
||||
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -5529,6 +5832,21 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/eventemitter3": {
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
||||
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/events": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/events/-/events-3.3.0.tgz",
|
||||
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.8.x"
|
||||
}
|
||||
},
|
||||
"node_modules/exec-async": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/exec-async/-/exec-async-2.2.0.tgz",
|
||||
@@ -5631,7 +5949,6 @@
|
||||
"resolved": "https://registry.npmjs.org/expo/-/expo-52.0.48.tgz",
|
||||
"integrity": "sha512-/HR/vuo57KGEWlvF3GWaquwEAjXuA5hrOCsaLcZ3pMSA8mQ27qKd1jva4GWzpxXYedlzs/7LLP1XpZo6hXTsog==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.0",
|
||||
"@expo/cli": "0.22.27",
|
||||
@@ -5707,6 +6024,18 @@
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-crypto": {
|
||||
"version": "14.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/expo-crypto/-/expo-crypto-14.0.2.tgz",
|
||||
"integrity": "sha512-WRc9PBpJraJN29VD5Ef7nCecxJmZNyRKcGkNiDQC1nhY5agppzwhqh7zEzNFarE/GqDgSiaDHS8yd5EgFhP9AQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"expo": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-font": {
|
||||
"version": "13.0.4",
|
||||
"resolved": "https://registry.npmjs.org/expo-font/-/expo-font-13.0.4.tgz",
|
||||
@@ -6726,6 +7055,18 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-network-error": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/is-network-error/-/is-network-error-1.3.0.tgz",
|
||||
"integrity": "sha512-6oIwpsgRfnDiyEDLMay/GqCl3HoAtH5+RUKW29gYkL0QA+ipzpDLA16yQs7/RHCSu+BwgbJaOUqa4A99qNVQVw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
@@ -7044,6 +7385,15 @@
|
||||
"integrity": "sha512-bF7vcQxbODoGK1imE2P9GS9aw4zD0Sd+Hni68IMZLj7zRnquH7dXUmMw9hDI5S/Jzt7q+IyTXN0rSg2GI0IKhQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-tiktoken": {
|
||||
"version": "1.0.21",
|
||||
"resolved": "https://registry.npmmirror.com/js-tiktoken/-/js-tiktoken-1.0.21.tgz",
|
||||
"integrity": "sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.5.1"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@@ -7164,6 +7514,65 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/langsmith": {
|
||||
"version": "0.4.12",
|
||||
"resolved": "https://registry.npmmirror.com/langsmith/-/langsmith-0.4.12.tgz",
|
||||
"integrity": "sha512-YWt0jcGvKqjUgIvd78rd4QcdMss0lUkeUaqp0UpVRq7H2yNDx8H5jOUO/laWUmaPtWGgcip0qturykXe1g9Gqw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/uuid": "^10.0.0",
|
||||
"chalk": "^4.1.2",
|
||||
"console-table-printer": "^2.12.1",
|
||||
"p-queue": "^6.6.2",
|
||||
"semver": "^7.6.3",
|
||||
"uuid": "^10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "*",
|
||||
"@opentelemetry/exporter-trace-otlp-proto": "*",
|
||||
"@opentelemetry/sdk-trace-base": "*",
|
||||
"openai": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@opentelemetry/api": {
|
||||
"optional": true
|
||||
},
|
||||
"@opentelemetry/exporter-trace-otlp-proto": {
|
||||
"optional": true
|
||||
},
|
||||
"@opentelemetry/sdk-trace-base": {
|
||||
"optional": true
|
||||
},
|
||||
"openai": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/langsmith/node_modules/semver": {
|
||||
"version": "7.7.3",
|
||||
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.3.tgz",
|
||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/langsmith/node_modules/uuid": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/uuid/-/uuid-10.0.0.tgz",
|
||||
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/leven": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
|
||||
@@ -8250,6 +8659,15 @@
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mustache": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/mustache/-/mustache-4.2.0.tgz",
|
||||
"integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mustache": "bin/mustache"
|
||||
}
|
||||
},
|
||||
"node_modules/mz": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
|
||||
@@ -8664,6 +9082,49 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-queue": {
|
||||
"version": "6.6.2",
|
||||
"resolved": "https://registry.npmmirror.com/p-queue/-/p-queue-6.6.2.tgz",
|
||||
"integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eventemitter3": "^4.0.4",
|
||||
"p-timeout": "^3.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-retry": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/p-retry/-/p-retry-7.1.1.tgz",
|
||||
"integrity": "sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-network-error": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-timeout": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/p-timeout/-/p-timeout-3.2.0.tgz",
|
||||
"integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-finally": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/p-try": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||
@@ -9014,6 +9475,15 @@
|
||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/process": {
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmmirror.com/process/-/process-0.11.10.tgz",
|
||||
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/progress": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
||||
@@ -9159,7 +9629,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
},
|
||||
@@ -9243,7 +9712,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.76.9.tgz",
|
||||
"integrity": "sha512-+LRwecWmTDco7OweGsrECIqJu0iyrREd6CTCgC/uLLYipiHvk+MH9nd6drFtCw/6Blz6eoKTcH9YTTJusNtrWg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jest/create-cache-key-function": "^29.6.3",
|
||||
"@react-native/assets-registry": "0.76.9",
|
||||
@@ -9345,7 +9813,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-4.12.0.tgz",
|
||||
"integrity": "sha512-ukk5PxcF4p3yu6qMZcmeiZgowhb5AsKRnil54YFUUAXVIS7PJcMHGGC+q44fCiBg44/1AJk5njGMez1m9H0BVQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
@@ -9356,7 +9823,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.4.0.tgz",
|
||||
"integrity": "sha512-c7zc7Zwjty6/pGyuuvh9gK3YBYqHPOxrhXfG1lF4gHlojQSmIx2piNbNaV+Uykj+RDTmFXK0e/hA+fucw/Qozg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"react-freeze": "^1.0.0",
|
||||
"warn-once": "^0.1.0"
|
||||
@@ -9489,6 +9955,22 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-4.7.0.tgz",
|
||||
"integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"abort-controller": "^3.0.0",
|
||||
"buffer": "^6.0.3",
|
||||
"events": "^3.3.0",
|
||||
"process": "^0.11.10",
|
||||
"string_decoder": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/readline": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/readline/-/readline-1.3.0.tgz",
|
||||
@@ -10035,6 +10517,12 @@
|
||||
"integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/simple-wcswidth": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/simple-wcswidth/-/simple-wcswidth-1.1.2.tgz",
|
||||
"integrity": "sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sisteransi": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
|
||||
@@ -10189,6 +10677,15 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||
@@ -10966,6 +11463,12 @@
|
||||
"integrity": "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vm-browserify": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/vm-browserify/-/vm-browserify-1.1.2.tgz",
|
||||
"integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/walker": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
|
||||
@@ -11035,6 +11538,30 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-url-without-unicode/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/whatwg-url-without-unicode/node_modules/webidl-conversions": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz",
|
||||
@@ -11346,6 +11873,15 @@
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "4.3.6",
|
||||
"resolved": "https://registry.npmmirror.com/zod/-/zod-4.3.6.tgz",
|
||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
13
package.json
@@ -11,13 +11,20 @@
|
||||
"dependencies": {
|
||||
"@expo/metro-runtime": "~4.0.1",
|
||||
"@expo/vector-icons": "~14.0.4",
|
||||
"@langchain/core": "^1.1.18",
|
||||
"@langchain/langgraph": "^1.1.3",
|
||||
"@noble/ciphers": "^1.3.0",
|
||||
"@noble/hashes": "^1.8.0",
|
||||
"@react-native-async-storage/async-storage": "^2.2.0",
|
||||
"@react-navigation/bottom-tabs": "^6.6.1",
|
||||
"@react-navigation/native": "^6.1.18",
|
||||
"@react-navigation/native-stack": "^6.11.0",
|
||||
"bip39": "^3.1.0",
|
||||
"buffer": "^6.0.3",
|
||||
"expo": "~52.0.0",
|
||||
"expo-asset": "~11.0.5",
|
||||
"expo-constants": "~17.0.8",
|
||||
"expo-crypto": "~14.0.2",
|
||||
"expo-font": "~13.0.4",
|
||||
"expo-haptics": "~14.0.0",
|
||||
"expo-image-picker": "^17.0.10",
|
||||
@@ -27,11 +34,13 @@
|
||||
"react-dom": "18.3.1",
|
||||
"react-native": "^0.76.9",
|
||||
"react-native-gesture-handler": "~2.20.2",
|
||||
"react-native-view-shot": "^3.8.0",
|
||||
"react-native-reanimated": "~3.16.1",
|
||||
"react-native-safe-area-context": "4.12.0",
|
||||
"react-native-screens": "~4.4.0",
|
||||
"react-native-web": "~0.19.13"
|
||||
"react-native-view-shot": "^3.8.0",
|
||||
"react-native-web": "~0.19.13",
|
||||
"readable-stream": "^4.7.0",
|
||||
"vm-browserify": "^1.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.25.2",
|
||||
|
||||
@@ -62,20 +62,18 @@ export default function BiometricModal({
|
||||
Animated.sequence([
|
||||
Animated.timing(scanAnimation, {
|
||||
toValue: 1,
|
||||
duration: 800,
|
||||
duration: 400,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(scanAnimation, {
|
||||
toValue: 0,
|
||||
duration: 800,
|
||||
duration: 400,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]),
|
||||
{ iterations: 2 }
|
||||
{ iterations: 1 }
|
||||
).start(() => {
|
||||
setTimeout(() => {
|
||||
onSuccess();
|
||||
}, 300);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -51,11 +51,13 @@ export const API_ENDPOINTS = {
|
||||
CREATE: '/assets/create',
|
||||
CLAIM: '/assets/claim',
|
||||
ASSIGN: '/assets/assign',
|
||||
DELETE: '/assets/delete',
|
||||
},
|
||||
|
||||
// AI Services
|
||||
AI: {
|
||||
PROXY: '/ai/proxy',
|
||||
GET_ROLES: '/get_ai_roles',
|
||||
},
|
||||
|
||||
// Admin Operations
|
||||
@@ -64,6 +66,48 @@ export const API_ENDPOINTS = {
|
||||
},
|
||||
} as const;
|
||||
|
||||
// =============================================================================
|
||||
// Vault storage (user-isolated, multi-account)
|
||||
// =============================================================================
|
||||
// - AsyncStorage keys for vault state (S0 share, initialized flag, mnemonic part backup).
|
||||
// - User-scoped: each account has its own keys so vault/mnemonic state is isolated.
|
||||
// - Store: use getVaultStorageKeys(userId) and write to INITIALIZED / SHARE_DEVICE / MNEMONIC_PART_LOCAL.
|
||||
// - Clear: use same keys in multiRemove (e.g. MeScreen Reset Vault State).
|
||||
// - Multi-account: same device, multiple users → each has independent vault (no cross-user leakage).
|
||||
|
||||
const VAULT_KEY_PREFIX = 'sentinel_vault';
|
||||
|
||||
/** Base key names (for reference). Prefer getVaultStorageKeys(userId) for all reads/writes. */
|
||||
export const VAULT_STORAGE_KEYS = {
|
||||
INITIALIZED: 'sentinel_vault_initialized',
|
||||
SHARE_DEVICE: 'sentinel_vault_s0',
|
||||
MNEMONIC_PART_LOCAL: 'sentinel_mnemonic_part_local',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Returns vault storage keys for the given user (user isolation).
|
||||
* - Use for: reading/writing S0, mnemonic part backup, clearing on Reset Vault State.
|
||||
* - userId null → guest namespace (_guest). userId set → per-user namespace (_u{userId}).
|
||||
*/
|
||||
export function getVaultStorageKeys(userId: number | string | null): {
|
||||
INITIALIZED: string;
|
||||
SHARE_DEVICE: string;
|
||||
MNEMONIC_PART_LOCAL: string;
|
||||
AES_KEY: string;
|
||||
SHARE_SERVER: string;
|
||||
SHARE_HEIR: string;
|
||||
} {
|
||||
const suffix = userId != null ? `_u${userId}` : '_guest';
|
||||
return {
|
||||
INITIALIZED: `${VAULT_KEY_PREFIX}_initialized${suffix}`,
|
||||
SHARE_DEVICE: `${VAULT_KEY_PREFIX}_s0${suffix}`,
|
||||
MNEMONIC_PART_LOCAL: `sentinel_mnemonic_part_local${suffix}`,
|
||||
AES_KEY: `sentinel_aes_key${suffix}`,
|
||||
SHARE_SERVER: `sentinel_share_server${suffix}`,
|
||||
SHARE_HEIR: `sentinel_share_heir${suffix}`,
|
||||
};
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Helper Functions
|
||||
// =============================================================================
|
||||
@@ -112,6 +156,7 @@ export const MOCK_CONFIG = {
|
||||
USER: {
|
||||
id: 999,
|
||||
username: 'MockCaptain',
|
||||
email: 'captain@sentinel.local',
|
||||
public_key: 'mock_public_key',
|
||||
is_admin: true,
|
||||
guale: false,
|
||||
@@ -137,6 +182,44 @@ export const AI_CONFIG = {
|
||||
* Mock response delay in milliseconds (for NO_BACKEND_MODE)
|
||||
*/
|
||||
MOCK_RESPONSE_DELAY: 500,
|
||||
|
||||
/**
|
||||
* AI Roles configuration
|
||||
*/
|
||||
ROLES: [
|
||||
{
|
||||
id: 'reflective',
|
||||
name: 'Reflective Assistant',
|
||||
description: 'Helps you dive deep into your thoughts and feelings through meaningful reflection.',
|
||||
systemPrompt: 'You are a helpful journal assistant. Help the user reflect on their thoughts and feelings.',
|
||||
icon: 'journal-outline',
|
||||
iconFamily: 'Ionicons',
|
||||
},
|
||||
{
|
||||
id: 'creative',
|
||||
name: 'Creative Spark',
|
||||
description: 'A partner for brainstorming, creative writing, and exploring new ideas.',
|
||||
systemPrompt: 'You are a creative brainstorming partner. Help the user explore new ideas, write stories, or look at things from a fresh perspective.',
|
||||
icon: 'bulb-outline',
|
||||
iconFamily: 'Ionicons',
|
||||
},
|
||||
{
|
||||
id: 'planner',
|
||||
name: 'Action Planner',
|
||||
description: 'Focused on turning thoughts into actionable plans and organized goals.',
|
||||
systemPrompt: 'You are a productivity coach. Help the user break down their thoughts into actionable steps and clear goals.',
|
||||
icon: 'list-outline',
|
||||
iconFamily: 'Ionicons',
|
||||
},
|
||||
{
|
||||
id: 'empathetic',
|
||||
name: 'Empathetic Guide',
|
||||
description: 'Provides a safe, non-judgmental space for emotional support and empathy.',
|
||||
systemPrompt: 'You are a supportive and empathetic friend. Listen to the user\'s concerns and provide emotional support without judgment.',
|
||||
icon: 'heart-outline',
|
||||
iconFamily: 'Ionicons',
|
||||
},
|
||||
],
|
||||
} as const;
|
||||
|
||||
// =============================================================================
|
||||
|
||||
@@ -7,8 +7,10 @@
|
||||
|
||||
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { User, LoginRequest, RegisterRequest } from '../types';
|
||||
import { User, LoginRequest, RegisterRequest, AIRole } from '../types';
|
||||
import { authService } from '../services/auth.service';
|
||||
import { aiService } from '../services/ai.service';
|
||||
import { storageService } from '../services/storage.service';
|
||||
|
||||
// =============================================================================
|
||||
// Type Definitions
|
||||
@@ -17,11 +19,13 @@ import { authService } from '../services/auth.service';
|
||||
interface AuthContextType {
|
||||
user: User | null;
|
||||
token: string | null;
|
||||
aiRoles: AIRole[];
|
||||
isLoading: boolean;
|
||||
isInitializing: boolean;
|
||||
signIn: (credentials: LoginRequest) => Promise<void>;
|
||||
signUp: (data: RegisterRequest) => Promise<void>;
|
||||
signOut: () => void;
|
||||
refreshAIRoles: () => Promise<void>;
|
||||
}
|
||||
|
||||
// Storage keys
|
||||
@@ -43,6 +47,7 @@ const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [token, setToken] = useState<string | null>(null);
|
||||
const [aiRoles, setAIRoles] = useState<AIRole[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isInitializing, setIsInitializing] = useState(true);
|
||||
|
||||
@@ -65,6 +70,8 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
setToken(storedToken);
|
||||
setUser(JSON.parse(storedUser));
|
||||
console.log('[Auth] Restored session for user:', JSON.parse(storedUser).username);
|
||||
// Fetch AI roles after restoring session
|
||||
fetchAIRoles(storedToken);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Auth] Failed to load stored auth:', error);
|
||||
@@ -73,6 +80,29 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch AI roles from API
|
||||
*/
|
||||
const fetchAIRoles = async (authToken: string) => {
|
||||
console.log('[Auth] Fetching AI roles with token:', authToken ? `${authToken.substring(0, 10)}...` : 'MISSING');
|
||||
try {
|
||||
const roles = await aiService.getAIRoles(authToken);
|
||||
setAIRoles(roles);
|
||||
console.log('[Auth] AI roles fetched successfully:', roles.length);
|
||||
} catch (error) {
|
||||
console.error('[Auth] Failed to fetch AI roles:', error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Manual refresh of AI roles
|
||||
*/
|
||||
const refreshAIRoles = async () => {
|
||||
if (token) {
|
||||
await fetchAIRoles(token);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Save authentication to AsyncStorage
|
||||
*/
|
||||
@@ -113,6 +143,8 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
setToken(response.access_token);
|
||||
setUser(response.user);
|
||||
await saveAuth(response.access_token, response.user);
|
||||
// Fetch AI roles immediately after login
|
||||
await fetchAIRoles(response.access_token);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
} finally {
|
||||
@@ -137,12 +169,16 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
};
|
||||
|
||||
/**
|
||||
* Sign out and clear stored auth
|
||||
* Sign out and clear stored auth and session data
|
||||
*/
|
||||
const signOut = () => {
|
||||
setUser(null);
|
||||
setToken(null);
|
||||
setAIRoles([]);
|
||||
clearAuth();
|
||||
|
||||
|
||||
//storageService.clearAllData();
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -150,11 +186,13 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
value={{
|
||||
user,
|
||||
token,
|
||||
aiRoles,
|
||||
isLoading,
|
||||
isInitializing,
|
||||
signIn,
|
||||
signUp,
|
||||
signOut
|
||||
signOut,
|
||||
refreshAIRoles
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
6
src/hooks/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* React hooks for Sentinel
|
||||
*/
|
||||
|
||||
export { useVaultAssets } from './useVaultAssets';
|
||||
export type { CreateAssetResult, UseVaultAssetsReturn } from './useVaultAssets';
|
||||
278
src/hooks/useVaultAssets.ts
Normal file
@@ -0,0 +1,278 @@
|
||||
/**
|
||||
* useVaultAssets: Encapsulates /assets/get and /assets/create for VaultScreen.
|
||||
* - Fetches assets when vault is unlocked and token exists.
|
||||
* - Exposes createAsset with 401/network error handling and list refresh on success.
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import * as bip39 from 'bip39';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { assetsService } from '../services/assets.service';
|
||||
import { getVaultStorageKeys, DEBUG_MODE } from '../config';
|
||||
import { SentinelVault } from '../utils/crypto_core';
|
||||
import { storageService } from '../services/storage.service';
|
||||
import {
|
||||
initialVaultAssets,
|
||||
mapApiAssetsToVaultAssets,
|
||||
type ApiAsset,
|
||||
} from '../utils/vaultAssets';
|
||||
import type { VaultAsset } from '../types';
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Types
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
export interface CreateAssetResult {
|
||||
success: boolean;
|
||||
isUnauthorized?: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface UseVaultAssetsReturn {
|
||||
/** Current list (mock until API succeeds) */
|
||||
assets: VaultAsset[];
|
||||
/** Replace list (e.g. after external refresh) */
|
||||
setAssets: React.Dispatch<React.SetStateAction<VaultAsset[]>>;
|
||||
/** Refetch from GET /assets/get */
|
||||
refreshAssets: () => Promise<void>;
|
||||
/** Create asset via POST /assets/create; on success refreshes list */
|
||||
createAsset: (params: { title: string; content: string }) => Promise<CreateAssetResult>;
|
||||
/** Delete asset via POST /assets/delete; on success refreshes list */
|
||||
deleteAsset: (assetId: number) => Promise<CreateAssetResult>;
|
||||
/** Assign asset to heir via POST /assets/assign */
|
||||
assignAsset: (assetId: number, heirEmail: string) => Promise<CreateAssetResult>;
|
||||
/** True while create request is in flight */
|
||||
isSealing: boolean;
|
||||
/** Error message from last create failure (non-401) */
|
||||
createError: string | null;
|
||||
/** Clear createError */
|
||||
clearCreateError: () => void;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Hook
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Vault assets list + create. Fetches on unlock when token exists; keeps mock on error.
|
||||
*/
|
||||
export function useVaultAssets(isUnlocked: boolean): UseVaultAssetsReturn {
|
||||
const { user, token, signOut } = useAuth();
|
||||
const [assets, setAssets] = useState<VaultAsset[]>(initialVaultAssets);
|
||||
const [isSealing, setIsSealing] = useState(false);
|
||||
const [createError, setCreateError] = useState<string | null>(null);
|
||||
|
||||
const refreshAssets = useCallback(async () => {
|
||||
if (!token) return;
|
||||
try {
|
||||
const list = await assetsService.getMyAssets(token);
|
||||
if (Array.isArray(list)) {
|
||||
setAssets(mapApiAssetsToVaultAssets(list as ApiAsset[]));
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const rawMessage = err instanceof Error ? err.message : String(err ?? '');
|
||||
if (/Could not validate credentials/i.test(rawMessage)) {
|
||||
signOut();
|
||||
}
|
||||
// Keep current assets (mock or previous fetch)
|
||||
}
|
||||
}, [token, signOut]);
|
||||
|
||||
// Fetch list when unlocked and token exists
|
||||
useEffect(() => {
|
||||
if (!isUnlocked || !token) return;
|
||||
let cancelled = false;
|
||||
assetsService
|
||||
.getMyAssets(token)
|
||||
.then((list) => {
|
||||
if (!cancelled && Array.isArray(list)) {
|
||||
setAssets(mapApiAssetsToVaultAssets(list as ApiAsset[]));
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (!cancelled) {
|
||||
const rawMessage = err instanceof Error ? err.message : String(err ?? '');
|
||||
if (/Could not validate credentials/i.test(rawMessage)) {
|
||||
signOut();
|
||||
}
|
||||
}
|
||||
// Keep initial (mock) assets
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [isUnlocked, token]);
|
||||
|
||||
const createAsset = useCallback(
|
||||
async ({
|
||||
title,
|
||||
content,
|
||||
}: {
|
||||
title: string;
|
||||
content: string;
|
||||
}): Promise<CreateAssetResult> => {
|
||||
if (!token) {
|
||||
return { success: false, error: 'Not logged in.' };
|
||||
}
|
||||
setIsSealing(true);
|
||||
setCreateError(null);
|
||||
try {
|
||||
const vaultKeys = getVaultStorageKeys(user?.id ?? null);
|
||||
const [s1Str, aesKeyHex, s0Str, s2Str] = await Promise.all([
|
||||
AsyncStorage.getItem(vaultKeys.SHARE_SERVER),
|
||||
AsyncStorage.getItem(vaultKeys.AES_KEY),
|
||||
AsyncStorage.getItem(vaultKeys.SHARE_DEVICE),
|
||||
AsyncStorage.getItem(vaultKeys.SHARE_HEIR),
|
||||
]);
|
||||
|
||||
if (!s1Str || !aesKeyHex) {
|
||||
throw new Error('Vault keys missing. Please re-unlock your vault.');
|
||||
}
|
||||
|
||||
const vault = new SentinelVault();
|
||||
const aesKey = Buffer.from(aesKeyHex, 'hex');
|
||||
const encryptedBuffer = vault.encryptData(aesKey, content.trim());
|
||||
const content_inner_encrypted = encryptedBuffer.toString('hex');
|
||||
|
||||
if (DEBUG_MODE) {
|
||||
console.log('[DEBUG] Crypto Data during Asset Creation:');
|
||||
console.log(' s0 (Device):', s0Str);
|
||||
console.log(' s1 (Server):', s1Str);
|
||||
console.log(' s2 (Heir): ', s2Str);
|
||||
console.log(' AES Key: ', aesKeyHex);
|
||||
console.log(' Encrypted: ', content_inner_encrypted);
|
||||
}
|
||||
|
||||
const createdAsset = await assetsService.createAsset(
|
||||
{
|
||||
title: title.trim(),
|
||||
private_key_shard: s1Str,
|
||||
content_inner_encrypted,
|
||||
},
|
||||
token
|
||||
);
|
||||
|
||||
// Backup plaintext content locally
|
||||
if (createdAsset && createdAsset.id && user?.id) {
|
||||
await storageService.saveAssetBackup(createdAsset.id, content, user.id);
|
||||
}
|
||||
await refreshAssets();
|
||||
return { success: true };
|
||||
} catch (err: unknown) {
|
||||
const status =
|
||||
err && typeof err === 'object' && 'status' in err
|
||||
? (err as { status?: number }).status
|
||||
: undefined;
|
||||
const rawMessage =
|
||||
err instanceof Error ? err.message : String(err ?? 'Failed to create.');
|
||||
const isUnauthorized =
|
||||
status === 401 || /401|Unauthorized/i.test(rawMessage);
|
||||
|
||||
if (isUnauthorized) {
|
||||
signOut();
|
||||
return { success: false, isUnauthorized: true };
|
||||
}
|
||||
|
||||
const friendlyMessage = /failed to fetch|network error/i.test(rawMessage)
|
||||
? 'Network error. Please check that the backend is running and reachable (see API_BASE_URL in config).'
|
||||
: rawMessage;
|
||||
setCreateError(friendlyMessage);
|
||||
return { success: false, error: friendlyMessage };
|
||||
} finally {
|
||||
setIsSealing(false);
|
||||
}
|
||||
},
|
||||
[token, user, refreshAssets, signOut]
|
||||
);
|
||||
|
||||
const deleteAsset = useCallback(
|
||||
async (assetId: number): Promise<CreateAssetResult> => {
|
||||
if (!token) {
|
||||
return { success: false, error: 'Not logged in.' };
|
||||
}
|
||||
setIsSealing(true);
|
||||
setCreateError(null);
|
||||
try {
|
||||
await assetsService.deleteAsset(assetId, token);
|
||||
await refreshAssets();
|
||||
return { success: true };
|
||||
} catch (err: unknown) {
|
||||
const status =
|
||||
err && typeof err === 'object' && 'status' in err
|
||||
? (err as { status?: number }).status
|
||||
: undefined;
|
||||
const rawMessage =
|
||||
err instanceof Error ? err.message : String(err ?? 'Failed to delete.');
|
||||
const isUnauthorized =
|
||||
status === 401 || /401|Unauthorized/i.test(rawMessage);
|
||||
|
||||
if (isUnauthorized) {
|
||||
signOut();
|
||||
return { success: false, isUnauthorized: true };
|
||||
}
|
||||
|
||||
const friendlyMessage = /failed to fetch|network error/i.test(rawMessage)
|
||||
? 'Network error. Please check that the backend is running and reachable.'
|
||||
: rawMessage;
|
||||
setCreateError(friendlyMessage);
|
||||
return { success: false, error: friendlyMessage };
|
||||
} finally {
|
||||
setIsSealing(false);
|
||||
}
|
||||
},
|
||||
[token, refreshAssets, signOut]
|
||||
);
|
||||
|
||||
const assignAsset = useCallback(
|
||||
async (assetId: number, heirEmail: string): Promise<CreateAssetResult> => {
|
||||
if (!token) {
|
||||
return { success: false, error: 'Not logged in.' };
|
||||
}
|
||||
setIsSealing(true);
|
||||
setCreateError(null);
|
||||
try {
|
||||
await assetsService.assignAsset({ asset_id: assetId, heir_email: heirEmail }, token);
|
||||
await refreshAssets();
|
||||
return { success: true };
|
||||
} catch (err: unknown) {
|
||||
const status =
|
||||
err && typeof err === 'object' && 'status' in err
|
||||
? (err as { status?: number }).status
|
||||
: undefined;
|
||||
const rawMessage =
|
||||
err instanceof Error ? err.message : String(err ?? 'Failed to assign.');
|
||||
const isUnauthorized =
|
||||
status === 401 || /401|Unauthorized/i.test(rawMessage);
|
||||
|
||||
if (isUnauthorized) {
|
||||
signOut();
|
||||
return { success: false, isUnauthorized: true };
|
||||
}
|
||||
|
||||
const friendlyMessage = /failed to fetch|network error/i.test(rawMessage)
|
||||
? 'Network error. Please check that the backend is running and reachable.'
|
||||
: rawMessage;
|
||||
setCreateError(friendlyMessage);
|
||||
return { success: false, error: friendlyMessage };
|
||||
} finally {
|
||||
setIsSealing(false);
|
||||
}
|
||||
},
|
||||
[token, signOut]
|
||||
);
|
||||
|
||||
const clearCreateError = useCallback(() => setCreateError(null), []);
|
||||
|
||||
return {
|
||||
assets,
|
||||
setAssets,
|
||||
refreshAssets,
|
||||
createAsset,
|
||||
deleteAsset,
|
||||
assignAsset,
|
||||
isSealing,
|
||||
createError,
|
||||
clearCreateError,
|
||||
};
|
||||
}
|
||||
45
src/polyfills.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Polyfills that must run before any other app code (including LangChain/LangGraph).
|
||||
* This file is imported as the very first line in App.tsx so that ReadableStream
|
||||
* and crypto.getRandomValues exist before @langchain/core / uuid are loaded.
|
||||
*/
|
||||
import 'web-streams-polyfill';
|
||||
|
||||
// Ensure globalThis has ReadableStream (main polyfill may not patch in RN/Metro)
|
||||
const g = typeof globalThis !== 'undefined' ? globalThis : (typeof global !== 'undefined' ? global : (typeof self !== 'undefined' ? self : {}));
|
||||
if (typeof (g as any).ReadableStream === 'undefined') {
|
||||
const ponyfill = require('web-streams-polyfill/dist/ponyfill.js');
|
||||
(g as any).ReadableStream = ponyfill.ReadableStream;
|
||||
(g as any).WritableStream = ponyfill.WritableStream;
|
||||
(g as any).TransformStream = ponyfill.TransformStream;
|
||||
}
|
||||
|
||||
// Polyfill crypto.getRandomValues for React Native/Expo (required by uuid, LangChain, etc.)
|
||||
if (typeof g !== 'undefined') {
|
||||
const cryptoObj = (g as any).crypto;
|
||||
if (!cryptoObj || typeof (cryptoObj.getRandomValues) !== 'function') {
|
||||
try {
|
||||
const ExpoCrypto = require('expo-crypto');
|
||||
const getRandomValues = (array: ArrayBufferView): ArrayBufferView => {
|
||||
ExpoCrypto.getRandomValues(array);
|
||||
return array;
|
||||
};
|
||||
if (!(g as any).crypto) (g as any).crypto = {};
|
||||
(g as any).crypto.getRandomValues = getRandomValues;
|
||||
} catch (e) {
|
||||
console.warn('[polyfills] crypto.getRandomValues polyfill failed:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Polyfill AbortSignal.prototype.throwIfAborted (required by fetch/LangChain in RN; not present in older runtimes)
|
||||
const AbortSignalGlobal = (g as any).AbortSignal;
|
||||
if (typeof AbortSignalGlobal === 'function' && AbortSignalGlobal.prototype && typeof AbortSignalGlobal.prototype.throwIfAborted !== 'function') {
|
||||
AbortSignalGlobal.prototype.throwIfAborted = function (this: AbortSignal) {
|
||||
if (this.aborted) {
|
||||
const e = new Error('Aborted');
|
||||
e.name = 'AbortError';
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -28,9 +28,18 @@ import {
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { Ionicons, Feather, FontAwesome5 } from '@expo/vector-icons';
|
||||
import * as ImagePicker from 'expo-image-picker';
|
||||
import { AIRole } from '../types';
|
||||
import { colors, typography, spacing, borderRadius, shadows } from '../theme/colors';
|
||||
import { aiService } from '../services/ai.service';
|
||||
import { aiService, AIMessage } from '../services/ai.service';
|
||||
import { langGraphService } from '../services/langgraph.service';
|
||||
import { HumanMessage, AIMessage as LangChainAIMessage, SystemMessage } from "@langchain/core/messages";
|
||||
import { assetsService } from '../services/assets.service';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { AI_CONFIG, getVaultStorageKeys } from '../config';
|
||||
import { storageService } from '../services/storage.service';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { SentinelVault } from '../utils/crypto_core';
|
||||
import { Buffer } from 'buffer';
|
||||
|
||||
// =============================================================================
|
||||
// Type Definitions
|
||||
@@ -57,7 +66,7 @@ interface ChatSession {
|
||||
// =============================================================================
|
||||
|
||||
export default function FlowScreen() {
|
||||
const { token, signOut } = useAuth();
|
||||
const { token, user, signOut, aiRoles, refreshAIRoles } = useAuth();
|
||||
const scrollViewRef = useRef<ScrollView>(null);
|
||||
|
||||
// Current conversation state
|
||||
@@ -67,10 +76,27 @@ export default function FlowScreen() {
|
||||
const [isRecording, setIsRecording] = useState(false);
|
||||
const [selectedImage, setSelectedImage] = useState<string | null>(null);
|
||||
|
||||
// AI Role state - start with null to detect first load
|
||||
const [selectedRole, setSelectedRole] = useState<AIRole | null>(aiRoles[0] || null);
|
||||
const [showRoleModal, setShowRoleModal] = useState(false);
|
||||
const [expandedRoleId, setExpandedRoleId] = useState<string | null>(null);
|
||||
|
||||
// History modal state
|
||||
const [showHistoryModal, setShowHistoryModal] = useState(false);
|
||||
const modalSlideAnim = useRef(new Animated.Value(0)).current;
|
||||
|
||||
// Summary state
|
||||
const [showSummaryConfirmModal, setShowSummaryConfirmModal] = useState(false);
|
||||
const [showSummaryResultModal, setShowSummaryResultModal] = useState(false);
|
||||
const [isSummarizing, setIsSummarizing] = useState(false);
|
||||
const [generatedSummary, setGeneratedSummary] = useState('');
|
||||
|
||||
// Save to Vault state
|
||||
const [showVaultConfirmModal, setShowVaultConfirmModal] = useState(false);
|
||||
const [showSaveResultModal, setShowSaveResultModal] = useState(false);
|
||||
const [saveResult, setSaveResult] = useState<{ success: boolean; message: string }>({ success: true, message: '' });
|
||||
const [isSavingToVault, setIsSavingToVault] = useState(false);
|
||||
|
||||
const [chatHistory, setChatHistory] = useState<ChatSession[]>([
|
||||
// Sample history data
|
||||
{
|
||||
@@ -104,14 +130,102 @@ export default function FlowScreen() {
|
||||
day: 'numeric'
|
||||
});
|
||||
|
||||
// Auto-scroll to bottom when new messages arrive
|
||||
// Load history on mount
|
||||
useEffect(() => {
|
||||
const loadHistory = async () => {
|
||||
if (!user) return;
|
||||
try {
|
||||
console.log('[FlowScreen] Loading chat history...');
|
||||
const savedHistory = await storageService.getChatHistory(user.id);
|
||||
if (savedHistory && savedHistory.length > 0) {
|
||||
const formattedHistory = savedHistory.map((session: any) => ({
|
||||
...session,
|
||||
createdAt: new Date(session.createdAt),
|
||||
updatedAt: new Date(session.updatedAt),
|
||||
messages: session.messages.map((msg: any) => ({
|
||||
...msg,
|
||||
createdAt: new Date(msg.createdAt)
|
||||
}))
|
||||
}));
|
||||
setChatHistory(formattedHistory);
|
||||
console.log('[FlowScreen] Chat history loaded:', formattedHistory.length, 'sessions');
|
||||
} else {
|
||||
console.log('[FlowScreen] No chat history found');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load history:', error);
|
||||
}
|
||||
};
|
||||
loadHistory();
|
||||
}, [user]);
|
||||
|
||||
// Load messages whenever role changes
|
||||
useEffect(() => {
|
||||
const loadRoleMessages = async () => {
|
||||
if (!user || !selectedRole) return;
|
||||
try {
|
||||
const savedMessages = await storageService.getCurrentChat(selectedRole?.id || '', user.id);
|
||||
if (savedMessages) {
|
||||
const formattedMessages = savedMessages.map((msg: any) => ({
|
||||
...msg,
|
||||
createdAt: new Date(msg.createdAt)
|
||||
}));
|
||||
setMessages(formattedMessages);
|
||||
} else {
|
||||
setMessages([]);
|
||||
}
|
||||
} catch (error) {
|
||||
if (selectedRole) {
|
||||
console.error(`Failed to load messages for role ${selectedRole?.id}:`, error);
|
||||
}
|
||||
setMessages([]);
|
||||
}
|
||||
};
|
||||
|
||||
loadRoleMessages();
|
||||
}, [selectedRole?.id, user]);
|
||||
|
||||
// Ensure we have a valid selected role from the dynamic list
|
||||
useEffect(() => {
|
||||
if (aiRoles.length > 0) {
|
||||
if (!selectedRole) {
|
||||
// Initial load or first time roles become available
|
||||
setSelectedRole(aiRoles[0]);
|
||||
} else {
|
||||
// If roles refreshed, make sure current selectedRole is still valid or updated
|
||||
const updatedRole = aiRoles.find(r => r.id === selectedRole?.id);
|
||||
if (updatedRole) {
|
||||
setSelectedRole(updatedRole);
|
||||
} else {
|
||||
// Current role no longer exists in dynamic list, fallback to first
|
||||
setSelectedRole(aiRoles[0]);
|
||||
}
|
||||
}
|
||||
} else if (!selectedRole) {
|
||||
// Fallback if no dynamic roles yet
|
||||
setSelectedRole(AI_CONFIG.ROLES[0]);
|
||||
}
|
||||
}, [aiRoles]);
|
||||
|
||||
// Save current messages for the active role when they change
|
||||
useEffect(() => {
|
||||
if (user && selectedRole && messages.length >= 0) { // Save even if empty to allow clearing
|
||||
storageService.saveCurrentChat(selectedRole?.id || '', messages, user.id);
|
||||
}
|
||||
|
||||
if (messages.length > 0) {
|
||||
setTimeout(() => {
|
||||
scrollViewRef.current?.scrollToEnd({ animated: true });
|
||||
}, 100);
|
||||
}
|
||||
}, [messages]);
|
||||
}, [messages, selectedRole?.id, user]);
|
||||
|
||||
// Save history when it changes
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
storageService.saveChatHistory(chatHistory, user.id);
|
||||
}
|
||||
}, [chatHistory, user]);
|
||||
|
||||
// Modal animation control
|
||||
const openHistoryModal = () => {
|
||||
@@ -142,7 +256,7 @@ export default function FlowScreen() {
|
||||
* Handle sending a message to AI
|
||||
*/
|
||||
const handleSendMessage = async () => {
|
||||
if (!newContent.trim() || isSending) return;
|
||||
if (!newContent.trim() || isSending || !selectedRole) return;
|
||||
|
||||
// Check authentication
|
||||
if (!token) {
|
||||
@@ -168,8 +282,23 @@ export default function FlowScreen() {
|
||||
setMessages(prev => [...prev, userMsg]);
|
||||
|
||||
try {
|
||||
// Call AI proxy
|
||||
const aiResponse = await aiService.sendMessage(userMessage, token);
|
||||
// 1. Convert current messages history to LangChain format
|
||||
const history: (HumanMessage | LangChainAIMessage | SystemMessage)[] = messages.map(msg => {
|
||||
if (msg.role === 'user') return new HumanMessage(msg.content);
|
||||
return new LangChainAIMessage(msg.content);
|
||||
});
|
||||
|
||||
// 2. Add system prompt
|
||||
const systemPrompt = new SystemMessage(selectedRole?.systemPrompt || '');
|
||||
|
||||
// 3. Add current new message
|
||||
const currentMsg = new HumanMessage(userMessage);
|
||||
|
||||
// 4. Combine all messages for LangGraph processing
|
||||
const fullMessages = [systemPrompt, ...history, currentMsg];
|
||||
|
||||
// 5. Execute via LangGraph service (handles token limits and context)
|
||||
const aiResponse = await langGraphService.execute(fullMessages, token);
|
||||
|
||||
// Add AI response
|
||||
const aiMsg: ChatMessage = {
|
||||
@@ -335,8 +464,11 @@ export default function FlowScreen() {
|
||||
setChatHistory(prev => [newSession, ...prev]);
|
||||
}
|
||||
|
||||
// Clear current messages
|
||||
// Clear current messages and storage for this role
|
||||
setMessages([]);
|
||||
if (user && selectedRole) {
|
||||
storageService.saveCurrentChat(selectedRole?.id || '', [], user.id);
|
||||
}
|
||||
closeHistoryModal();
|
||||
};
|
||||
|
||||
@@ -379,6 +511,112 @@ export default function FlowScreen() {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle generating summary for current conversation
|
||||
*/
|
||||
const handleGenerateSummary = async () => {
|
||||
if (messages.length === 0) {
|
||||
Alert.alert('No Messages', 'There are no messages to summarize.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
Alert.alert('Login Required', 'Please login to generate a summary.');
|
||||
return;
|
||||
}
|
||||
|
||||
setShowSummaryConfirmModal(false);
|
||||
setIsSummarizing(true);
|
||||
|
||||
try {
|
||||
// Convert messages to AIMessage format
|
||||
const aiMessages: AIMessage[] = messages.map(msg => ({
|
||||
role: msg.role,
|
||||
content: msg.content,
|
||||
}));
|
||||
|
||||
const summary = await aiService.summarizeChat(aiMessages, token);
|
||||
setGeneratedSummary(summary);
|
||||
setShowSummaryResultModal(true);
|
||||
} catch (error) {
|
||||
console.error('Failed to generate summary:', error);
|
||||
Alert.alert('Error', 'Failed to generate summary. Please try again later.');
|
||||
} finally {
|
||||
setIsSummarizing(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle saving the generated summary to the vault
|
||||
*/
|
||||
const handleSaveToVault = async () => {
|
||||
if (!generatedSummary || isSavingToVault) return;
|
||||
|
||||
if (!token) {
|
||||
Alert.alert('Login Required', 'Please login to save to vault.');
|
||||
return;
|
||||
}
|
||||
|
||||
setShowVaultConfirmModal(false);
|
||||
setIsSavingToVault(true);
|
||||
|
||||
try {
|
||||
// Retrieve vault keys
|
||||
if (!user) {
|
||||
Alert.alert('Error', 'User information not found. Please login again.');
|
||||
return;
|
||||
}
|
||||
const vaultKeys = getVaultStorageKeys(user.id);
|
||||
const shareServer = await AsyncStorage.getItem(vaultKeys.SHARE_SERVER);
|
||||
const aesKeyHex = await AsyncStorage.getItem(vaultKeys.AES_KEY);
|
||||
|
||||
if (!shareServer || !aesKeyHex) {
|
||||
Alert.alert(
|
||||
'Vault Not Initialized',
|
||||
'Your vault is not fully initialized. Please set it up in the Vault tab first.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Encrypt summary with AES key
|
||||
const vault = new SentinelVault();
|
||||
const aesKey = Buffer.from(aesKeyHex, 'hex');
|
||||
const encryptedSummary = vault.encryptData(aesKey, generatedSummary).toString('hex');
|
||||
|
||||
// Create asset in backend
|
||||
const createdAsset = await assetsService.createAsset({
|
||||
title: `Chat Summary - ${new Date().toLocaleDateString()}`,
|
||||
private_key_shard: shareServer,
|
||||
content_inner_encrypted: encryptedSummary,
|
||||
}, token);
|
||||
|
||||
// Backup plaintext content locally
|
||||
if (createdAsset && createdAsset.id && user?.id) {
|
||||
await storageService.saveAssetBackup(createdAsset.id, generatedSummary, user.id);
|
||||
}
|
||||
|
||||
setSaveResult({ success: true, message: 'Summary encrypted and saved to your vault successfully.' });
|
||||
setShowSaveResultModal(true);
|
||||
} catch (error) {
|
||||
console.error('Failed to save to vault:', error);
|
||||
setSaveResult({ success: false, message: 'Failed to save summary to vault. Please try again.' });
|
||||
setShowSaveResultModal(true);
|
||||
} finally {
|
||||
setIsSavingToVault(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle closing all summary related modals after successful save or manual close of result
|
||||
*/
|
||||
const handleFinishSaveFlow = () => {
|
||||
setShowSaveResultModal(false);
|
||||
if (saveResult.success) {
|
||||
setShowSummaryResultModal(false);
|
||||
setShowVaultConfirmModal(false);
|
||||
}
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Helper Functions
|
||||
// =============================================================================
|
||||
@@ -451,9 +689,9 @@ export default function FlowScreen() {
|
||||
<View style={styles.emptyIcon}>
|
||||
<Feather name="feather" size={48} color={colors.nautical.seafoam} />
|
||||
</View>
|
||||
<Text style={styles.emptyTitle}>Start a conversation</Text>
|
||||
<Text style={styles.emptyTitle}>Chatting with {selectedRole?.name || 'AI'}</Text>
|
||||
<Text style={styles.emptySubtitle}>
|
||||
Ask me anything or share your thoughts
|
||||
{selectedRole?.description || 'Loading AI Assistant...'}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
@@ -505,6 +743,38 @@ export default function FlowScreen() {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Role Header Dropdown */}
|
||||
<TouchableOpacity
|
||||
style={styles.headerRoleButton}
|
||||
onPress={() => setShowRoleModal(true)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
{selectedRole && (
|
||||
<Ionicons
|
||||
name={(selectedRole?.icon || 'help-outline') as any}
|
||||
size={16}
|
||||
color={colors.nautical.teal}
|
||||
/>
|
||||
)}
|
||||
<Text style={styles.headerRoleText} numberOfLines={1}>
|
||||
{selectedRole?.name || 'Loading...'}
|
||||
</Text>
|
||||
<Ionicons name="chevron-down" size={14} color={colors.flow.textSecondary} />
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Summary Button */}
|
||||
<TouchableOpacity
|
||||
style={[styles.historyButton, { marginRight: spacing.sm }]}
|
||||
onPress={() => setShowSummaryConfirmModal(true)}
|
||||
disabled={messages.length === 0 || isSummarizing}
|
||||
>
|
||||
<Ionicons
|
||||
name="document-text-outline"
|
||||
size={20}
|
||||
color={messages.length === 0 || isSummarizing ? colors.flow.textSecondary : colors.flow.primary}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* History Button */}
|
||||
<TouchableOpacity
|
||||
style={styles.historyButton}
|
||||
@@ -607,7 +877,6 @@ export default function FlowScreen() {
|
||||
</SafeAreaView>
|
||||
</LinearGradient>
|
||||
|
||||
{/* History Modal - Background appears instantly, content slides up */}
|
||||
<Modal
|
||||
visible={showHistoryModal}
|
||||
animationType="none"
|
||||
@@ -670,6 +939,295 @@ export default function FlowScreen() {
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
</Modal>
|
||||
|
||||
{/* Role Selection Modal */}
|
||||
<Modal
|
||||
visible={showRoleModal}
|
||||
animationType="fade"
|
||||
transparent
|
||||
onRequestClose={() => setShowRoleModal(false)}
|
||||
>
|
||||
<TouchableWithoutFeedback onPress={() => setShowRoleModal(false)}>
|
||||
<View style={styles.modalOverlay}>
|
||||
<TouchableWithoutFeedback onPress={(e) => e.stopPropagation()}>
|
||||
<View style={[styles.modalContent, styles.roleModalContent]}>
|
||||
<View style={styles.modalHandle} />
|
||||
<Text style={styles.modalTitle}>Choose AI Assistant</Text>
|
||||
|
||||
<ScrollView style={styles.roleList} showsVerticalScrollIndicator={false}>
|
||||
{aiRoles.map((role) => (
|
||||
<View key={role.id} style={styles.roleItemContainer}>
|
||||
<View
|
||||
style={[
|
||||
styles.roleItem,
|
||||
selectedRole?.id === role.id && styles.roleItemActive
|
||||
]}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={styles.roleSelectionArea}
|
||||
onPress={() => {
|
||||
setSelectedRole(role);
|
||||
setShowRoleModal(false);
|
||||
}}
|
||||
>
|
||||
<View style={[
|
||||
styles.roleItemIcon,
|
||||
selectedRole?.id === role.id && styles.roleItemIconActive
|
||||
]}>
|
||||
<Ionicons
|
||||
name={role.icon as any}
|
||||
size={20}
|
||||
color={selectedRole?.id === role.id ? '#fff' : colors.nautical.teal}
|
||||
/>
|
||||
</View>
|
||||
<Text style={[
|
||||
styles.roleItemName,
|
||||
selectedRole?.id === role.id && styles.roleItemNameActive
|
||||
]}>
|
||||
{role.name}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.infoButton}
|
||||
onPress={() => {
|
||||
setExpandedRoleId(expandedRoleId === role.id ? null : role.id);
|
||||
}}
|
||||
>
|
||||
<Ionicons
|
||||
name={expandedRoleId === role.id ? "close-circle-outline" : "information-circle-outline"}
|
||||
size={24}
|
||||
color={expandedRoleId === role.id ? colors.nautical.coral : colors.flow.textSecondary}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{expandedRoleId === role.id && (
|
||||
<View style={styles.roleDescription}>
|
||||
<Text style={styles.roleDescriptionText}>{role.description}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.closeButton}
|
||||
onPress={() => setShowRoleModal(false)}
|
||||
>
|
||||
<Text style={styles.closeButtonText}>Cancel</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
</Modal>
|
||||
|
||||
{/* Summary Confirmation Modal */}
|
||||
<Modal
|
||||
visible={showSummaryConfirmModal}
|
||||
transparent
|
||||
animationType="fade"
|
||||
onRequestClose={() => setShowSummaryConfirmModal(false)}
|
||||
>
|
||||
<TouchableWithoutFeedback onPress={() => setShowSummaryConfirmModal(false)}>
|
||||
<View style={styles.modalOverlay}>
|
||||
<TouchableWithoutFeedback onPress={e => e.stopPropagation()}>
|
||||
<View style={[styles.modalContent, { paddingBottom: spacing.xl }]}>
|
||||
<View style={styles.modalHandle} />
|
||||
<Text style={styles.modalTitle}>Generate Summary</Text>
|
||||
<Text style={[styles.modalSubtitle, { marginVertical: spacing.base }]}>
|
||||
Would you like to generate a summary for the current conversation?
|
||||
</Text>
|
||||
|
||||
<View style={styles.modalActions}>
|
||||
<TouchableOpacity
|
||||
style={[styles.actionButton, styles.cancelButton]}
|
||||
onPress={() => setShowSummaryConfirmModal(false)}
|
||||
>
|
||||
<Text style={styles.cancelButtonText}>No</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.actionButton, styles.confirmButton]}
|
||||
onPress={handleGenerateSummary}
|
||||
>
|
||||
<LinearGradient
|
||||
colors={[colors.nautical.teal, colors.nautical.seafoam]}
|
||||
style={styles.actionButtonGradient}
|
||||
>
|
||||
<Text style={styles.confirmButtonText}>Yes, Generate</Text>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
</Modal>
|
||||
|
||||
{/* Summary Result Modal */}
|
||||
<Modal
|
||||
visible={showSummaryResultModal}
|
||||
transparent
|
||||
animationType="slide"
|
||||
onRequestClose={() => setShowSummaryResultModal(false)}
|
||||
>
|
||||
<TouchableWithoutFeedback onPress={() => setShowSummaryResultModal(false)}>
|
||||
<View style={styles.modalOverlay}>
|
||||
<TouchableWithoutFeedback onPress={e => e.stopPropagation()}>
|
||||
<View style={[styles.modalContent, { maxHeight: '70%' }]}>
|
||||
<View style={styles.modalHandle} />
|
||||
<View style={styles.modalHeader}>
|
||||
<Text style={styles.modalTitle}>Conversation Summary</Text>
|
||||
<TouchableOpacity onPress={() => setShowSummaryResultModal(false)}>
|
||||
<Ionicons name="close" size={24} color={colors.flow.textSecondary} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<ScrollView style={styles.summaryContainer} showsVerticalScrollIndicator={false}>
|
||||
<View style={styles.summaryCard}>
|
||||
<Text style={styles.summaryText}>{generatedSummary}</Text>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
<View style={styles.summaryActions}>
|
||||
<TouchableOpacity
|
||||
style={[styles.actionButton, styles.saveToVaultButton]}
|
||||
onPress={() => setShowVaultConfirmModal(true)}
|
||||
disabled={isSavingToVault}
|
||||
>
|
||||
<LinearGradient
|
||||
colors={[colors.nautical.teal, colors.nautical.seafoam]}
|
||||
style={styles.actionButtonGradient}
|
||||
>
|
||||
{isSavingToVault ? (
|
||||
<ActivityIndicator size="small" color="#fff" />
|
||||
) : (
|
||||
<>
|
||||
<Ionicons name="shield-checkmark-outline" size={20} color="#fff" />
|
||||
<Text style={styles.confirmButtonText}>Save to Vault</Text>
|
||||
</>
|
||||
)}
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.closeButton}
|
||||
onPress={() => setShowSummaryResultModal(false)}
|
||||
>
|
||||
<Text style={styles.closeButtonText}>Done</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
</Modal>
|
||||
|
||||
{/* Save to Vault Confirmation Modal */}
|
||||
<Modal
|
||||
visible={showVaultConfirmModal}
|
||||
transparent
|
||||
animationType="fade"
|
||||
onRequestClose={() => setShowVaultConfirmModal(false)}
|
||||
>
|
||||
<TouchableWithoutFeedback onPress={() => setShowVaultConfirmModal(false)}>
|
||||
<View style={styles.modalOverlay}>
|
||||
<TouchableWithoutFeedback onPress={e => e.stopPropagation()}>
|
||||
<View style={[styles.modalContent, { paddingBottom: spacing.xl }]}>
|
||||
<View style={styles.modalHandle} />
|
||||
<Text style={styles.modalTitle}>Save to Vault</Text>
|
||||
<Text style={[styles.modalSubtitle, { marginVertical: spacing.base }]}>
|
||||
Would you like to securely save this summary to your digital vault?
|
||||
</Text>
|
||||
|
||||
<View style={styles.modalActions}>
|
||||
<TouchableOpacity
|
||||
style={[styles.actionButton, styles.cancelButton]}
|
||||
onPress={() => setShowVaultConfirmModal(false)}
|
||||
>
|
||||
<Text style={styles.cancelButtonText}>Cancel</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.actionButton, styles.confirmButton]}
|
||||
onPress={handleSaveToVault}
|
||||
>
|
||||
<LinearGradient
|
||||
colors={[colors.nautical.teal, colors.nautical.seafoam]}
|
||||
style={styles.actionButtonGradient}
|
||||
>
|
||||
<Text style={styles.confirmButtonText}>Yes, Save</Text>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
</Modal>
|
||||
|
||||
{/* Save Result Modal */}
|
||||
<Modal
|
||||
visible={showSaveResultModal}
|
||||
transparent
|
||||
animationType="fade"
|
||||
onRequestClose={handleFinishSaveFlow}
|
||||
>
|
||||
<TouchableWithoutFeedback onPress={handleFinishSaveFlow}>
|
||||
<View style={styles.modalOverlay}>
|
||||
<TouchableWithoutFeedback onPress={e => e.stopPropagation()}>
|
||||
<View style={[styles.modalContent, { paddingBottom: spacing.xl, alignItems: 'center' }]}>
|
||||
<View style={styles.modalHandle} />
|
||||
|
||||
<View style={[
|
||||
styles.resultIconContainer,
|
||||
saveResult.success ? styles.successIconBg : styles.errorIconBg
|
||||
]}>
|
||||
<Ionicons
|
||||
name={saveResult.success ? "checkmark-circle" : "alert-circle"}
|
||||
size={64}
|
||||
color={saveResult.success ? colors.nautical.teal : colors.nautical.coral}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Text style={styles.modalTitle}>
|
||||
{saveResult.success ? 'Success!' : 'Oops!'}
|
||||
</Text>
|
||||
|
||||
<Text style={[styles.modalSubtitle, { marginVertical: spacing.base, textAlign: 'center' }]}>
|
||||
{saveResult.message}
|
||||
</Text>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.actionButton, styles.confirmButton, { width: '100%' }]}
|
||||
onPress={handleFinishSaveFlow}
|
||||
>
|
||||
<LinearGradient
|
||||
colors={[colors.nautical.teal, colors.nautical.seafoam]}
|
||||
style={styles.actionButtonGradient}
|
||||
>
|
||||
<Text style={styles.confirmButtonText}>Confirm</Text>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
</Modal>
|
||||
|
||||
{/* Summary Loading Modal */}
|
||||
<Modal
|
||||
visible={isSummarizing}
|
||||
transparent
|
||||
animationType="fade"
|
||||
>
|
||||
<View style={styles.loadingOverlay}>
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color={colors.nautical.teal} />
|
||||
<Text style={styles.loadingText}>Generating Summary...</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -697,12 +1255,33 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: spacing.base,
|
||||
paddingTop: spacing.sm,
|
||||
paddingBottom: spacing.md,
|
||||
paddingBottom: spacing.sm,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: 'rgba(0,0,0,0.05)',
|
||||
},
|
||||
headerLeft: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.sm,
|
||||
flex: 1,
|
||||
},
|
||||
headerRoleButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.flow.cardBackground,
|
||||
paddingHorizontal: spacing.sm,
|
||||
paddingVertical: 6,
|
||||
borderRadius: borderRadius.full,
|
||||
marginHorizontal: spacing.sm,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.flow.cardBorder,
|
||||
maxWidth: '40%',
|
||||
},
|
||||
headerRoleText: {
|
||||
fontSize: typography.fontSize.xs,
|
||||
fontWeight: '600',
|
||||
color: colors.flow.text,
|
||||
marginHorizontal: 4,
|
||||
},
|
||||
iconCircle: {
|
||||
width: 44,
|
||||
@@ -773,6 +1352,96 @@ const styles = StyleSheet.create({
|
||||
textAlign: 'center',
|
||||
},
|
||||
|
||||
// Role selection styles
|
||||
roleDropdown: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.flow.cardBackground,
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.sm,
|
||||
borderRadius: borderRadius.lg,
|
||||
marginBottom: spacing.md,
|
||||
...shadows.soft,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.flow.cardBorder,
|
||||
},
|
||||
roleIcon: {
|
||||
marginRight: spacing.sm,
|
||||
},
|
||||
roleDropdownText: {
|
||||
fontSize: typography.fontSize.base,
|
||||
fontWeight: '600',
|
||||
color: colors.flow.text,
|
||||
marginRight: spacing.xs,
|
||||
},
|
||||
roleModalContent: {
|
||||
paddingBottom: spacing.xl,
|
||||
},
|
||||
roleList: {
|
||||
marginTop: spacing.sm,
|
||||
maxHeight: 400,
|
||||
},
|
||||
roleItemContainer: {
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
roleItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
borderRadius: borderRadius.lg,
|
||||
backgroundColor: 'transparent',
|
||||
borderWidth: 1,
|
||||
borderColor: 'transparent',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
roleItemActive: {
|
||||
backgroundColor: colors.nautical.paleAqua,
|
||||
borderColor: colors.nautical.lightMint,
|
||||
},
|
||||
roleSelectionArea: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: spacing.md,
|
||||
},
|
||||
roleItemIcon: {
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 18,
|
||||
backgroundColor: colors.flow.backgroundGradientStart,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginRight: spacing.md,
|
||||
},
|
||||
roleItemIconActive: {
|
||||
backgroundColor: colors.nautical.teal,
|
||||
},
|
||||
roleItemName: {
|
||||
fontSize: typography.fontSize.base,
|
||||
fontWeight: '500',
|
||||
color: colors.flow.text,
|
||||
},
|
||||
roleItemNameActive: {
|
||||
fontWeight: '700',
|
||||
color: colors.nautical.teal,
|
||||
},
|
||||
infoButton: {
|
||||
padding: spacing.md,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
roleDescription: {
|
||||
paddingHorizontal: spacing.md + 36 + spacing.md, // icon width + margins
|
||||
paddingBottom: spacing.sm,
|
||||
paddingTop: 0,
|
||||
},
|
||||
roleDescriptionText: {
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: colors.flow.textSecondary,
|
||||
fontStyle: 'italic',
|
||||
lineHeight: 18,
|
||||
},
|
||||
|
||||
// Message bubble styles
|
||||
messageBubble: {
|
||||
flexDirection: 'row',
|
||||
@@ -997,4 +1666,101 @@ const styles = StyleSheet.create({
|
||||
color: colors.flow.textSecondary,
|
||||
fontWeight: '600',
|
||||
},
|
||||
|
||||
// Summary Modal styles
|
||||
modalSubtitle: {
|
||||
fontSize: typography.fontSize.base,
|
||||
color: colors.flow.textSecondary,
|
||||
lineHeight: 22,
|
||||
},
|
||||
modalActions: {
|
||||
flexDirection: 'row',
|
||||
gap: spacing.md,
|
||||
marginTop: spacing.base,
|
||||
},
|
||||
actionButton: {
|
||||
flex: 1,
|
||||
height: 50,
|
||||
borderRadius: borderRadius.lg,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
actionButtonGradient: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
cancelButton: {
|
||||
backgroundColor: colors.nautical.paleAqua,
|
||||
},
|
||||
confirmButton: {
|
||||
// Gradient handled in child
|
||||
},
|
||||
cancelButtonText: {
|
||||
fontSize: typography.fontSize.base,
|
||||
fontWeight: '600',
|
||||
color: colors.flow.textSecondary,
|
||||
},
|
||||
confirmButtonText: {
|
||||
fontSize: typography.fontSize.base,
|
||||
fontWeight: '600',
|
||||
color: '#fff',
|
||||
},
|
||||
summaryContainer: {
|
||||
marginVertical: spacing.md,
|
||||
},
|
||||
summaryCard: {
|
||||
backgroundColor: colors.nautical.paleAqua + '40', // 25% opacity
|
||||
padding: spacing.md,
|
||||
borderRadius: borderRadius.lg,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.nautical.lightMint,
|
||||
},
|
||||
summaryText: {
|
||||
fontSize: typography.fontSize.base,
|
||||
color: colors.flow.text,
|
||||
lineHeight: 24,
|
||||
},
|
||||
summaryActions: {
|
||||
marginTop: spacing.md,
|
||||
gap: spacing.sm,
|
||||
},
|
||||
saveToVaultButton: {
|
||||
height: 54,
|
||||
},
|
||||
resultIconContainer: {
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: 40,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
successIconBg: {
|
||||
backgroundColor: colors.nautical.paleAqua,
|
||||
},
|
||||
errorIconBg: {
|
||||
backgroundColor: 'rgba(231, 76, 60, 0.1)', // coral at 10%
|
||||
},
|
||||
loadingOverlay: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(26, 58, 74, 0.6)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
loadingContainer: {
|
||||
backgroundColor: colors.flow.cardBackground,
|
||||
padding: spacing.xl,
|
||||
borderRadius: borderRadius.xl,
|
||||
alignItems: 'center',
|
||||
...shadows.soft,
|
||||
gap: spacing.md,
|
||||
},
|
||||
loadingText: {
|
||||
fontSize: typography.fontSize.base,
|
||||
color: colors.flow.text,
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -14,9 +14,11 @@ import {
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { Ionicons, Feather, MaterialCommunityIcons, FontAwesome5 } from '@expo/vector-icons';
|
||||
import { colors, typography, spacing, borderRadius, shadows } from '../theme/colors';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { Heir, HeirStatus, PaymentStrategy } from '../types';
|
||||
import HeritageScreen from './HeritageScreen';
|
||||
import { getVaultStorageKeys } from '../config';
|
||||
|
||||
// Mock heirs data
|
||||
const initialHeirs: Heir[] = [
|
||||
@@ -220,6 +222,7 @@ export default function MeScreen() {
|
||||
const [showHeritageModal, setShowHeritageModal] = useState(false);
|
||||
const [showThemeModal, setShowThemeModal] = useState(false);
|
||||
const [isDarkMode, setIsDarkMode] = useState(false);
|
||||
const [showSignOutModal, setShowSignOutModal] = useState(false);
|
||||
|
||||
// Heritage / Fleet Legacy states
|
||||
const [heirs, setHeirs] = useState<Heir[]>(initialHeirs);
|
||||
@@ -245,6 +248,7 @@ export default function MeScreen() {
|
||||
});
|
||||
const [sanctumArchive, setSanctumArchive] = useState<'off' | 'standard' | 'strict'>('standard');
|
||||
const [sanctumRehearsal, setSanctumRehearsal] = useState<'monthly' | 'quarterly'>('quarterly');
|
||||
const [resetVaultFeedback, setResetVaultFeedback] = useState<{ status: 'idle' | 'success' | 'error'; message: string }>({ status: 'idle', message: '' });
|
||||
const [triggerDisconnectDays, setTriggerDisconnectDays] = useState(30);
|
||||
const [triggerGraceDays, setTriggerGraceDays] = useState(15);
|
||||
const [triggerSource, setTriggerSource] = useState<'dual' | 'subscription' | 'activity'>('dual');
|
||||
@@ -294,18 +298,40 @@ export default function MeScreen() {
|
||||
};
|
||||
|
||||
const handleAbandonIsland = () => {
|
||||
Alert.alert(
|
||||
'Sign Out',
|
||||
'Are you sure you want to sign out?',
|
||||
[
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
{
|
||||
text: 'Sign Out',
|
||||
style: 'destructive',
|
||||
onPress: signOut
|
||||
},
|
||||
]
|
||||
);
|
||||
console.log('[MeScreen] Sign out button clicked');
|
||||
setShowSignOutModal(true);
|
||||
};
|
||||
|
||||
const handleConfirmSignOut = () => {
|
||||
console.log('[MeScreen] User confirmed sign out');
|
||||
setShowSignOutModal(false);
|
||||
signOut();
|
||||
};
|
||||
|
||||
const handleResetVault = async () => {
|
||||
setResetVaultFeedback({ status: 'idle', message: '' });
|
||||
const vaultKeys = getVaultStorageKeys(user?.id ?? null);
|
||||
try {
|
||||
await AsyncStorage.multiRemove([
|
||||
vaultKeys.INITIALIZED,
|
||||
vaultKeys.SHARE_DEVICE,
|
||||
vaultKeys.MNEMONIC_PART_LOCAL,
|
||||
]);
|
||||
setResetVaultFeedback({
|
||||
status: 'success',
|
||||
message: 'Vault state has been reset. Next time you open Shadow Vault you will see the mnemonic flow again.',
|
||||
});
|
||||
} catch (e) {
|
||||
setResetVaultFeedback({
|
||||
status: 'error',
|
||||
message: 'Failed to reset vault state. Please try again.',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloseSanctumModal = () => {
|
||||
setResetVaultFeedback({ status: 'idle', message: '' });
|
||||
setShowSanctumModal(false);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -749,7 +775,7 @@ export default function MeScreen() {
|
||||
visible={showSanctumModal}
|
||||
animationType="fade"
|
||||
transparent
|
||||
onRequestClose={() => setShowSanctumModal(false)}
|
||||
onRequestClose={handleCloseSanctumModal}
|
||||
>
|
||||
<View style={styles.spiritOverlay}>
|
||||
<View style={styles.spiritModal}>
|
||||
@@ -885,12 +911,51 @@ export default function MeScreen() {
|
||||
<Text style={styles.sanctumValue}>View</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{__DEV__ && (
|
||||
<View style={styles.sanctumSection}>
|
||||
<Text style={styles.tideLabel}>DEV ONLY</Text>
|
||||
<TouchableOpacity
|
||||
style={styles.devResetButton}
|
||||
onPress={handleResetVault}
|
||||
activeOpacity={0.85}
|
||||
>
|
||||
<Ionicons name="refresh" size={16} color={colors.nautical.coral} />
|
||||
<Text style={styles.devResetText}>Reset Vault State</Text>
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.sanctumHint}>Clear S0 (SHARE_DEVICE) from storage. Next vault open uses mnemonic flow.</Text>
|
||||
{resetVaultFeedback.status !== 'idle' && (
|
||||
<View
|
||||
style={[
|
||||
styles.resetVaultFeedback,
|
||||
resetVaultFeedback.status === 'success' ? styles.resetVaultFeedbackSuccess : styles.resetVaultFeedbackError,
|
||||
]}
|
||||
>
|
||||
<Ionicons
|
||||
name={resetVaultFeedback.status === 'success' ? 'checkmark-circle' : 'alert-circle'}
|
||||
size={20}
|
||||
color={resetVaultFeedback.status === 'success' ? colors.sentinel?.statusNormal ?? '#6BBF8A' : colors.nautical.coral}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
styles.resetVaultFeedbackText,
|
||||
resetVaultFeedback.status === 'success' ? styles.resetVaultFeedbackTextSuccess : styles.resetVaultFeedbackTextError,
|
||||
]}
|
||||
>
|
||||
{resetVaultFeedback.status === 'success' ? 'Success' : 'Error'}
|
||||
{' — '}
|
||||
{resetVaultFeedback.message}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
<View style={styles.tideModalButtons}>
|
||||
<TouchableOpacity
|
||||
style={styles.confirmPulseButton}
|
||||
activeOpacity={0.85}
|
||||
onPress={() => setShowSanctumModal(false)}
|
||||
onPress={handleCloseSanctumModal}
|
||||
>
|
||||
<Ionicons name="checkmark-circle" size={18} color={colors.nautical.teal} />
|
||||
<Text style={styles.confirmPulseText}>Save</Text>
|
||||
@@ -898,7 +963,7 @@ export default function MeScreen() {
|
||||
<TouchableOpacity
|
||||
style={styles.confirmPulseButton}
|
||||
activeOpacity={0.85}
|
||||
onPress={() => setShowSanctumModal(false)}
|
||||
onPress={handleCloseSanctumModal}
|
||||
>
|
||||
<Ionicons name="close-circle" size={18} color={colors.nautical.teal} />
|
||||
<Text style={styles.confirmPulseText}>Close</Text>
|
||||
@@ -1412,6 +1477,46 @@ export default function MeScreen() {
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
|
||||
{/* Sign Out Confirmation Modal */}
|
||||
<Modal
|
||||
visible={showSignOutModal}
|
||||
animationType="fade"
|
||||
transparent
|
||||
onRequestClose={() => setShowSignOutModal(false)}
|
||||
>
|
||||
<View style={styles.spiritOverlay}>
|
||||
<View style={styles.signOutModal}>
|
||||
<View style={styles.signOutHeader}>
|
||||
<View style={styles.signOutIcon}>
|
||||
<Feather name="log-out" size={32} color={colors.nautical.coral} />
|
||||
</View>
|
||||
<Text style={styles.signOutTitle}>Sign Out</Text>
|
||||
<Text style={styles.signOutMessage}>
|
||||
Are you sure you want to sign out? You'll need to log in again to access your account.
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.signOutButtons}>
|
||||
<TouchableOpacity
|
||||
style={styles.signOutCancelButton}
|
||||
onPress={() => setShowSignOutModal(false)}
|
||||
activeOpacity={0.85}
|
||||
>
|
||||
<Text style={styles.signOutCancelText}>Cancel</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.signOutConfirmButton}
|
||||
onPress={handleConfirmSignOut}
|
||||
activeOpacity={0.85}
|
||||
>
|
||||
<Text style={styles.signOutConfirmText}>Sign Out</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -1814,6 +1919,23 @@ const styles = StyleSheet.create({
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: colors.me.textSecondary,
|
||||
},
|
||||
devResetButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.sm,
|
||||
paddingVertical: spacing.sm,
|
||||
paddingHorizontal: spacing.md,
|
||||
backgroundColor: `${colors.nautical.coral}15`,
|
||||
borderRadius: borderRadius.lg,
|
||||
borderWidth: 1,
|
||||
borderColor: `${colors.nautical.coral}40`,
|
||||
alignSelf: 'flex-start',
|
||||
},
|
||||
devResetText: {
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: colors.nautical.coral,
|
||||
fontWeight: '600',
|
||||
},
|
||||
sanctumAlert: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
@@ -1827,6 +1949,34 @@ const styles = StyleSheet.create({
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: colors.nautical.coral,
|
||||
},
|
||||
resetVaultFeedback: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.sm,
|
||||
borderRadius: borderRadius.lg,
|
||||
padding: spacing.base,
|
||||
marginTop: spacing.md,
|
||||
},
|
||||
resetVaultFeedbackSuccess: {
|
||||
backgroundColor: 'rgba(107, 191, 138, 0.2)',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(107, 191, 138, 0.5)',
|
||||
},
|
||||
resetVaultFeedbackError: {
|
||||
backgroundColor: 'rgba(229, 115, 115, 0.2)',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(229, 115, 115, 0.5)',
|
||||
},
|
||||
resetVaultFeedbackText: {
|
||||
flex: 1,
|
||||
fontSize: typography.fontSize.sm,
|
||||
},
|
||||
resetVaultFeedbackTextSuccess: {
|
||||
color: '#2E7D5E',
|
||||
},
|
||||
resetVaultFeedbackTextError: {
|
||||
color: colors.nautical.coral,
|
||||
},
|
||||
confirmPulseButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
@@ -2333,4 +2483,71 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'center',
|
||||
zIndex: 10,
|
||||
},
|
||||
// Sign Out Modal Styles
|
||||
signOutModal: {
|
||||
backgroundColor: colors.me.cardBackground,
|
||||
borderRadius: borderRadius.xl,
|
||||
padding: spacing.xl,
|
||||
marginHorizontal: spacing.xl,
|
||||
maxWidth: 400,
|
||||
width: '100%',
|
||||
...shadows.medium,
|
||||
},
|
||||
signOutHeader: {
|
||||
alignItems: 'center',
|
||||
marginBottom: spacing.xl,
|
||||
},
|
||||
signOutIcon: {
|
||||
width: 72,
|
||||
height: 72,
|
||||
borderRadius: 36,
|
||||
backgroundColor: `${colors.nautical.coral}15`,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: spacing.base,
|
||||
},
|
||||
signOutTitle: {
|
||||
fontSize: typography.fontSize.xl,
|
||||
fontWeight: '700',
|
||||
color: colors.me.text,
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
signOutMessage: {
|
||||
fontSize: typography.fontSize.base,
|
||||
color: colors.me.textSecondary,
|
||||
textAlign: 'center',
|
||||
lineHeight: typography.fontSize.base * 1.5,
|
||||
},
|
||||
signOutButtons: {
|
||||
flexDirection: 'row',
|
||||
gap: spacing.md,
|
||||
},
|
||||
signOutCancelButton: {
|
||||
flex: 1,
|
||||
paddingVertical: spacing.base,
|
||||
paddingHorizontal: spacing.lg,
|
||||
borderRadius: borderRadius.lg,
|
||||
backgroundColor: colors.me.cardBorder,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
signOutCancelText: {
|
||||
fontSize: typography.fontSize.base,
|
||||
fontWeight: '600',
|
||||
color: colors.me.text,
|
||||
},
|
||||
signOutConfirmButton: {
|
||||
flex: 1,
|
||||
paddingVertical: spacing.base,
|
||||
paddingHorizontal: spacing.lg,
|
||||
borderRadius: borderRadius.lg,
|
||||
backgroundColor: colors.nautical.coral,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
signOutConfirmText: {
|
||||
fontSize: typography.fontSize.base,
|
||||
fontWeight: '600',
|
||||
color: '#fff',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
@@ -8,40 +8,12 @@ import {
|
||||
SafeAreaView,
|
||||
Animated,
|
||||
Modal,
|
||||
TextInput,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
Share,
|
||||
Alert,
|
||||
Linking,
|
||||
} from 'react-native';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { Ionicons, Feather, MaterialCommunityIcons, FontAwesome5 } from '@expo/vector-icons';
|
||||
import { captureRef } from 'react-native-view-shot';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { colors, typography, spacing, borderRadius, shadows } from '../theme/colors';
|
||||
import { SystemStatus, KillSwitchLog } from '../types';
|
||||
import VaultScreen from './VaultScreen';
|
||||
import {
|
||||
SSSShare,
|
||||
mnemonicToEntropy,
|
||||
splitSecret,
|
||||
formatShareCompact,
|
||||
serializeShare,
|
||||
verifyShares,
|
||||
} from '../utils/sss';
|
||||
|
||||
// Nautical-themed mnemonic word list (unique words only)
|
||||
const MNEMONIC_WORDS = [
|
||||
'anchor', 'harbor', 'compass', 'lighthouse', 'current', 'ocean', 'tide', 'voyage',
|
||||
'keel', 'stern', 'bow', 'mast', 'sail', 'port', 'starboard', 'reef',
|
||||
'signal', 'beacon', 'chart', 'helm', 'gale', 'calm', 'cove', 'isle',
|
||||
'horizon', 'sextant', 'sound', 'drift', 'wake', 'mariner', 'pilot', 'fathom',
|
||||
'buoy', 'lantern', 'harpoon', 'lagoon', 'bay', 'strait', 'riptide', 'foam',
|
||||
'coral', 'pearl', 'trident', 'ebb', 'flow', 'vault', 'cipher', 'shroud',
|
||||
'salt', 'wave', 'grotto', 'storm', 'north', 'south', 'east', 'west',
|
||||
'ember', 'cabin', 'ledger', 'torch', 'sanctum', 'oath', 'depths', 'captain',
|
||||
] as const;
|
||||
|
||||
// Animation timing constants
|
||||
const ANIMATION_DURATION = {
|
||||
@@ -51,49 +23,6 @@ const ANIMATION_DURATION = {
|
||||
heartbeatPress: 150,
|
||||
} as const;
|
||||
|
||||
const generateMnemonic = (wordCount = 12) => {
|
||||
const words: string[] = [];
|
||||
for (let i = 0; i < wordCount; i += 1) {
|
||||
const index = Math.floor(Math.random() * MNEMONIC_WORDS.length);
|
||||
words.push(MNEMONIC_WORDS[index]);
|
||||
}
|
||||
return words;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate SSS shares from mnemonic words
|
||||
* Uses Shamir's Secret Sharing (3,2) threshold scheme
|
||||
*/
|
||||
const generateSSSShares = (words: string[]): SSSShare[] => {
|
||||
try {
|
||||
// Convert mnemonic to entropy (big integer)
|
||||
const entropy = mnemonicToEntropy(words, MNEMONIC_WORDS);
|
||||
|
||||
// Split entropy into 3 shares using SSS
|
||||
const shares = splitSecret(entropy);
|
||||
|
||||
// Verify shares can recover the original (optional, for debugging)
|
||||
if (__DEV__) {
|
||||
const isValid = verifyShares(shares, entropy);
|
||||
if (!isValid) {
|
||||
console.warn('SSS verification failed!');
|
||||
} else {
|
||||
console.log('SSS shares verified successfully');
|
||||
}
|
||||
}
|
||||
|
||||
return shares;
|
||||
} catch (error) {
|
||||
console.error('Failed to generate SSS shares:', error);
|
||||
// Fallback: return empty shares (should not happen in production)
|
||||
return [
|
||||
{ x: 1, y: BigInt(0), label: 'device' },
|
||||
{ x: 2, y: BigInt(0), label: 'cloud' },
|
||||
{ x: 3, y: BigInt(0), label: 'heir' },
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
// Icon names type for type safety
|
||||
type StatusIconName = 'checkmark-circle' | 'warning' | 'alert-circle';
|
||||
|
||||
@@ -130,28 +59,14 @@ const statusConfig: Record<SystemStatus, {
|
||||
|
||||
// Mock data
|
||||
const initialLogs: KillSwitchLog[] = [
|
||||
{
|
||||
id: '1',
|
||||
action: 'HEARTBEAT_CONFIRMED',
|
||||
timestamp: new Date('2024-01-18T09:30:00'),
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
action: 'SUBSCRIPTION_VERIFIED',
|
||||
timestamp: new Date('2024-01-17T00:00:00'),
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
action: 'JOURNAL_ACTIVITY',
|
||||
timestamp: new Date('2024-01-16T15:42:00'),
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
action: 'HEARTBEAT_CONFIRMED',
|
||||
timestamp: new Date('2024-01-15T11:20:00'),
|
||||
},
|
||||
{ id: '1', action: 'HEARTBEAT_CONFIRMED', timestamp: new Date('2024-01-18T09:30:00') },
|
||||
{ id: '2', action: 'SUBSCRIPTION_VERIFIED', timestamp: new Date('2024-01-17T00:00:00') },
|
||||
{ id: '3', action: 'JOURNAL_ACTIVITY', timestamp: new Date('2024-01-16T15:42:00') },
|
||||
{ id: '4', action: 'HEARTBEAT_CONFIRMED', timestamp: new Date('2024-01-15T11:20:00') },
|
||||
];
|
||||
|
||||
export { VAULT_STORAGE_KEYS } from '../config';
|
||||
|
||||
export default function SentinelScreen() {
|
||||
const [status, setStatus] = useState<SystemStatus>('normal');
|
||||
const [lastSubscriptionCheck] = useState(new Date('2024-01-18T00:00:00'));
|
||||
@@ -161,16 +76,8 @@ export default function SentinelScreen() {
|
||||
const [glowAnim] = useState(new Animated.Value(0.5));
|
||||
const [rotateAnim] = useState(new Animated.Value(0));
|
||||
const [showVault, setShowVault] = useState(false);
|
||||
const [showMnemonic, setShowMnemonic] = useState(false);
|
||||
const [mnemonicWords, setMnemonicWords] = useState<string[]>([]);
|
||||
const [sssShares, setSssShares] = useState<SSSShare[]>([]);
|
||||
const [showEmailForm, setShowEmailForm] = useState(false);
|
||||
const [emailAddress, setEmailAddress] = useState('');
|
||||
const [isCapturing, setIsCapturing] = useState(false);
|
||||
const mnemonicRef = useRef<View>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Pulse animation
|
||||
const pulseAnimation = Animated.loop(
|
||||
Animated.sequence([
|
||||
Animated.timing(pulseAnim, {
|
||||
@@ -187,7 +94,6 @@ export default function SentinelScreen() {
|
||||
);
|
||||
pulseAnimation.start();
|
||||
|
||||
// Glow animation
|
||||
const glowAnimation = Animated.loop(
|
||||
Animated.sequence([
|
||||
Animated.timing(glowAnim, {
|
||||
@@ -204,7 +110,6 @@ export default function SentinelScreen() {
|
||||
);
|
||||
glowAnimation.start();
|
||||
|
||||
// Slow rotate for ship wheel
|
||||
const rotateAnimation = Animated.loop(
|
||||
Animated.timing(rotateAnim, {
|
||||
toValue: 1,
|
||||
@@ -214,7 +119,6 @@ export default function SentinelScreen() {
|
||||
);
|
||||
rotateAnimation.start();
|
||||
|
||||
// Cleanup animations on unmount to prevent memory leaks
|
||||
return () => {
|
||||
pulseAnimation.stop();
|
||||
glowAnimation.stop();
|
||||
@@ -222,73 +126,9 @@ export default function SentinelScreen() {
|
||||
};
|
||||
}, [pulseAnim, glowAnim, rotateAnim]);
|
||||
|
||||
const openVaultWithMnemonic = () => {
|
||||
const words = generateMnemonic();
|
||||
const shares = generateSSSShares(words);
|
||||
setMnemonicWords(words);
|
||||
setSssShares(shares);
|
||||
setShowMnemonic(true);
|
||||
setShowVault(false);
|
||||
setShowEmailForm(false);
|
||||
setEmailAddress('');
|
||||
|
||||
// Store Share A (device share) locally
|
||||
if (shares[0]) {
|
||||
AsyncStorage.setItem('sentinel_share_device', serializeShare(shares[0])).catch(() => {
|
||||
// Best-effort local store; UI remains available
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleScreenshot = async () => {
|
||||
try {
|
||||
setIsCapturing(true);
|
||||
const uri = await captureRef(mnemonicRef, {
|
||||
format: 'png',
|
||||
quality: 1,
|
||||
result: 'tmpfile',
|
||||
});
|
||||
await Share.share({
|
||||
url: uri,
|
||||
message: 'Sentinel key backup',
|
||||
});
|
||||
setShowMnemonic(false);
|
||||
setShowVault(true);
|
||||
} catch (error) {
|
||||
Alert.alert('Screenshot failed', 'Please try again or use email backup.');
|
||||
} finally {
|
||||
setIsCapturing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEmailBackup = () => {
|
||||
setShowEmailForm(true);
|
||||
};
|
||||
|
||||
const handleSendEmail = async () => {
|
||||
const trimmed = emailAddress.trim();
|
||||
if (!trimmed || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)) {
|
||||
Alert.alert('Invalid email', 'Please enter a valid email address.');
|
||||
return;
|
||||
}
|
||||
|
||||
const subject = encodeURIComponent('Sentinel Vault Recovery Key');
|
||||
const body = encodeURIComponent(`Your 12-word mnemonic:\n${mnemonicWords.join(' ')}`);
|
||||
const mailtoUrl = `mailto:${trimmed}?subject=${subject}&body=${body}`;
|
||||
|
||||
try {
|
||||
await Linking.openURL(mailtoUrl);
|
||||
setShowMnemonic(false);
|
||||
setShowEmailForm(false);
|
||||
setEmailAddress('');
|
||||
setShowVault(true);
|
||||
} catch (error) {
|
||||
Alert.alert('Email failed', 'Unable to open email client.');
|
||||
}
|
||||
};
|
||||
const openVault = () => setShowVault(true);
|
||||
|
||||
const handleHeartbeat = () => {
|
||||
// Animate pulse
|
||||
Animated.sequence([
|
||||
Animated.timing(pulseAnim, {
|
||||
toValue: 1.15,
|
||||
@@ -302,7 +142,6 @@ export default function SentinelScreen() {
|
||||
}),
|
||||
]).start();
|
||||
|
||||
// Add new log using functional update to avoid stale closure
|
||||
const newLog: KillSwitchLog = {
|
||||
id: Date.now().toString(),
|
||||
action: 'HEARTBEAT_CONFIRMED',
|
||||
@@ -310,35 +149,27 @@ export default function SentinelScreen() {
|
||||
};
|
||||
setLogs((prevLogs) => [newLog, ...prevLogs]);
|
||||
|
||||
// Reset status if warning
|
||||
if (status === 'warning') {
|
||||
setStatus('normal');
|
||||
}
|
||||
};
|
||||
|
||||
const formatDateTime = (date: Date) => {
|
||||
return date.toLocaleString('en-US', {
|
||||
const formatDateTime = (date: Date) =>
|
||||
date.toLocaleString('en-US', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
};
|
||||
|
||||
const formatTimeAgo = (date: Date) => {
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - date.getTime();
|
||||
const hours = Math.floor(diff / (1000 * 60 * 60));
|
||||
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
|
||||
|
||||
if (hours > 24) {
|
||||
const days = Math.floor(hours / 24);
|
||||
return `${days} days ago`;
|
||||
}
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${minutes}m ago`;
|
||||
}
|
||||
if (hours > 24) return `${Math.floor(hours / 24)} days ago`;
|
||||
if (hours > 0) return `${hours}h ${minutes}m ago`;
|
||||
return `${minutes}m ago`;
|
||||
};
|
||||
|
||||
@@ -378,7 +209,7 @@ export default function SentinelScreen() {
|
||||
transform: [{ scale: pulseAnim }],
|
||||
opacity: glowAnim,
|
||||
backgroundColor: `${currentStatus.color}20`,
|
||||
}
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Animated.View style={{ transform: [{ scale: pulseAnim }] }}>
|
||||
@@ -416,24 +247,16 @@ export default function SentinelScreen() {
|
||||
<FontAwesome5 name="anchor" size={16} color={colors.sentinel.primary} />
|
||||
</View>
|
||||
<Text style={styles.metricLabel}>SUBSCRIPTION</Text>
|
||||
<Text style={styles.metricValue}>
|
||||
{formatTimeAgo(lastSubscriptionCheck)}
|
||||
</Text>
|
||||
<Text style={styles.metricTime}>
|
||||
{formatDateTime(lastSubscriptionCheck)}
|
||||
</Text>
|
||||
<Text style={styles.metricValue}>{formatTimeAgo(lastSubscriptionCheck)}</Text>
|
||||
<Text style={styles.metricTime}>{formatDateTime(lastSubscriptionCheck)}</Text>
|
||||
</View>
|
||||
<View style={styles.metricCard}>
|
||||
<View style={styles.metricIconContainer}>
|
||||
<Feather name="edit-3" size={16} color={colors.sentinel.primary} />
|
||||
</View>
|
||||
<Text style={styles.metricLabel}>LAST JOURNAL</Text>
|
||||
<Text style={styles.metricValue}>
|
||||
{formatTimeAgo(lastFlowActivity)}
|
||||
</Text>
|
||||
<Text style={styles.metricTime}>
|
||||
{formatDateTime(lastFlowActivity)}
|
||||
</Text>
|
||||
<Text style={styles.metricValue}>{formatTimeAgo(lastFlowActivity)}</Text>
|
||||
<Text style={styles.metricTime}>{formatDateTime(lastFlowActivity)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -450,7 +273,7 @@ export default function SentinelScreen() {
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={styles.vaultAccessButton}
|
||||
onPress={openVaultWithMnemonic}
|
||||
onPress={openVault}
|
||||
activeOpacity={0.8}
|
||||
accessibilityLabel="Open Shadow Vault"
|
||||
accessibilityRole="button"
|
||||
@@ -494,9 +317,7 @@ export default function SentinelScreen() {
|
||||
<View style={styles.logDot} />
|
||||
<View style={styles.logContent}>
|
||||
<Text style={styles.logAction}>{log.action}</Text>
|
||||
<Text style={styles.logTime}>
|
||||
{formatDateTime(log.timestamp)}
|
||||
</Text>
|
||||
<Text style={styles.logTime}>{formatDateTime(log.timestamp)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
@@ -512,7 +333,7 @@ export default function SentinelScreen() {
|
||||
onRequestClose={() => setShowVault(false)}
|
||||
>
|
||||
<View style={styles.vaultModalContainer}>
|
||||
<VaultScreen />
|
||||
{showVault ? <VaultScreen /> : null}
|
||||
<TouchableOpacity
|
||||
style={styles.vaultCloseButton}
|
||||
onPress={() => setShowVault(false)}
|
||||
@@ -524,140 +345,20 @@ export default function SentinelScreen() {
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</Modal>
|
||||
|
||||
{/* Mnemonic Modal */}
|
||||
<Modal
|
||||
visible={showMnemonic}
|
||||
animationType="fade"
|
||||
transparent
|
||||
onRequestClose={() => setShowMnemonic(false)}
|
||||
>
|
||||
<KeyboardAvoidingView
|
||||
style={styles.mnemonicOverlay}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||
>
|
||||
<View ref={mnemonicRef} collapsable={false}>
|
||||
<LinearGradient
|
||||
colors={[colors.sentinel.cardBackground, colors.sentinel.backgroundGradientEnd]}
|
||||
style={styles.mnemonicCard}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={styles.mnemonicClose}
|
||||
onPress={() => setShowMnemonic(false)}
|
||||
activeOpacity={0.85}
|
||||
accessibilityLabel="Close mnemonic modal"
|
||||
accessibilityRole="button"
|
||||
>
|
||||
<Ionicons name="close" size={18} color={colors.sentinel.textSecondary} />
|
||||
</TouchableOpacity>
|
||||
<View style={styles.mnemonicHeader}>
|
||||
<MaterialCommunityIcons name="key-variant" size={22} color={colors.sentinel.primary} />
|
||||
<Text style={styles.mnemonicTitle}>12-Word Mnemonic</Text>
|
||||
</View>
|
||||
<Text style={styles.mnemonicSubtitle}>
|
||||
Your seed is protected by SSS (3,2) threshold encryption. Any 2 shares can restore your vault.
|
||||
</Text>
|
||||
<View style={styles.mnemonicBlock}>
|
||||
<Text style={styles.mnemonicBlockText}>
|
||||
{mnemonicWords.join(' ')}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.partGrid}>
|
||||
<View style={[styles.partCard, styles.partCardStored]}>
|
||||
<Text style={styles.partLabel}>SHARE A • DEVICE</Text>
|
||||
<Text style={styles.partValue}>
|
||||
{sssShares[0] ? formatShareCompact(sssShares[0]) : '---'}
|
||||
</Text>
|
||||
<Text style={styles.partHint}>Stored on this device</Text>
|
||||
</View>
|
||||
<View style={styles.partCard}>
|
||||
<Text style={styles.partLabel}>SHARE B • CLOUD</Text>
|
||||
<Text style={styles.partValue}>
|
||||
{sssShares[1] ? formatShareCompact(sssShares[1]) : '---'}
|
||||
</Text>
|
||||
<Text style={styles.partHint}>To be synced to Sentinel</Text>
|
||||
</View>
|
||||
<View style={styles.partCard}>
|
||||
<Text style={styles.partLabel}>SHARE C • HEIR</Text>
|
||||
<Text style={styles.partValue}>
|
||||
{sssShares[2] ? formatShareCompact(sssShares[2]) : '---'}
|
||||
</Text>
|
||||
<Text style={styles.partHint}>For your heir (2-of-3 required)</Text>
|
||||
</View>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={[styles.mnemonicPrimaryButton, isCapturing && styles.mnemonicButtonDisabled]}
|
||||
onPress={handleScreenshot}
|
||||
activeOpacity={0.85}
|
||||
disabled={isCapturing}
|
||||
accessibilityLabel="Take screenshot backup of mnemonic"
|
||||
accessibilityRole="button"
|
||||
accessibilityState={{ disabled: isCapturing }}
|
||||
>
|
||||
<Text style={styles.mnemonicPrimaryText}>
|
||||
{isCapturing ? 'CAPTURING...' : 'PHYSICAL BACKUP (SCREENSHOT)'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={styles.mnemonicSecondaryButton}
|
||||
onPress={handleEmailBackup}
|
||||
activeOpacity={0.85}
|
||||
accessibilityLabel="Send mnemonic backup via email"
|
||||
accessibilityRole="button"
|
||||
>
|
||||
<Text style={styles.mnemonicSecondaryText}>EMAIL BACKUP</Text>
|
||||
</TouchableOpacity>
|
||||
{showEmailForm ? (
|
||||
<View style={styles.emailForm}>
|
||||
<TextInput
|
||||
style={styles.emailInput}
|
||||
value={emailAddress}
|
||||
onChangeText={setEmailAddress}
|
||||
placeholder="you@email.com"
|
||||
placeholderTextColor={colors.sentinel.textSecondary}
|
||||
keyboardType="email-address"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
style={styles.emailSendButton}
|
||||
onPress={handleSendEmail}
|
||||
activeOpacity={0.85}
|
||||
accessibilityLabel="Send backup email"
|
||||
accessibilityRole="button"
|
||||
>
|
||||
<Text style={styles.emailSendText}>SEND EMAIL</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
) : null}
|
||||
</LinearGradient>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
</Modal>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
gradient: {
|
||||
flex: 1,
|
||||
},
|
||||
safeArea: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
},
|
||||
container: { flex: 1 },
|
||||
gradient: { flex: 1 },
|
||||
safeArea: { flex: 1 },
|
||||
scrollView: { flex: 1 },
|
||||
scrollContent: {
|
||||
padding: spacing.lg,
|
||||
paddingBottom: 120,
|
||||
},
|
||||
header: {
|
||||
marginBottom: spacing.xl,
|
||||
},
|
||||
header: { marginBottom: spacing.xl },
|
||||
headerTitleRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
@@ -763,9 +464,7 @@ const styles = StyleSheet.create({
|
||||
marginBottom: spacing.xl,
|
||||
...shadows.medium,
|
||||
},
|
||||
heartbeatGradient: {
|
||||
padding: spacing.lg,
|
||||
},
|
||||
heartbeatGradient: { padding: spacing.lg },
|
||||
heartbeatContent: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
@@ -819,9 +518,7 @@ const styles = StyleSheet.create({
|
||||
marginTop: 6,
|
||||
marginRight: spacing.md,
|
||||
},
|
||||
logContent: {
|
||||
flex: 1,
|
||||
},
|
||||
logContent: { flex: 1 },
|
||||
logAction: {
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: colors.sentinel.text,
|
||||
@@ -834,7 +531,6 @@ const styles = StyleSheet.create({
|
||||
color: colors.sentinel.textSecondary,
|
||||
fontFamily: typography.fontFamily.mono,
|
||||
},
|
||||
// Shadow Vault Access Card
|
||||
vaultAccessCard: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
@@ -854,9 +550,7 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'center',
|
||||
marginRight: spacing.md,
|
||||
},
|
||||
vaultAccessContent: {
|
||||
flex: 1,
|
||||
},
|
||||
vaultAccessContent: { flex: 1 },
|
||||
vaultAccessTitle: {
|
||||
fontSize: typography.fontSize.base,
|
||||
fontWeight: '600',
|
||||
@@ -878,7 +572,6 @@ const styles = StyleSheet.create({
|
||||
fontWeight: '700',
|
||||
fontSize: typography.fontSize.sm,
|
||||
},
|
||||
// Vault Modal
|
||||
vaultModalContainer: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.vault.background,
|
||||
@@ -894,148 +587,4 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
mnemonicOverlay: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(11, 20, 24, 0.72)',
|
||||
justifyContent: 'center',
|
||||
padding: spacing.lg,
|
||||
},
|
||||
mnemonicCard: {
|
||||
borderRadius: borderRadius.xl,
|
||||
padding: spacing.lg,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.sentinel.cardBorder,
|
||||
...shadows.glow,
|
||||
},
|
||||
mnemonicHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.sm,
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
mnemonicClose: {
|
||||
position: 'absolute',
|
||||
top: spacing.sm,
|
||||
right: spacing.sm,
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: 'rgba(26, 58, 74, 0.35)',
|
||||
},
|
||||
mnemonicTitle: {
|
||||
fontSize: typography.fontSize.lg,
|
||||
fontWeight: '700',
|
||||
color: colors.sentinel.text,
|
||||
letterSpacing: typography.letterSpacing.wide,
|
||||
},
|
||||
mnemonicSubtitle: {
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: colors.sentinel.textSecondary,
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
mnemonicBlock: {
|
||||
backgroundColor: colors.sentinel.cardBackground,
|
||||
borderRadius: borderRadius.lg,
|
||||
paddingVertical: spacing.md,
|
||||
paddingHorizontal: spacing.md,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.sentinel.cardBorder,
|
||||
marginBottom: spacing.lg,
|
||||
},
|
||||
partGrid: {
|
||||
gap: spacing.sm,
|
||||
marginBottom: spacing.lg,
|
||||
},
|
||||
partCard: {
|
||||
backgroundColor: colors.sentinel.cardBackground,
|
||||
borderRadius: borderRadius.lg,
|
||||
paddingVertical: spacing.sm,
|
||||
paddingHorizontal: spacing.md,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.sentinel.cardBorder,
|
||||
},
|
||||
partCardStored: {
|
||||
borderColor: colors.sentinel.primary,
|
||||
},
|
||||
partLabel: {
|
||||
fontSize: typography.fontSize.xs,
|
||||
color: colors.sentinel.textSecondary,
|
||||
letterSpacing: typography.letterSpacing.wide,
|
||||
marginBottom: 4,
|
||||
fontWeight: '600',
|
||||
},
|
||||
partValue: {
|
||||
fontSize: typography.fontSize.md,
|
||||
color: colors.sentinel.text,
|
||||
fontFamily: typography.fontFamily.mono,
|
||||
fontWeight: '700',
|
||||
marginBottom: 2,
|
||||
},
|
||||
partHint: {
|
||||
fontSize: typography.fontSize.xs,
|
||||
color: colors.sentinel.textSecondary,
|
||||
},
|
||||
mnemonicBlockText: {
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: colors.sentinel.text,
|
||||
fontFamily: typography.fontFamily.mono,
|
||||
fontWeight: '600',
|
||||
lineHeight: 22,
|
||||
textAlign: 'center',
|
||||
},
|
||||
mnemonicPrimaryButton: {
|
||||
backgroundColor: colors.sentinel.primary,
|
||||
paddingVertical: spacing.sm,
|
||||
borderRadius: borderRadius.full,
|
||||
alignItems: 'center',
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
mnemonicButtonDisabled: {
|
||||
opacity: 0.6,
|
||||
},
|
||||
mnemonicPrimaryText: {
|
||||
color: colors.nautical.cream,
|
||||
fontWeight: '700',
|
||||
letterSpacing: typography.letterSpacing.wide,
|
||||
},
|
||||
mnemonicSecondaryButton: {
|
||||
backgroundColor: 'transparent',
|
||||
paddingVertical: spacing.sm,
|
||||
borderRadius: borderRadius.full,
|
||||
alignItems: 'center',
|
||||
borderWidth: 1,
|
||||
borderColor: colors.sentinel.cardBorder,
|
||||
},
|
||||
mnemonicSecondaryText: {
|
||||
color: colors.sentinel.text,
|
||||
fontWeight: '700',
|
||||
letterSpacing: typography.letterSpacing.wide,
|
||||
},
|
||||
emailForm: {
|
||||
marginTop: spacing.sm,
|
||||
},
|
||||
emailInput: {
|
||||
height: 44,
|
||||
borderRadius: borderRadius.full,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.sentinel.cardBorder,
|
||||
paddingHorizontal: spacing.md,
|
||||
color: colors.sentinel.text,
|
||||
fontSize: typography.fontSize.sm,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.02)',
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
emailSendButton: {
|
||||
backgroundColor: colors.nautical.teal,
|
||||
paddingVertical: spacing.sm,
|
||||
borderRadius: borderRadius.full,
|
||||
alignItems: 'center',
|
||||
},
|
||||
emailSendText: {
|
||||
color: colors.nautical.cream,
|
||||
fontWeight: '700',
|
||||
letterSpacing: typography.letterSpacing.wide,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
getApiHeaders,
|
||||
logApiDebug,
|
||||
} from '../config';
|
||||
import { AIRole } from '../types';
|
||||
import { trimInternalMessages } from '../utils/token_utils';
|
||||
|
||||
// =============================================================================
|
||||
// Type Definitions
|
||||
@@ -143,13 +145,14 @@ export const aiService = {
|
||||
* Simple helper for single message chat
|
||||
* @param content - User message content
|
||||
* @param token - JWT token for authentication
|
||||
* @param systemPrompt - Optional custom system prompt
|
||||
* @returns AI response text
|
||||
*/
|
||||
async sendMessage(content: string, token?: string): Promise<string> {
|
||||
async sendMessage(content: string, token?: string, systemPrompt?: string): Promise<string> {
|
||||
const messages: AIMessage[] = [
|
||||
{
|
||||
role: 'system',
|
||||
content: AI_CONFIG.DEFAULT_SYSTEM_PROMPT,
|
||||
content: systemPrompt || AI_CONFIG.DEFAULT_SYSTEM_PROMPT,
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
@@ -240,4 +243,86 @@ export const aiService = {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Summarize a chat conversation
|
||||
* @param messages - Array of chat messages
|
||||
* @param token - JWT token for authentication
|
||||
* @returns AI summary text
|
||||
*/
|
||||
async summarizeChat(messages: AIMessage[], token?: string): Promise<string> {
|
||||
if (NO_BACKEND_MODE) {
|
||||
logApiDebug('AI Summary', 'Using mock mode');
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve('This is a mock summary of your conversation. You discussed various topics including AI integration and UI design. The main conclusion was to proceed with the proposed implementation plan.');
|
||||
}, AI_CONFIG.MOCK_RESPONSE_DELAY);
|
||||
});
|
||||
}
|
||||
|
||||
// Enforce token limit (10,000 tokens)
|
||||
const trimmedMessages = trimInternalMessages(messages);
|
||||
|
||||
const historicalMessages = trimmedMessages.map(msg => ({
|
||||
role: msg.role,
|
||||
content: msg.content,
|
||||
}));
|
||||
|
||||
const summaryPrompt: AIMessage = {
|
||||
role: 'user',
|
||||
content: 'Please provide a concise summary of the conversation above in English. Focus on the main topics discussed and any key conclusions or actions mentioned.',
|
||||
};
|
||||
|
||||
const response = await this.chat([...historicalMessages, summaryPrompt], token);
|
||||
return response.choices[0]?.message?.content || 'No summary generated';
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch available AI roles from backend
|
||||
* @param token - Optional JWT token for authentication
|
||||
* @returns Array of AI roles
|
||||
*/
|
||||
async getAIRoles(token?: string): Promise<AIRole[]> {
|
||||
if (NO_BACKEND_MODE) {
|
||||
logApiDebug('AI Roles', 'Using mock roles');
|
||||
return [...AI_CONFIG.ROLES];
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
console.warn('[AI Service] getAIRoles called without token, falling back to static roles');
|
||||
return [...AI_CONFIG.ROLES];
|
||||
}
|
||||
|
||||
const url = buildApiUrl(API_ENDPOINTS.AI.GET_ROLES);
|
||||
const headers = getApiHeaders(token);
|
||||
|
||||
logApiDebug('AI Roles Request', {
|
||||
url,
|
||||
hasToken: !!token,
|
||||
headers: {
|
||||
...headers,
|
||||
Authorization: headers.Authorization ? `${headers.Authorization.substring(0, 15)}...` : 'MISSING'
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(`[AI Service] Failed to fetch AI roles: ${response.status}. Falling back to static roles.`);
|
||||
return [...AI_CONFIG.ROLES];
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
logApiDebug('AI Roles Success', { count: data.length });
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('[AI Service] Fetch AI roles error:', error);
|
||||
// Fallback to config roles if API fails for better UX
|
||||
return [...AI_CONFIG.ROLES];
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -23,6 +23,7 @@ export interface Asset {
|
||||
author_id: number;
|
||||
private_key_shard: string;
|
||||
content_outer_encrypted: string;
|
||||
heir_email?: string;
|
||||
}
|
||||
|
||||
export interface AssetCreate {
|
||||
@@ -45,7 +46,7 @@ export interface AssetClaimResponse {
|
||||
|
||||
export interface AssetAssign {
|
||||
asset_id: number;
|
||||
heir_name: string;
|
||||
heir_email: string;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -59,6 +60,7 @@ const MOCK_ASSETS: Asset[] = [
|
||||
author_id: MOCK_CONFIG.USER.id,
|
||||
private_key_shard: 'mock_shard_1',
|
||||
content_outer_encrypted: 'mock_encrypted_content_1',
|
||||
heir_email: 'heir@example.com',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
@@ -142,11 +144,16 @@ export const assetsService = {
|
||||
body: JSON.stringify(asset),
|
||||
});
|
||||
|
||||
logApiDebug('Create Asset Response Status', response.status);
|
||||
const responseStatus = response.status;
|
||||
logApiDebug('Create Asset Response Status', responseStatus);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || 'Failed to create asset');
|
||||
const detail = errorData.detail || 'Failed to create asset';
|
||||
const message = responseStatus === 401 ? `Unauthorized (401): ${detail}` : detail;
|
||||
const err = new Error(message) as Error & { status?: number };
|
||||
err.status = responseStatus;
|
||||
throw err;
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
@@ -212,7 +219,7 @@ export const assetsService = {
|
||||
logApiDebug('Assign Asset', 'Using mock mode');
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({ message: `Asset assigned to ${assignment.heir_name}` });
|
||||
resolve({ message: `Asset assigned to ${assignment.heir_email}` });
|
||||
}, MOCK_CONFIG.RESPONSE_DELAY);
|
||||
});
|
||||
}
|
||||
@@ -240,4 +247,44 @@ export const assetsService = {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete an asset
|
||||
* @param assetId - ID of the asset to delete
|
||||
* @param token - JWT token for authentication
|
||||
* @returns Success message
|
||||
*/
|
||||
async deleteAsset(assetId: number, token: string): Promise<{ message: string }> {
|
||||
if (NO_BACKEND_MODE) {
|
||||
logApiDebug('Delete Asset', `Using mock mode for ID: ${assetId}`);
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({ message: 'Asset deleted successfully' });
|
||||
}, MOCK_CONFIG.RESPONSE_DELAY);
|
||||
});
|
||||
}
|
||||
|
||||
const url = buildApiUrl(API_ENDPOINTS.ASSETS.DELETE);
|
||||
logApiDebug('Delete Asset URL', url);
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: getApiHeaders(token),
|
||||
body: JSON.stringify({ asset_id: assetId }),
|
||||
});
|
||||
|
||||
logApiDebug('Delete Asset Response Status', response.status);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || 'Failed to delete asset');
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Delete asset error:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -23,3 +23,9 @@ export {
|
||||
type DeclareGualeRequest,
|
||||
type DeclareGualeResponse
|
||||
} from './admin.service';
|
||||
export {
|
||||
createVaultPayload,
|
||||
createAssetPayload,
|
||||
type CreateVaultPayloadResult,
|
||||
type CreateAssetPayloadResult,
|
||||
} from './vault.service';
|
||||
|
||||
96
src/services/langgraph.service.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* LangGraph Service
|
||||
*
|
||||
* Implements AI chat logic using LangGraph.js for state management
|
||||
* and context handling.
|
||||
*/
|
||||
|
||||
import { StateGraph, START, END, Annotation } from "@langchain/langgraph";
|
||||
import { BaseMessage, HumanMessage, AIMessage, SystemMessage } from "@langchain/core/messages";
|
||||
import { aiService } from "./ai.service";
|
||||
import { trimLangChainMessages } from "../utils/token_utils";
|
||||
|
||||
// =============================================================================
|
||||
// Settings
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Define the State using Annotation (Standard for latest LangGraph.js)
|
||||
*/
|
||||
const GraphAnnotation = Annotation.Root({
|
||||
messages: Annotation<BaseMessage[]>({
|
||||
reducer: (x, y) => x.concat(y),
|
||||
default: () => [],
|
||||
}),
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Graph Definition
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* The main node that calls our existing AI API
|
||||
*/
|
||||
async function callModel(state: typeof GraphAnnotation.State, config: any) {
|
||||
const { messages } = state;
|
||||
const { token } = config.configurable || {};
|
||||
|
||||
// 1. Trim messages to stay under token limit
|
||||
const trimmedMessages = trimLangChainMessages(messages);
|
||||
|
||||
// 2. Convert LangChain messages to our internal AIMessage format for the API
|
||||
const apiMessages = trimmedMessages.map(m => {
|
||||
let role: 'system' | 'user' | 'assistant' = 'user';
|
||||
const type = (m as any)._getType?.() || (m instanceof SystemMessage ? 'system' : m instanceof HumanMessage ? 'human' : m instanceof AIMessage ? 'ai' : 'user');
|
||||
|
||||
if (type === 'system') role = 'system';
|
||||
else if (type === 'human') role = 'user';
|
||||
else if (type === 'ai') role = 'assistant';
|
||||
|
||||
return {
|
||||
role,
|
||||
content: m.content.toString()
|
||||
};
|
||||
});
|
||||
|
||||
// 3. Call the proxy service
|
||||
const response = await aiService.chat(apiMessages, token);
|
||||
const content = response.choices[0]?.message?.content || "No response generated";
|
||||
|
||||
// 4. Return the new message to satisfy the Graph (it will be appended due to reducer)
|
||||
return {
|
||||
messages: [new AIMessage(content)]
|
||||
};
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Service Export
|
||||
// =============================================================================
|
||||
|
||||
export const langGraphService = {
|
||||
/**
|
||||
* Run the chat graph with history
|
||||
*/
|
||||
async execute(
|
||||
currentMessages: BaseMessage[],
|
||||
userToken: string,
|
||||
): Promise<string> {
|
||||
// Define the graph
|
||||
const workflow = new StateGraph(GraphAnnotation)
|
||||
.addNode("agent", callModel)
|
||||
.addEdge(START, "agent")
|
||||
.addEdge("agent", END);
|
||||
|
||||
const app = workflow.compile();
|
||||
|
||||
// Execute the graph
|
||||
const result = await app.invoke(
|
||||
{ messages: currentMessages },
|
||||
{ configurable: { token: userToken } }
|
||||
);
|
||||
|
||||
// Return the content of the last message (the AI response)
|
||||
const lastMsg = result.messages[result.messages.length - 1];
|
||||
return lastMsg.content.toString();
|
||||
}
|
||||
};
|
||||
147
src/services/storage.service.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* Storage Service
|
||||
*
|
||||
* Handles local persistence of chat history and active conversations
|
||||
* using AsyncStorage with user-specific isolation.
|
||||
*/
|
||||
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
|
||||
// =============================================================================
|
||||
// Constants
|
||||
// =============================================================================
|
||||
|
||||
const STORAGE_KEYS = {
|
||||
CHAT_HISTORY: '@sentinel:chat_history',
|
||||
CURRENT_MESSAGES: '@sentinel:current_messages',
|
||||
ASSET_BACKUP: '@sentinel:asset_backup',
|
||||
} as const;
|
||||
|
||||
// =============================================================================
|
||||
// Service Implementation
|
||||
// =============================================================================
|
||||
|
||||
export const storageService = {
|
||||
/**
|
||||
* Get user-specific storage key
|
||||
*/
|
||||
getUserKey(baseKey: string, userId: string | number): string {
|
||||
return `${baseKey}:user_${userId}`;
|
||||
},
|
||||
|
||||
/**
|
||||
* Save the complete list of chat sessions to local storage for a specific user
|
||||
*/
|
||||
async saveChatHistory(history: any[], userId: string | number): Promise<void> {
|
||||
try {
|
||||
const jsonValue = JSON.stringify(history);
|
||||
const key = this.getUserKey(STORAGE_KEYS.CHAT_HISTORY, userId);
|
||||
await AsyncStorage.setItem(key, jsonValue);
|
||||
console.log(`[Storage] Saved chat history for user ${userId}:`, history.length, 'sessions');
|
||||
} catch (e) {
|
||||
console.error('Error saving chat history:', e);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Load the list of chat sessions from local storage for a specific user
|
||||
*/
|
||||
async getChatHistory(userId: string | number): Promise<any[]> {
|
||||
try {
|
||||
const key = this.getUserKey(STORAGE_KEYS.CHAT_HISTORY, userId);
|
||||
const jsonValue = await AsyncStorage.getItem(key);
|
||||
const result = jsonValue != null ? JSON.parse(jsonValue) : [];
|
||||
console.log(`[Storage] Loaded chat history for user ${userId}:`, result.length, 'sessions');
|
||||
return result;
|
||||
} catch (e) {
|
||||
console.error('Error getting chat history:', e);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Save the current active conversation messages for a specific role and user
|
||||
*/
|
||||
async saveCurrentChat(roleId: string, messages: any[], userId: string | number): Promise<void> {
|
||||
try {
|
||||
const jsonValue = JSON.stringify(messages);
|
||||
const key = `${this.getUserKey(STORAGE_KEYS.CURRENT_MESSAGES, userId)}:${roleId}`;
|
||||
await AsyncStorage.setItem(key, jsonValue);
|
||||
console.log(`[Storage] Saved current chat for user ${userId}, role ${roleId}:`, messages.length, 'messages');
|
||||
} catch (e) {
|
||||
console.error(`Error saving current chat for role ${roleId}:`, e);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Load the current active conversation messages for a specific role and user
|
||||
*/
|
||||
async getCurrentChat(roleId: string, userId: string | number): Promise<any[]> {
|
||||
try {
|
||||
const key = `${this.getUserKey(STORAGE_KEYS.CURRENT_MESSAGES, userId)}:${roleId}`;
|
||||
const jsonValue = await AsyncStorage.getItem(key);
|
||||
const result = jsonValue != null ? JSON.parse(jsonValue) : [];
|
||||
console.log(`[Storage] Loaded current chat for user ${userId}, role ${roleId}:`, result.length, 'messages');
|
||||
return result;
|
||||
} catch (e) {
|
||||
console.error(`Error getting current chat for role ${roleId}:`, e);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear all stored chat data for a specific user
|
||||
*/
|
||||
async clearUserData(userId: string | number): Promise<void> {
|
||||
try {
|
||||
const keys = await AsyncStorage.getAllKeys();
|
||||
const userPrefix = `:user_${userId}`;
|
||||
const userKeys = keys.filter(key => key.includes(userPrefix));
|
||||
await AsyncStorage.multiRemove(userKeys);
|
||||
console.log(`[Storage] Cleared all data for user ${userId}:`, userKeys.length, 'keys removed');
|
||||
} catch (e) {
|
||||
console.error('Error clearing user data:', e);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear all stored chat data (all users)
|
||||
*/
|
||||
async clearAllData(): Promise<void> {
|
||||
try {
|
||||
const keys = await AsyncStorage.getAllKeys();
|
||||
const sentinelKeys = keys.filter(key => key.startsWith('@sentinel:'));
|
||||
await AsyncStorage.multiRemove(sentinelKeys);
|
||||
console.log('[Storage] Cleared all data:', sentinelKeys.length, 'keys removed');
|
||||
} catch (e) {
|
||||
console.error('Error clearing storage data:', e);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Save the plaintext backup of an asset locally
|
||||
*/
|
||||
async saveAssetBackup(assetId: number, content: string, userId: string | number): Promise<void> {
|
||||
try {
|
||||
const key = `${this.getUserKey(STORAGE_KEYS.ASSET_BACKUP, userId)}:${assetId}`;
|
||||
await AsyncStorage.setItem(key, content);
|
||||
console.log(`[Storage] Saved asset backup for user ${userId}, asset ${assetId}`);
|
||||
} catch (e) {
|
||||
console.error(`Error saving asset backup for asset ${assetId}:`, e);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve the plaintext backup of an asset locally
|
||||
*/
|
||||
async getAssetBackup(assetId: number, userId: string | number): Promise<string | null> {
|
||||
try {
|
||||
const key = `${this.getUserKey(STORAGE_KEYS.ASSET_BACKUP, userId)}:${assetId}`;
|
||||
return await AsyncStorage.getItem(key);
|
||||
} catch (e) {
|
||||
console.error(`Error getting asset backup for asset ${assetId}:`, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
81
src/services/vault.service.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Vault Service: 为 /assets/create 生成 private_key_shard 与 content_inner_encrypted
|
||||
*
|
||||
* 流程(与后端 test_scenario / SentinelVault 一致):
|
||||
* 1. 用 SSS 生成助记词并分片 → 选一个分片作为 private_key_shard(存后端,继承时返回)
|
||||
* 2. 用助记词派生 AES 密钥,对明文做 AES-GCM 加密 → content_inner_encrypted(hex 字符串)
|
||||
*
|
||||
* 使用方式:在任意页面调用 createVaultPayload(plaintext, wordList),得到可直接传给 assetsService.createAsset 的字段。
|
||||
*/
|
||||
|
||||
import {
|
||||
generateVaultKeys,
|
||||
serializeShare,
|
||||
type SSSShare,
|
||||
type VaultKeyData,
|
||||
} from '../utils/sss';
|
||||
import { deriveKey, encryptDataGCM, bytesToHex } from '../utils/vaultCrypto';
|
||||
|
||||
export interface CreateVaultPayloadResult {
|
||||
/** 传给后端的 private_key_shard(存一个 SSS 分片的序列化字符串,如云端分片) */
|
||||
private_key_shard: string;
|
||||
/** 传给后端的 content_inner_encrypted(AES-GCM 密文的 hex) */
|
||||
content_inner_encrypted: string;
|
||||
/** 本次生成的助记词(用户需妥善保管,恢复时需任意 2 个分片) */
|
||||
mnemonic: string[];
|
||||
/** 三个分片:device / cloud / heir,可与后端返回的 server_shard 组合恢复助记词 */
|
||||
shares: SSSShare[];
|
||||
}
|
||||
|
||||
export interface CreateAssetPayloadResult {
|
||||
title: string;
|
||||
type: string;
|
||||
private_key_shard: string;
|
||||
content_inner_encrypted: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成金库:助记词 + SSS 分片 + 内层加密内容
|
||||
* @param plaintext 要加密的明文(如遗产说明、账号密码等)
|
||||
* @param wordList 助记词词表(与 sss 使用的词表一致)
|
||||
* @param shareIndexForServer 哪个分片存后端,0=device, 1=cloud, 2=heir,默认 1(云端)
|
||||
*/
|
||||
export async function createVaultPayload(
|
||||
plaintext: string,
|
||||
wordList: readonly string[],
|
||||
shareIndexForServer: 0 | 1 | 2 = 1
|
||||
): Promise<CreateVaultPayloadResult> {
|
||||
const { mnemonic, shares }: VaultKeyData = generateVaultKeys(wordList, 12);
|
||||
const mnemonicPhrase = mnemonic.join(' ');
|
||||
const key = await deriveKey(mnemonicPhrase);
|
||||
const encrypted = await encryptDataGCM(key, plaintext);
|
||||
const content_inner_encrypted = bytesToHex(encrypted);
|
||||
const shareForServer = shares[shareIndexForServer];
|
||||
const private_key_shard = serializeShare(shareForServer);
|
||||
|
||||
return {
|
||||
private_key_shard,
|
||||
content_inner_encrypted,
|
||||
mnemonic,
|
||||
shares,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成可直接用于 POST /assets/create 的请求体(含 title / type)
|
||||
*/
|
||||
export async function createAssetPayload(
|
||||
title: string,
|
||||
plaintext: string,
|
||||
wordList: readonly string[],
|
||||
assetType: string = 'note',
|
||||
shareIndexForServer: 0 | 1 | 2 = 1
|
||||
): Promise<CreateAssetPayloadResult> {
|
||||
const vault = await createVaultPayload(plaintext, wordList, shareIndexForServer);
|
||||
return {
|
||||
title,
|
||||
type: assetType,
|
||||
private_key_shard: vault.private_key_shard,
|
||||
content_inner_encrypted: vault.content_inner_encrypted,
|
||||
};
|
||||
}
|
||||
@@ -28,6 +28,8 @@ export interface VaultAsset {
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
isEncrypted: boolean;
|
||||
heirEmail?: string;
|
||||
rawData?: any; // For debug logging
|
||||
}
|
||||
|
||||
// Sentinel Types
|
||||
@@ -77,6 +79,7 @@ export interface ProtocolInfo {
|
||||
export interface User {
|
||||
id: number;
|
||||
username: string;
|
||||
email?: string;
|
||||
public_key: string;
|
||||
is_admin: boolean;
|
||||
guale: boolean;
|
||||
@@ -101,3 +104,13 @@ export interface LoginResponse {
|
||||
token_type: string;
|
||||
user: User;
|
||||
}
|
||||
|
||||
// AI Types
|
||||
export interface AIRole {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
systemPrompt: string;
|
||||
icon: string;
|
||||
iconFamily: string;
|
||||
}
|
||||
|
||||
22
src/utils/async_hooks_mock.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Mock for Node.js async_hooks
|
||||
* Used to fix LangGraph.js compatibility with React Native
|
||||
*/
|
||||
|
||||
export class AsyncLocalStorage {
|
||||
disable() { }
|
||||
getStore() {
|
||||
return undefined;
|
||||
}
|
||||
run(store: any, callback: (...args: any[]) => any, ...args: any[]) {
|
||||
return callback(...args);
|
||||
}
|
||||
exit(callback: (...args: any[]) => any, ...args: any[]) {
|
||||
return callback(...args);
|
||||
}
|
||||
enterWith(store: any) { }
|
||||
}
|
||||
|
||||
export default {
|
||||
AsyncLocalStorage,
|
||||
};
|
||||
202
src/utils/crypto_core.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
import * as bip39 from 'bip39';
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
// 定义分片类型:[x坐标, y坐标]
|
||||
export type Share = [bigint, bigint];
|
||||
|
||||
// 定义生成密钥的返回接口
|
||||
export interface VaultKeys {
|
||||
mnemonic: string;
|
||||
entropyHex: string;
|
||||
}
|
||||
|
||||
export class SentinelKeyEngine {
|
||||
// 使用第 13 个梅森素数 (2^521 - 1)
|
||||
// readonly 确保不会被修改
|
||||
private readonly PRIME: bigint = 2n ** 521n - 1n;
|
||||
|
||||
/**
|
||||
* 1. 生成原始 12 助记词 (Master Key)
|
||||
*/
|
||||
public generateVaultKeys(): VaultKeys {
|
||||
// 生成 128 位强度的助记词 (12 个单词)
|
||||
const mnemonic = bip39.generateMnemonic(128);
|
||||
|
||||
// 将助记词转为 16 进制熵 (Hex String)
|
||||
const entropyHex = bip39.mnemonicToEntropy(mnemonic);
|
||||
|
||||
return { mnemonic, entropyHex };
|
||||
}
|
||||
|
||||
public mnemonicToEntropy(mnemonic: string): string {
|
||||
return bip39.mnemonicToEntropy(mnemonic);
|
||||
}
|
||||
|
||||
/**
|
||||
* 2. SSS (3,2) 门限分片逻辑
|
||||
* @param entropyHex - 16进制字符串 (32字符)
|
||||
*/
|
||||
public splitToShares(entropyHex: string): Share[] {
|
||||
// 将 Hex 熵转换为 BigInt
|
||||
const secretInt = BigInt('0x' + entropyHex);
|
||||
|
||||
// 生成随机系数 a,范围 [0, PRIME-1]
|
||||
const a = this.secureRandomBigInt(this.PRIME);
|
||||
|
||||
// 定义函数 f(x) = (S + a * x) % PRIME
|
||||
const f = (x: number): bigint => {
|
||||
const xBi = BigInt(x);
|
||||
return (secretInt + a * xBi) % this.PRIME;
|
||||
};
|
||||
|
||||
// 生成 3 个分片: x=1, x=2, x=3
|
||||
const share1: Share = [1n, f(1)]; // 手机分片
|
||||
const share2: Share = [2n, f(2)]; // 云端分片
|
||||
const share3: Share = [3n, f(3)]; // 传承卡分片
|
||||
|
||||
return [share1, share2, share3];
|
||||
}
|
||||
|
||||
/**
|
||||
* 3. 恢复逻辑:拉格朗日插值还原
|
||||
* @param shareA - 第一个分片
|
||||
* @param shareB - 第二个分片
|
||||
*/
|
||||
public recoverFromShares(shareA: Share, shareB: Share): string {
|
||||
const [x1, y1] = shareA;
|
||||
const [x2, y2] = shareB;
|
||||
|
||||
// 计算分子: (x2 * y1 - x1 * y2) % PRIME
|
||||
// TS/JS 的 % 运算符对负数返回负数,需修正为正余数
|
||||
let numerator = (x2 * y1 - x1 * y2) % this.PRIME;
|
||||
if (numerator < 0n) numerator += this.PRIME;
|
||||
|
||||
// 计算分母: (x2 - x1)
|
||||
let denominator = (x2 - x1) % this.PRIME;
|
||||
if (denominator < 0n) denominator += this.PRIME;
|
||||
|
||||
// 计算分母的模逆: denominator^-1 mod PRIME
|
||||
// 费马小定理: a^(p-2) = a^-1 (mod p)
|
||||
const invDenominator = this.modPow(denominator, this.PRIME - 2n, this.PRIME);
|
||||
|
||||
// 还原常数项 S
|
||||
const secretInt = (numerator * invDenominator) % this.PRIME;
|
||||
|
||||
// 转回 Hex 字符串
|
||||
let recoveredEntropyHex = secretInt.toString(16);
|
||||
|
||||
// 补齐前导零 (Pad Start)
|
||||
// 128 bit 熵 = 16 字节 = 32 个 Hex 字符
|
||||
// 如果你的熵是 256 bit,这里需要改为 64
|
||||
recoveredEntropyHex = recoveredEntropyHex.padStart(32, '0');
|
||||
|
||||
return bip39.entropyToMnemonic(recoveredEntropyHex);
|
||||
}
|
||||
|
||||
// --- Private Helper Methods ---
|
||||
|
||||
/**
|
||||
* 生成小于 limit 的安全随机 BigInt
|
||||
*/
|
||||
private secureRandomBigInt(limit: bigint): bigint {
|
||||
// 计算需要的字节数
|
||||
const bitLength = limit.toString(2).length;
|
||||
const byteLength = Math.ceil(bitLength / 8);
|
||||
|
||||
let randomBi: bigint;
|
||||
do {
|
||||
const buf = crypto.randomBytes(byteLength);
|
||||
randomBi = BigInt('0x' + buf.toString('hex'));
|
||||
// 拒绝采样:确保结果小于 limit
|
||||
} while (randomBi >= limit);
|
||||
|
||||
return randomBi;
|
||||
}
|
||||
|
||||
/**
|
||||
* 模幂运算: (base^exp) % modulus
|
||||
* 用于计算模逆
|
||||
*/
|
||||
private modPow(base: bigint, exp: bigint, modulus: bigint): bigint {
|
||||
let result = 1n;
|
||||
base = base % modulus;
|
||||
while (exp > 0n) {
|
||||
if (exp % 2n === 1n) result = (result * base) % modulus;
|
||||
exp = exp >> 1n; // 相当于除以 2
|
||||
base = (base * base) % modulus;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export class SentinelVault {
|
||||
private salt: Buffer;
|
||||
|
||||
constructor(salt?: string | Buffer) {
|
||||
// 默认盐值与 Python 版本保持一致
|
||||
this.salt = salt ? Buffer.from(salt) : Buffer.from('Sentinel_Salt_2026');
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 PBKDF2 将助记词转换为 AES-256 密钥 (32 bytes)
|
||||
*/
|
||||
public async deriveKey(mnemonicPhrase: string): Promise<Buffer> {
|
||||
// 1. BIP-39 助记词转种子 (遵循 BIP-39 标准)
|
||||
// Python 的 to_seed 默认返回 64 字节种子
|
||||
const seed = await bip39.mnemonicToSeed(mnemonicPhrase);
|
||||
|
||||
// 2. PBKDF2 派生密钥
|
||||
// 注意:PyCryptodome 的 PBKDF2 默认使用 HMAC-SHA1 (如未指定)
|
||||
// 为了确保与 Python 逻辑严格一致,这里使用 'sha1'
|
||||
return new Promise((resolve, reject) => {
|
||||
crypto.pbkdf2(seed, this.salt, 100000, 32, 'sha1', (err, derivedKey) => {
|
||||
if (err) reject(err);
|
||||
resolve(derivedKey);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 AES-256-GCM 模式进行加密
|
||||
*/
|
||||
public encryptData(key: Buffer, plaintext: string): Buffer {
|
||||
// GCM 模式推荐 nonce 长度,Python 默认通常为 16 字节
|
||||
const iv = crypto.randomBytes(16);
|
||||
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
|
||||
|
||||
const ciphertext = Buffer.concat([
|
||||
cipher.update(plaintext, 'utf8'),
|
||||
cipher.final()
|
||||
]);
|
||||
|
||||
// 获取 GCM 认证标签 (16 bytes)
|
||||
const tag = cipher.getAuthTag();
|
||||
|
||||
// 拼接结果:Nonce + Tag + Ciphertext
|
||||
return Buffer.concat([iv, tag, ciphertext]);
|
||||
}
|
||||
|
||||
/**
|
||||
* AES-256-GCM 解密
|
||||
*/
|
||||
public decryptData(key: Buffer, encryptedBlob: Buffer): string {
|
||||
try {
|
||||
// 切片提取组件
|
||||
const iv = encryptedBlob.subarray(0, 16);
|
||||
const tag = encryptedBlob.subarray(16, 32);
|
||||
const ciphertext = encryptedBlob.subarray(32);
|
||||
|
||||
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
|
||||
decipher.setAuthTag(tag);
|
||||
|
||||
const decrypted = Buffer.concat([
|
||||
decipher.update(ciphertext),
|
||||
decipher.final()
|
||||
]);
|
||||
|
||||
return decrypted.toString('utf8');
|
||||
} catch (error) {
|
||||
return "【解密失败】:密钥错误或数据被篡改";
|
||||
}
|
||||
}
|
||||
}
|
||||
135
src/utils/crypto_polyfill.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import * as ExpoCrypto from 'expo-crypto';
|
||||
import { Buffer } from 'buffer';
|
||||
import { pbkdf2 as noblePbkdf2 } from '@noble/hashes/pbkdf2';
|
||||
import { sha1 } from '@noble/hashes/sha1';
|
||||
import { sha256 } from '@noble/hashes/sha256';
|
||||
import { sha512 } from '@noble/hashes/sha512';
|
||||
import { gcm } from '@noble/ciphers/aes';
|
||||
|
||||
/**
|
||||
* Node.js Crypto Polyfill for React Native
|
||||
*/
|
||||
|
||||
export function randomBytes(size: number): Buffer {
|
||||
const bytes = new Uint8Array(size);
|
||||
ExpoCrypto.getRandomValues(bytes);
|
||||
return Buffer.from(bytes);
|
||||
}
|
||||
|
||||
const hashMap: Record<string, any> = {
|
||||
sha1,
|
||||
sha256,
|
||||
sha512,
|
||||
};
|
||||
|
||||
export function pbkdf2(
|
||||
password: string | Buffer,
|
||||
salt: string | Buffer,
|
||||
iterations: number,
|
||||
keylen: number,
|
||||
digest: string,
|
||||
callback: (err: Error | null, derivedKey: Buffer) => void
|
||||
): void {
|
||||
try {
|
||||
const passwordBytes = typeof password === 'string' ? Buffer.from(password) : password;
|
||||
const saltBytes = typeof salt === 'string' ? Buffer.from(salt) : salt;
|
||||
const hasher = hashMap[digest.toLowerCase()];
|
||||
|
||||
if (!hasher) {
|
||||
throw new Error(`Unsupported digest: ${digest}`);
|
||||
}
|
||||
|
||||
const result = noblePbkdf2(hasher, passwordBytes, saltBytes, {
|
||||
c: iterations,
|
||||
dkLen: keylen,
|
||||
});
|
||||
|
||||
callback(null, Buffer.from(result));
|
||||
} catch (err) {
|
||||
callback(err as Error, Buffer.alloc(0));
|
||||
}
|
||||
}
|
||||
|
||||
// AES-GCM Implementation
|
||||
class Cipher {
|
||||
private key: Uint8Array;
|
||||
private iv: Uint8Array;
|
||||
private authTag: Buffer | null = null;
|
||||
private aesGcm: any;
|
||||
private buffer: Buffer = Buffer.alloc(0);
|
||||
|
||||
constructor(key: Buffer, iv: Buffer) {
|
||||
this.key = new Uint8Array(key);
|
||||
this.iv = new Uint8Array(iv);
|
||||
// @noble/ciphers/aes gcm takes (key, nonce)
|
||||
this.aesGcm = gcm(this.key, this.iv);
|
||||
}
|
||||
|
||||
update(data: string | Buffer, inputEncoding?: string): Buffer {
|
||||
const input = typeof data === 'string' ? Buffer.from(data, inputEncoding as any) : data;
|
||||
this.buffer = Buffer.concat([this.buffer, input]);
|
||||
return Buffer.alloc(0);
|
||||
}
|
||||
|
||||
final(): Buffer {
|
||||
const result = this.aesGcm.encrypt(this.buffer);
|
||||
// @noble/ciphers returns ciphertext + tag (16 bytes)
|
||||
const tag = result.slice(-16);
|
||||
const ciphertext = result.slice(0, -16);
|
||||
this.authTag = Buffer.from(tag);
|
||||
return Buffer.from(ciphertext);
|
||||
}
|
||||
|
||||
getAuthTag(): Buffer {
|
||||
if (!this.authTag) throw new Error('Ciphers: TAG not available before final()');
|
||||
return this.authTag;
|
||||
}
|
||||
}
|
||||
|
||||
class Decipher {
|
||||
private key: Uint8Array;
|
||||
private iv: Uint8Array;
|
||||
private tag: Uint8Array | null = null;
|
||||
private aesGcm: any;
|
||||
private buffer: Buffer = Buffer.alloc(0);
|
||||
|
||||
constructor(key: Buffer, iv: Buffer) {
|
||||
this.key = new Uint8Array(key);
|
||||
this.iv = new Uint8Array(iv);
|
||||
this.aesGcm = gcm(this.key, this.iv);
|
||||
}
|
||||
|
||||
setAuthTag(tag: Buffer): void {
|
||||
this.tag = new Uint8Array(tag);
|
||||
}
|
||||
|
||||
update(data: Buffer): Buffer {
|
||||
this.buffer = Buffer.concat([this.buffer, data]);
|
||||
return Buffer.alloc(0);
|
||||
}
|
||||
|
||||
final(): Buffer {
|
||||
if (!this.tag) throw new Error('Decipher: Auth tag not set');
|
||||
// @noble/ciphers expects ciphertext then tag
|
||||
const full = new Uint8Array(this.buffer.length + this.tag.length);
|
||||
full.set(this.buffer);
|
||||
full.set(this.tag, this.buffer.length);
|
||||
|
||||
const decrypted = this.aesGcm.decrypt(full);
|
||||
return Buffer.from(decrypted);
|
||||
}
|
||||
}
|
||||
|
||||
export function createCipheriv(algorithm: string, key: Buffer, iv: Buffer): Cipher {
|
||||
if (algorithm !== 'aes-256-gcm') {
|
||||
throw new Error(`Polyfill only supports aes-256-gcm, got ${algorithm}`);
|
||||
}
|
||||
return new Cipher(key, iv);
|
||||
}
|
||||
|
||||
export function createDecipheriv(algorithm: string, key: Buffer, iv: Buffer): Decipher {
|
||||
if (algorithm !== 'aes-256-gcm') {
|
||||
throw new Error(`Polyfill only supports aes-256-gcm, got ${algorithm}`);
|
||||
}
|
||||
return new Decipher(key, iv);
|
||||
}
|
||||
@@ -3,3 +3,4 @@
|
||||
*/
|
||||
|
||||
export * from './sss';
|
||||
export * from './vaultAssets';
|
||||
|
||||
@@ -5,7 +5,12 @@
|
||||
* - Secret is split into 3 shares
|
||||
* - Any 2 shares can recover the original secret
|
||||
*
|
||||
* Based on the Sentinel crypto_core_demo Python implementation.
|
||||
* Correspondence with crypto_core_demo (Python):
|
||||
* - sp_trust_sharding.py: split_to_shares(), recover_from_shares()
|
||||
* - Same algorithm: f(x) = secret + a*x (mod P), Lagrange interpolation
|
||||
* - Difference: entropy conversion. Python uses BIP-39 (mnemonic.to_entropy);
|
||||
* we use custom word list index-based encoding for compatibility with
|
||||
* existing MNEMONIC_WORDS. SSS split/recover logic is identical.
|
||||
*/
|
||||
|
||||
// Use a large prime for the finite field
|
||||
|
||||
76
src/utils/token_utils.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Token Utilities
|
||||
*
|
||||
* Shared logic for trimming messages to stay within token limits.
|
||||
*/
|
||||
|
||||
import { BaseMessage, SystemMessage } from "@langchain/core/messages";
|
||||
import { AIMessage as ServiceAIMessage } from "../services/ai.service";
|
||||
|
||||
export const TOKEN_LIMIT = 10000;
|
||||
const CHARS_PER_TOKEN = 3; // Conservative estimate: 1 token ≈ 3 chars
|
||||
export const MAX_CHARS = TOKEN_LIMIT * CHARS_PER_TOKEN;
|
||||
|
||||
/**
|
||||
* Trims LangChain messages to fit within token limit
|
||||
*/
|
||||
export function trimLangChainMessages(messages: BaseMessage[]): BaseMessage[] {
|
||||
let totalLength = 0;
|
||||
const trimmed: BaseMessage[] = [];
|
||||
|
||||
// Always keep the system message if it's at the start
|
||||
let systemMsg: BaseMessage | null = null;
|
||||
if (messages.length > 0 && (messages[0] instanceof SystemMessage || (messages[0] as any)._getType?.() === 'system')) {
|
||||
systemMsg = messages[0];
|
||||
totalLength += systemMsg.content.toString().length;
|
||||
}
|
||||
|
||||
// Iterate backwards and add messages until we hit the char limit
|
||||
for (let i = messages.length - 1; i >= (systemMsg ? 1 : 0); i--) {
|
||||
const msg = messages[i];
|
||||
const len = msg.content.toString().length;
|
||||
|
||||
if (totalLength + len > MAX_CHARS) break;
|
||||
|
||||
trimmed.unshift(msg);
|
||||
totalLength += len;
|
||||
}
|
||||
|
||||
if (systemMsg) {
|
||||
trimmed.unshift(systemMsg);
|
||||
}
|
||||
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trims internal AIMessage format messages to fit within token limit
|
||||
*/
|
||||
export function trimInternalMessages(messages: ServiceAIMessage[]): ServiceAIMessage[] {
|
||||
let totalLength = 0;
|
||||
const trimmed: ServiceAIMessage[] = [];
|
||||
|
||||
// Always keep the system message if it's at the start
|
||||
let systemMsg: ServiceAIMessage | null = null;
|
||||
if (messages.length > 0 && messages[0].role === 'system') {
|
||||
systemMsg = messages[0];
|
||||
totalLength += systemMsg.content.length;
|
||||
}
|
||||
|
||||
// Iterate backwards and add messages until we hit the char limit
|
||||
for (let i = messages.length - 1; i >= (systemMsg ? 1 : 0); i--) {
|
||||
const msg = messages[i];
|
||||
const len = msg.content.length;
|
||||
|
||||
if (totalLength + len > MAX_CHARS) break;
|
||||
|
||||
trimmed.unshift(msg);
|
||||
totalLength += len;
|
||||
}
|
||||
|
||||
if (systemMsg) {
|
||||
trimmed.unshift(systemMsg);
|
||||
}
|
||||
|
||||
return trimmed;
|
||||
}
|
||||
71
src/utils/vaultAssets.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Vault assets: API ↔ UI mapping and initial mock data.
|
||||
* Used by useVaultAssets and VaultScreen for /assets/get and /assets/create flows.
|
||||
*/
|
||||
|
||||
import type { VaultAsset, VaultAssetType } from '../types';
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Types
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/** Shape returned by GET /assets/get (backend AssetOut) */
|
||||
export interface ApiAsset {
|
||||
id: number;
|
||||
title: string;
|
||||
type?: string;
|
||||
author_id?: number;
|
||||
private_key_shard?: string;
|
||||
content_outer_encrypted?: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
heir_id?: number;
|
||||
heir_email?: string;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Constants
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
export const VAULT_ASSET_TYPES: VaultAssetType[] = [
|
||||
'game_account',
|
||||
'private_key',
|
||||
'document',
|
||||
'photo',
|
||||
'will',
|
||||
'custom',
|
||||
];
|
||||
|
||||
export const initialVaultAssets: VaultAsset[] = [];
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Mapping
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Map backend API asset to VaultAsset for UI.
|
||||
*/
|
||||
export function mapApiAssetToVaultAsset(api: ApiAsset): VaultAsset {
|
||||
const type: VaultAssetType =
|
||||
api.type && VAULT_ASSET_TYPES.includes(api.type as VaultAssetType)
|
||||
? (api.type as VaultAssetType)
|
||||
: 'custom';
|
||||
return {
|
||||
id: String(api.id),
|
||||
type,
|
||||
label: api.title,
|
||||
createdAt: api.created_at ? new Date(api.created_at) : new Date(),
|
||||
updatedAt: api.updated_at ? new Date(api.updated_at) : new Date(),
|
||||
isEncrypted: true,
|
||||
heirEmail: api.heir_email,
|
||||
rawData: api,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Map array of API assets to VaultAsset[].
|
||||
*/
|
||||
export function mapApiAssetsToVaultAssets(apiList: ApiAsset[]): VaultAsset[] {
|
||||
return apiList.map(mapApiAssetToVaultAsset);
|
||||
}
|
||||
|
||||
107
src/utils/vaultCrypto.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* Vault crypto: PBKDF2 key derivation + AES-256-GCM encrypt/decrypt.
|
||||
* Matches backend SentinelVault semantics (PBKDF2 from mnemonic, AES-GCM).
|
||||
* Uses Web Crypto API (crypto.subtle). Requires secure context / React Native polyfill if needed.
|
||||
*/
|
||||
|
||||
const SALT = new TextEncoder().encode('Sentinel_Salt_2026');
|
||||
const PBKDF2_ITERATIONS = 100000;
|
||||
const AES_KEY_LEN = 256;
|
||||
const GCM_IV_LEN = 16;
|
||||
const GCM_TAG_LEN = 16;
|
||||
|
||||
function getCrypto(): Crypto {
|
||||
if (typeof crypto !== 'undefined' && crypto.subtle) return crypto;
|
||||
throw new Error('vaultCrypto: crypto.subtle not available');
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive a 32-byte AES key from mnemonic phrase (space-separated words).
|
||||
*/
|
||||
export async function deriveKey(mnemonicPhrase: string, salt: Uint8Array = SALT): Promise<ArrayBuffer> {
|
||||
const crypto = getCrypto();
|
||||
const keyMaterial = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
new TextEncoder().encode(mnemonicPhrase),
|
||||
'PBKDF2',
|
||||
false,
|
||||
['deriveBits']
|
||||
);
|
||||
const saltBuf = salt.buffer.slice(salt.byteOffset, salt.byteOffset + salt.byteLength) as ArrayBuffer;
|
||||
const bits = await crypto.subtle.deriveBits(
|
||||
{
|
||||
name: 'PBKDF2',
|
||||
salt: saltBuf,
|
||||
iterations: PBKDF2_ITERATIONS,
|
||||
hash: 'SHA-256',
|
||||
},
|
||||
keyMaterial,
|
||||
AES_KEY_LEN
|
||||
);
|
||||
return bits;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt plaintext with AES-256-GCM. Returns nonce(16) + tag(16) + ciphertext (matches Python SentinelVault).
|
||||
*/
|
||||
export async function encryptDataGCM(key: ArrayBuffer, plaintext: string): Promise<Uint8Array> {
|
||||
const crypto = getCrypto();
|
||||
const iv = crypto.getRandomValues(new Uint8Array(GCM_IV_LEN));
|
||||
const cryptoKey = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
key,
|
||||
{ name: 'AES-GCM' },
|
||||
false,
|
||||
['encrypt']
|
||||
);
|
||||
const encoded = new TextEncoder().encode(plaintext);
|
||||
const ciphertextWithTag = await crypto.subtle.encrypt(
|
||||
{ name: 'AES-GCM', iv, tagLength: GCM_TAG_LEN * 8 },
|
||||
cryptoKey,
|
||||
encoded
|
||||
);
|
||||
const out = new Uint8Array(iv.length + ciphertextWithTag.byteLength);
|
||||
out.set(iv, 0);
|
||||
out.set(new Uint8Array(ciphertextWithTag), iv.length);
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt blob from encryptDataGCM (nonce(16) + ciphertext+tag).
|
||||
*/
|
||||
export async function decryptDataGCM(key: ArrayBuffer, blob: Uint8Array): Promise<string> {
|
||||
const crypto = getCrypto();
|
||||
const iv = blob.subarray(0, GCM_IV_LEN);
|
||||
const ciphertextWithTag = blob.subarray(GCM_IV_LEN);
|
||||
const ivBuf = iv.buffer.slice(iv.byteOffset, iv.byteOffset + iv.byteLength) as ArrayBuffer;
|
||||
const ctBuf = ciphertextWithTag.buffer.slice(
|
||||
ciphertextWithTag.byteOffset,
|
||||
ciphertextWithTag.byteOffset + ciphertextWithTag.byteLength
|
||||
) as ArrayBuffer;
|
||||
const cryptoKey = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
key,
|
||||
{ name: 'AES-GCM' },
|
||||
false,
|
||||
['decrypt']
|
||||
);
|
||||
const dec = await crypto.subtle.decrypt(
|
||||
{ name: 'AES-GCM', iv: ivBuf, tagLength: GCM_TAG_LEN * 8 },
|
||||
cryptoKey,
|
||||
ctBuf
|
||||
);
|
||||
return new TextDecoder().decode(dec);
|
||||
}
|
||||
|
||||
export function bytesToHex(bytes: Uint8Array): string {
|
||||
return Array.from(bytes)
|
||||
.map((b) => b.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
}
|
||||
|
||||
export function hexToBytes(hex: string): Uint8Array {
|
||||
const len = hex.length / 2;
|
||||
const out = new Uint8Array(len);
|
||||
for (let i = 0; i < len; i++) out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
||||
return out;
|
||||
}
|
||||