Compare commits
10 Commits
feature/va
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
465881c0e4 | ||
|
|
8994a3e045 | ||
|
|
e33ea62e35 | ||
|
|
96d95a50fc | ||
|
|
c1ce804d14 | ||
|
|
0aab9a838b | ||
|
|
6822638d47 | ||
|
|
5c1172a912 | ||
|
|
b5373c2d9a | ||
|
|
3ffcc60ee8 |
1
App.tsx
1
App.tsx
@@ -4,6 +4,7 @@
|
|||||||
* Main application component with authentication routing.
|
* Main application component with authentication routing.
|
||||||
* Shows loading screen while restoring auth state.
|
* Shows loading screen while restoring auth state.
|
||||||
*/
|
*/
|
||||||
|
import './src/polyfills';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Buffer } from 'buffer';
|
import { Buffer } from 'buffer';
|
||||||
|
|||||||
798
README.md
798
README.md
@@ -1,287 +1,595 @@
|
|||||||
# Sentinel App
|
# Sentinel Frontend
|
||||||
|
|
||||||
[中文版](#中文版)
|
A React Native mobile application for secure digital legacy management, built with Expo and TypeScript. Sentinel enables users to preserve, encrypt, and conditionally transfer their digital assets to designated heirs through a sophisticated cryptographic architecture.
|
||||||
|
|
||||||
## Digital Legacy Management
|
## Table of Contents
|
||||||
|
|
||||||
Sentinel is a mobile application that helps users securely manage their digital legacy. Built with React Native (Expo) and TypeScript.
|
- [Overview](#overview)
|
||||||
|
- [Business Features](#business-features)
|
||||||
|
- [Technical Architecture](#technical-architecture)
|
||||||
|
- [Security & Cryptography](#security--cryptography)
|
||||||
|
- [Getting Started](#getting-started)
|
||||||
|
- [Project Structure](#project-structure)
|
||||||
|
- [Configuration](#configuration)
|
||||||
|
- [Development](#development)
|
||||||
|
|
||||||
## Features
|
## Overview
|
||||||
|
|
||||||
|
Sentinel is a comprehensive digital inheritance platform that addresses the critical challenge of preserving and transferring digital assets securely. The application combines advanced cryptographic techniques with an intuitive user interface, ensuring that sensitive information remains protected while enabling controlled access for designated beneficiaries.
|
||||||
|
|
||||||
|
### Core Value Proposition
|
||||||
|
|
||||||
|
- **Zero-Knowledge Architecture**: The platform cannot access user data without cryptographic keys
|
||||||
|
- **Conditional Release**: Assets are released only when specific conditions are met (e.g., subscription expiration, inactivity)
|
||||||
|
- **Multi-Layer Encryption**: Combines symmetric and asymmetric encryption for maximum security
|
||||||
|
- **Distributed Trust**: Uses secret sharing to prevent single points of failure
|
||||||
|
|
||||||
|
## Business Features
|
||||||
|
|
||||||
### 🗞️ Flow - Captain's Journal
|
### 🗞️ Flow - Captain's Journal
|
||||||
- Record daily thoughts, emotions, and reflections
|
|
||||||
|
An AI-powered journaling interface for daily thoughts, emotions, and reflections.
|
||||||
|
|
||||||
|
**Capabilities:**
|
||||||
|
- Multi-modal entry support: text, voice, and image inputs
|
||||||
- AI-inferred emotional state tracking
|
- AI-inferred emotional state tracking
|
||||||
- Archive entries to the encrypted Vault
|
- Conversational AI with multiple role configurations:
|
||||||
- Support for text, voice, and image entries
|
- Reflective Assistant: Deep introspection and emotional exploration
|
||||||
|
- Creative Spark: Brainstorming and creative writing
|
||||||
|
- Action Planner: Goal setting and productivity coaching
|
||||||
|
- Empathetic Guide: Emotional support and non-judgmental listening
|
||||||
|
- Archive entries to encrypted Vault for long-term preservation
|
||||||
|
- Conversation summarization for efficient review
|
||||||
|
|
||||||
|
**Use Cases:**
|
||||||
|
- Daily emotional tracking and mental health monitoring
|
||||||
|
- Creative writing and idea generation
|
||||||
|
- Goal planning and accountability
|
||||||
|
- Therapeutic journaling with AI support
|
||||||
|
|
||||||
### 📦 Vault - The Deep Vault
|
### 📦 Vault - The Deep Vault
|
||||||
- End-to-end encrypted asset storage
|
|
||||||
- Support for game accounts, private keys, documents, photos, wills
|
End-to-end encrypted storage for sensitive digital assets.
|
||||||
- Biometric authentication required for access
|
|
||||||
- Zero-knowledge architecture
|
**Supported Asset Types:**
|
||||||
|
- **Game Accounts**: Credentials for gaming platforms (Steam, etc.)
|
||||||
|
- **Private Keys**: Cryptographic keys and wallet seeds
|
||||||
|
- **Documents**: Legal documents, contracts, and important files
|
||||||
|
- **Photos**: Personal memories and sensitive images
|
||||||
|
- **Wills**: Testamentary documents and final wishes
|
||||||
|
- **Custom**: User-defined asset categories
|
||||||
|
|
||||||
|
**Security Features:**
|
||||||
|
- Biometric authentication required for access (Face ID / Touch ID / Fingerprint)
|
||||||
|
- Shamir's Secret Sharing (3-of-2 threshold) for key management
|
||||||
|
- AES-256-GCM encryption for content protection
|
||||||
|
- User-isolated storage (multi-account support)
|
||||||
|
- Zero-knowledge architecture (server cannot decrypt without user shares)
|
||||||
|
|
||||||
|
**Workflow:**
|
||||||
|
1. User creates asset with plaintext content
|
||||||
|
2. System generates mnemonic phrase and splits into 3 shares
|
||||||
|
3. Content encrypted with derived AES key
|
||||||
|
4. One share stored on device, one on server, one for heir
|
||||||
|
5. Any 2 shares required to recover decryption key
|
||||||
|
|
||||||
### ⚓ Sentinel - Lighthouse Watch
|
### ⚓ Sentinel - Lighthouse Watch
|
||||||
- Dead Man's Switch monitoring system
|
|
||||||
- Heartbeat confirmation mechanism
|
Dead Man's Switch monitoring system for conditional asset release.
|
||||||
- Subscription and activity tracking
|
|
||||||
- Configurable grace periods
|
**Mechanism:**
|
||||||
|
- Continuous heartbeat monitoring of user activity
|
||||||
|
- Subscription status tracking
|
||||||
|
- Configurable grace periods before triggering release
|
||||||
|
- Activity logging and audit trail
|
||||||
|
- Status indicators: Normal, Warning, Releasing
|
||||||
|
|
||||||
|
**Trigger Conditions:**
|
||||||
|
- Subscription expiration without renewal
|
||||||
|
- Extended inactivity period
|
||||||
|
- Manual activation by user
|
||||||
|
- Administrative declaration (for testing/emergencies)
|
||||||
|
|
||||||
|
**Release Process:**
|
||||||
|
1. System detects trigger condition
|
||||||
|
2. Outer encryption layer removed (RSA gateway unlocked)
|
||||||
|
3. Heirs notified of available assets
|
||||||
|
4. Heirs can claim assets with their share + server share
|
||||||
|
|
||||||
### 🧭 Heritage - Fleet Legacy
|
### 🧭 Heritage - Fleet Legacy
|
||||||
- Heir management with release levels
|
|
||||||
- Customizable release order and timing
|
Heir management and asset distribution system.
|
||||||
- Payment strategy configuration
|
|
||||||
- Legal document-style interface
|
**Features:**
|
||||||
|
- **Heir Management**: Add, edit, and remove designated beneficiaries
|
||||||
|
- **Release Levels**: Configure priority tiers (1-3) for asset access
|
||||||
|
- **Release Order**: Define sequence for multi-heir scenarios
|
||||||
|
- **Payment Strategies**:
|
||||||
|
- Prepaid: Heir pays upfront for access
|
||||||
|
- Pay on Access: Payment required when claiming assets
|
||||||
|
- **Legal Document Interface**: Formal, testamentary-style presentation
|
||||||
|
- **Assignment Workflow**: Assign specific assets to specific heirs
|
||||||
|
|
||||||
|
**Heir Status:**
|
||||||
|
- **Invited**: Heir has been designated but not yet confirmed
|
||||||
|
- **Confirmed**: Heir has accepted invitation and verified identity
|
||||||
|
|
||||||
### ⛵ Me - Captain's Quarters
|
### ⛵ Me - Captain's Quarters
|
||||||
- Subscription and protocol status
|
|
||||||
- Sentinel configuration
|
|
||||||
- Security center
|
|
||||||
- Data export and backup
|
|
||||||
- Social responsibility program
|
|
||||||
|
|
||||||
## Tech Stack
|
User account management and system configuration.
|
||||||
|
|
||||||
- **Framework**: React Native (Expo SDK 52)
|
**Sections:**
|
||||||
- **Language**: TypeScript
|
- **Subscription Status**: Current tier, expiration date, features enabled
|
||||||
- **Navigation**: React Navigation (Bottom Tabs)
|
- **Protocol Information**: Version tracking and update status
|
||||||
- **Icons**: @expo/vector-icons (Feather, Ionicons, FontAwesome5)
|
- **Sentinel Configuration**: Heartbeat intervals, grace periods, monitoring settings
|
||||||
- **Styling**: Custom nautical theme with gradients
|
- **Security Center**:
|
||||||
- **State Management**: React Context (AuthContext)
|
- Biometric settings
|
||||||
- **Storage**: AsyncStorage for auth persistence
|
- Vault state management
|
||||||
|
- Key recovery options
|
||||||
|
- **Data Export**: Backup encrypted vault data
|
||||||
|
- **Social Responsibility**: Program information and participation
|
||||||
|
|
||||||
## Configuration
|
## Technical Architecture
|
||||||
|
|
||||||
The application uses a centralized configuration file located at `src/config/index.ts`.
|
### Technology Stack
|
||||||
|
|
||||||
### Key Configuration Options
|
| Category | Technology | Purpose |
|
||||||
|
|----------|-----------|---------|
|
||||||
|
| **Framework** | React Native (Expo SDK 52) | Cross-platform mobile development |
|
||||||
|
| **Language** | TypeScript 5.3+ | Type-safe development |
|
||||||
|
| **Navigation** | React Navigation 6 | Bottom tabs + stack navigation |
|
||||||
|
| **State Management** | React Context API | Authentication and app state |
|
||||||
|
| **Storage** | AsyncStorage | Local persistence (auth tokens, vault state) |
|
||||||
|
| **Icons** | @expo/vector-icons | Feather, Ionicons, FontAwesome5, Material |
|
||||||
|
| **Styling** | StyleSheet + LinearGradient | Custom nautical-themed UI |
|
||||||
|
| **Crypto** | Web Crypto API + Polyfills | Cryptographic operations |
|
||||||
|
| **AI Integration** | LangChain + LangGraph | AI conversation management |
|
||||||
|
| **Build System** | Metro Bundler | JavaScript bundling |
|
||||||
|
|
||||||
| Option | Description | Default |
|
### Architecture Patterns
|
||||||
|--------|-------------|---------|
|
|
||||||
| `NO_BACKEND_MODE` | Use mock data instead of real backend | `false` |
|
|
||||||
| `DEBUG_MODE` | Enable API debug logging | `true` |
|
|
||||||
| `API_BASE_URL` | Backend API server URL | `http://localhost:8000` |
|
|
||||||
| `API_TIMEOUT` | Request timeout (ms) | `30000` |
|
|
||||||
|
|
||||||
### API Endpoints
|
#### Service Layer Architecture
|
||||||
|
|
||||||
All backend API routes are defined in `API_ENDPOINTS`:
|
|
||||||
- **AUTH**: `/login`, `/register`
|
|
||||||
- **ASSETS**: `/assets/get`, `/assets/create`, `/assets/claim`, `/assets/assign`
|
|
||||||
- **AI**: `/ai/proxy`
|
|
||||||
- **ADMIN**: `/admin/declare-guale`
|
|
||||||
|
|
||||||
### Environment Setup
|
|
||||||
|
|
||||||
For development, you may need to modify `API_BASE_URL` in the config file to match your backend server address.
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
src/
|
|
||||||
├── components/
|
|
||||||
│ └── common/
|
|
||||||
│ ├── BiometricModal.tsx
|
|
||||||
│ ├── Icons.tsx
|
|
||||||
│ └── VaultDoorAnimation.tsx
|
|
||||||
├── config/
|
|
||||||
│ └── index.ts # Centralized configuration
|
|
||||||
├── context/
|
|
||||||
│ └── AuthContext.tsx # Authentication state management
|
|
||||||
├── navigation/
|
|
||||||
│ ├── AuthNavigator.tsx # Login/Register navigation
|
|
||||||
│ └── TabNavigator.tsx # Main app navigation
|
|
||||||
├── screens/
|
|
||||||
│ ├── FlowScreen.tsx # AI chat interface
|
|
||||||
│ ├── VaultScreen.tsx
|
|
||||||
│ ├── SentinelScreen.tsx
|
|
||||||
│ ├── HeritageScreen.tsx
|
|
||||||
│ ├── MeScreen.tsx
|
|
||||||
│ ├── LoginScreen.tsx
|
|
||||||
│ └── RegisterScreen.tsx
|
|
||||||
├── services/
|
|
||||||
│ ├── index.ts # Service exports
|
|
||||||
│ ├── ai.service.ts # AI API integration
|
|
||||||
│ ├── auth.service.ts # Authentication API
|
|
||||||
│ ├── assets.service.ts # Asset management API
|
|
||||||
│ └── admin.service.ts # Admin operations API
|
|
||||||
├── theme/
|
|
||||||
│ └── colors.ts
|
|
||||||
└── types/
|
|
||||||
└── index.ts
|
|
||||||
|
|
||||||
assets/
|
|
||||||
├── icon.png # App icon (1024x1024)
|
|
||||||
├── adaptive-icon.png # Android adaptive icon
|
|
||||||
├── splash.png # Splash screen
|
|
||||||
├── favicon.png # Web favicon (32x32)
|
|
||||||
├── favicon.svg # SVG favicon for web
|
|
||||||
├── logo.svg # Vector logo (512x512)
|
|
||||||
└── images/
|
|
||||||
└── captain-avatar.svg # Avatar placeholder
|
|
||||||
```
|
|
||||||
|
|
||||||
## Services
|
|
||||||
|
|
||||||
The application uses a modular service architecture for API communication:
|
The application uses a modular service architecture for API communication:
|
||||||
|
|
||||||
- **AuthService**: User authentication (login, register)
|
```
|
||||||
- **AIService**: AI conversation proxy with support for text and image input
|
src/services/
|
||||||
- **AssetsService**: Digital asset management
|
├── auth.service.ts # Authentication (login, register)
|
||||||
- **AdminService**: Administrative operations
|
├── assets.service.ts # Asset CRUD and inheritance
|
||||||
|
├── vault.service.ts # Vault encryption/decryption
|
||||||
## Icons & Branding
|
├── ai.service.ts # AI conversation proxy
|
||||||
|
├── admin.service.ts # Administrative operations
|
||||||
The Sentinel brand uses a nautical anchor-and-star logo on a teal (#459E9E) background.
|
├── langgraph.service.ts # LangGraph workflow integration
|
||||||
|
└── storage.service.ts # AsyncStorage abstraction
|
||||||
### Logo Elements
|
|
||||||
- **Anchor**: Symbolizes stability and anchoring your digital legacy
|
|
||||||
- **Star/Compass**: Represents guidance and direction for heirs
|
|
||||||
- **Teal Color**: Evokes ocean depth and calm security
|
|
||||||
|
|
||||||
### Generating Icons
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# View icon specifications
|
|
||||||
node scripts/generate-icons.js
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Use the `assets/logo.svg` as the source and export to required sizes.
|
**Service Pattern:**
|
||||||
|
- Centralized API configuration
|
||||||
|
- Mock mode support for development
|
||||||
|
- Consistent error handling
|
||||||
|
- Debug logging integration
|
||||||
|
- Type-safe request/response interfaces
|
||||||
|
|
||||||
|
#### Context-Based State Management
|
||||||
|
|
||||||
|
**AuthContext** (`src/context/AuthContext.tsx`):
|
||||||
|
- Manages authentication state
|
||||||
|
- Handles token persistence
|
||||||
|
- Provides user information throughout app
|
||||||
|
- Handles initialization and loading states
|
||||||
|
|
||||||
|
**Usage Pattern:**
|
||||||
|
```typescript
|
||||||
|
const { user, token, login, logout, isInitializing } = useAuth();
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Navigation Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
App (Root)
|
||||||
|
├── AuthNavigator (if not authenticated)
|
||||||
|
│ ├── LoginScreen
|
||||||
|
│ └── RegisterScreen
|
||||||
|
└── TabNavigator (if authenticated)
|
||||||
|
├── FlowScreen (🗞️)
|
||||||
|
├── VaultScreen (📦)
|
||||||
|
├── SentinelScreen (⚓)
|
||||||
|
├── HeritageScreen (🧭)
|
||||||
|
└── MeScreen (⛵)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Dependencies
|
||||||
|
|
||||||
|
**Core:**
|
||||||
|
- `react`: 18.3.1
|
||||||
|
- `react-native`: 0.76.9
|
||||||
|
- `expo`: ~52.0.0
|
||||||
|
|
||||||
|
**Navigation:**
|
||||||
|
- `@react-navigation/native`: ^6.1.18
|
||||||
|
- `@react-navigation/bottom-tabs`: ^6.6.1
|
||||||
|
- `@react-navigation/native-stack`: ^6.11.0
|
||||||
|
|
||||||
|
**Cryptography:**
|
||||||
|
- `@noble/ciphers`: ^1.3.0 (AES encryption)
|
||||||
|
- `@noble/hashes`: ^1.8.0 (Hash functions)
|
||||||
|
- `bip39`: ^3.1.0 (Mnemonic generation)
|
||||||
|
- `expo-crypto`: ~14.0.2 (Crypto polyfills)
|
||||||
|
|
||||||
|
**AI & Language Models:**
|
||||||
|
- `@langchain/core`: ^1.1.18
|
||||||
|
- `@langchain/langgraph`: ^1.1.3
|
||||||
|
|
||||||
|
**Storage & Utilities:**
|
||||||
|
- `@react-native-async-storage/async-storage`: ^2.2.0
|
||||||
|
- `buffer`: ^6.0.3 (Node.js Buffer polyfill)
|
||||||
|
- `readable-stream`: ^4.7.0
|
||||||
|
|
||||||
|
## Security & Cryptography
|
||||||
|
|
||||||
|
### Encryption Architecture
|
||||||
|
|
||||||
|
Sentinel implements a multi-layer encryption system:
|
||||||
|
|
||||||
|
#### Layer 1: Vault Encryption (User-Controlled)
|
||||||
|
|
||||||
|
**Process:**
|
||||||
|
1. Generate 12-word BIP-39 mnemonic phrase
|
||||||
|
2. Derive AES-256 key using PBKDF2:
|
||||||
|
- Input: Mnemonic phrase
|
||||||
|
- Salt: `Sentinel_Salt_2026`
|
||||||
|
- Iterations: 100,000
|
||||||
|
- Hash: SHA-256
|
||||||
|
- Output: 32-byte AES key
|
||||||
|
3. Encrypt plaintext with AES-256-GCM:
|
||||||
|
- Mode: Galois/Counter Mode (authenticated encryption)
|
||||||
|
- IV: 16-byte random nonce
|
||||||
|
- Tag: 16-byte authentication tag
|
||||||
|
- Output: `IV + Ciphertext + Tag` (hex encoded)
|
||||||
|
|
||||||
|
**Implementation:** `src/utils/vaultCrypto.ts`
|
||||||
|
|
||||||
|
#### Layer 2: Secret Sharing (Distributed Trust)
|
||||||
|
|
||||||
|
**Shamir's Secret Sharing (3-of-2 Threshold):**
|
||||||
|
|
||||||
|
1. Convert mnemonic to entropy (BigInt representation)
|
||||||
|
2. Split entropy into 3 shares using linear polynomial:
|
||||||
|
```
|
||||||
|
f(x) = secret + a*x (mod P)
|
||||||
|
```
|
||||||
|
Where:
|
||||||
|
- `secret`: Mnemonic entropy
|
||||||
|
- `a`: Random coefficient
|
||||||
|
- `P`: Prime modulus (2^127 - 1)
|
||||||
|
- Shares at x = 1, 2, 3
|
||||||
|
|
||||||
|
3. Share Distribution:
|
||||||
|
- **Device Share (S0)**: Stored locally on user's device
|
||||||
|
- **Cloud Share (S1)**: Stored on Sentinel server
|
||||||
|
- **Heir Share (S2)**: Provided to designated heir
|
||||||
|
|
||||||
|
4. Recovery: Any 2 shares can recover original entropy via Lagrange interpolation
|
||||||
|
|
||||||
|
**Implementation:** `src/utils/sss.ts`
|
||||||
|
|
||||||
|
#### Layer 3: Gateway Encryption (Server-Controlled)
|
||||||
|
|
||||||
|
**RSA Outer Encryption:**
|
||||||
|
- Server generates RSA-4096 key pair per asset
|
||||||
|
- Inner encrypted content encrypted again with RSA public key
|
||||||
|
- Private key held by server, released only on trigger conditions
|
||||||
|
- Prevents unauthorized access even if inner encryption is compromised
|
||||||
|
|
||||||
|
**Note:** Gateway encryption is handled by backend; frontend sends `content_inner_encrypted` which backend wraps with RSA.
|
||||||
|
|
||||||
|
### Key Management
|
||||||
|
|
||||||
|
**Storage Strategy:**
|
||||||
|
- User-isolated keys: `getVaultStorageKeys(userId)` generates per-user storage keys
|
||||||
|
- Device share (S0): Stored in AsyncStorage with user-scoped key
|
||||||
|
- Mnemonic backup: Optional local backup of mnemonic portion (encrypted)
|
||||||
|
- Multi-account support: Each user has independent vault state
|
||||||
|
|
||||||
|
**Storage Keys:**
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
INITIALIZED: `sentinel_vault_initialized_u{userId}`,
|
||||||
|
SHARE_DEVICE: `sentinel_vault_s0_u{userId}`,
|
||||||
|
MNEMONIC_PART_LOCAL: `sentinel_mnemonic_part_local_u{userId}`,
|
||||||
|
AES_KEY: `sentinel_aes_key_u{userId}`,
|
||||||
|
SHARE_SERVER: `sentinel_share_server_u{userId}`,
|
||||||
|
SHARE_HEIR: `sentinel_share_heir_u{userId}`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Security Properties
|
||||||
|
|
||||||
|
1. **Zero-Knowledge**: Server cannot decrypt user data without user's share
|
||||||
|
2. **Forward Secrecy**: Compromising one share reveals nothing about the secret
|
||||||
|
3. **Authenticated Encryption**: GCM mode ensures data integrity
|
||||||
|
4. **Key Derivation**: PBKDF2 with high iteration count prevents brute force
|
||||||
|
5. **Distributed Trust**: No single point of failure for key recovery
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Node.js 18+ and npm
|
||||||
|
- Expo CLI (installed globally or via npx)
|
||||||
|
- iOS Simulator (macOS) or Android Emulator / physical device
|
||||||
|
- Backend API server running (or use `NO_BACKEND_MODE`)
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
# Start the development server
|
# Start Expo development server
|
||||||
npx expo start
|
npx expo start
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Platform-Specific Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# iOS Simulator (macOS only)
|
||||||
|
npm run ios
|
||||||
|
|
||||||
|
# Android Emulator / Device
|
||||||
|
npm run android
|
||||||
|
|
||||||
|
# Web Browser
|
||||||
|
npm run web
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development Modes
|
||||||
|
|
||||||
|
**With Backend:**
|
||||||
|
1. Ensure backend server is running (default: `http://localhost:8000`)
|
||||||
|
2. Update `API_BASE_URL` in `src/config/index.ts` if needed
|
||||||
|
3. Start Expo: `npx expo start`
|
||||||
|
|
||||||
|
**Without Backend (Mock Mode):**
|
||||||
|
1. Set `NO_BACKEND_MODE = true` in `src/config/index.ts`
|
||||||
|
2. All API calls return mock data
|
||||||
|
3. Useful for UI development and testing
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
frontend/
|
||||||
|
├── App.tsx # Root component with auth routing
|
||||||
|
├── app.json # Expo configuration
|
||||||
|
├── package.json # Dependencies and scripts
|
||||||
|
├── tsconfig.json # TypeScript configuration
|
||||||
|
│
|
||||||
|
├── assets/ # Static assets
|
||||||
|
│ ├── icon.png # App icon (1024x1024)
|
||||||
|
│ ├── adaptive-icon.png # Android adaptive icon
|
||||||
|
│ ├── splash.png # Splash screen
|
||||||
|
│ ├── favicon.png # Web favicon
|
||||||
|
│ ├── logo.svg # Vector logo
|
||||||
|
│ └── images/ # Additional images
|
||||||
|
│
|
||||||
|
├── scripts/ # Build scripts
|
||||||
|
│ └── generate-icons.js # Icon generation utility
|
||||||
|
│
|
||||||
|
└── src/
|
||||||
|
├── components/ # Reusable UI components
|
||||||
|
│ └── common/
|
||||||
|
│ ├── BiometricModal.tsx # Biometric auth modal
|
||||||
|
│ ├── Icons.tsx # Icon helper component
|
||||||
|
│ └── VaultDoorAnimation.tsx # Vault unlock animation
|
||||||
|
│
|
||||||
|
├── config/ # Configuration
|
||||||
|
│ └── index.ts # Centralized config (API, endpoints, etc.)
|
||||||
|
│
|
||||||
|
├── context/ # React Context providers
|
||||||
|
│ └── AuthContext.tsx # Authentication state management
|
||||||
|
│
|
||||||
|
├── hooks/ # Custom React hooks
|
||||||
|
│ ├── index.ts
|
||||||
|
│ └── useVaultAssets.ts # Vault asset management hook
|
||||||
|
│
|
||||||
|
├── navigation/ # Navigation configuration
|
||||||
|
│ ├── AuthNavigator.tsx # Login/Register navigation
|
||||||
|
│ └── TabNavigator.tsx # Main app tab navigation
|
||||||
|
│
|
||||||
|
├── screens/ # Screen components
|
||||||
|
│ ├── FlowScreen.tsx # AI journaling interface
|
||||||
|
│ ├── VaultScreen.tsx # Encrypted asset management
|
||||||
|
│ ├── SentinelScreen.tsx # Dead Man's Switch monitoring
|
||||||
|
│ ├── HeritageScreen.tsx # Heir management
|
||||||
|
│ ├── MeScreen.tsx # User settings and account
|
||||||
|
│ ├── LoginScreen.tsx # Authentication
|
||||||
|
│ └── RegisterScreen.tsx
|
||||||
|
│
|
||||||
|
├── services/ # API service layer
|
||||||
|
│ ├── index.ts # Service exports
|
||||||
|
│ ├── auth.service.ts # Authentication API
|
||||||
|
│ ├── assets.service.ts # Asset CRUD API
|
||||||
|
│ ├── vault.service.ts # Vault encryption utilities
|
||||||
|
│ ├── ai.service.ts # AI conversation API
|
||||||
|
│ ├── admin.service.ts # Admin operations
|
||||||
|
│ ├── langgraph.service.ts # LangGraph integration
|
||||||
|
│ └── storage.service.ts # AsyncStorage abstraction
|
||||||
|
│
|
||||||
|
├── theme/ # Design system
|
||||||
|
│ └── colors.ts # Color palette and typography
|
||||||
|
│
|
||||||
|
├── types/ # TypeScript type definitions
|
||||||
|
│ └── index.ts # Shared types and interfaces
|
||||||
|
│
|
||||||
|
├── utils/ # Utility functions
|
||||||
|
│ ├── index.ts
|
||||||
|
│ ├── crypto_core.ts # Crypto utilities (if needed)
|
||||||
|
│ ├── crypto_polyfill.ts # Crypto API polyfills
|
||||||
|
│ ├── sss.ts # Shamir's Secret Sharing
|
||||||
|
│ ├── vaultCrypto.ts # AES encryption/decryption
|
||||||
|
│ ├── vaultAssets.ts # Asset management utilities
|
||||||
|
│ ├── token_utils.ts # Token management
|
||||||
|
│ └── async_hooks_mock.ts # Async hooks polyfill
|
||||||
|
│
|
||||||
|
└── polyfills.ts # Global polyfills (Buffer, etc.)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Environment Configuration
|
||||||
|
|
||||||
|
All configuration is centralized in `src/config/index.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Development mode
|
||||||
|
export const NO_BACKEND_MODE = false; // Use mock data
|
||||||
|
export const DEBUG_MODE = true; // API debug logging
|
||||||
|
|
||||||
|
// API Configuration
|
||||||
|
export const API_BASE_URL = 'http://localhost:8000';
|
||||||
|
export const API_TIMEOUT = 30000; // 30 seconds
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Endpoints
|
||||||
|
|
||||||
|
Defined in `API_ENDPOINTS` constant:
|
||||||
|
|
||||||
|
| Category | Endpoint | Method | Purpose |
|
||||||
|
|----------|----------|--------|---------|
|
||||||
|
| **Auth** | `/login` | POST | User authentication |
|
||||||
|
| | `/register` | POST | User registration |
|
||||||
|
| **Assets** | `/assets/get` | GET | Fetch user's assets |
|
||||||
|
| | `/assets/create` | POST | Create new asset |
|
||||||
|
| | `/assets/claim` | POST | Claim inherited asset |
|
||||||
|
| | `/assets/assign` | POST | Assign asset to heir |
|
||||||
|
| | `/assets/delete` | POST | Delete asset |
|
||||||
|
| **AI** | `/ai/proxy` | POST | AI conversation proxy |
|
||||||
|
| | `/get_ai_roles` | GET | Fetch available AI roles |
|
||||||
|
| **Admin** | `/admin/declare-guale` | POST | Admin: Declare user deceased |
|
||||||
|
|
||||||
|
### Vault Storage Configuration
|
||||||
|
|
||||||
|
Vault storage uses user-scoped keys for multi-account support:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Get storage keys for a user
|
||||||
|
const keys = getVaultStorageKeys(userId);
|
||||||
|
|
||||||
|
// Keys are prefixed with user ID to prevent cross-user access
|
||||||
|
// Format: sentinel_vault_{key}_{suffix}
|
||||||
|
// Suffix: _u{userId} for authenticated users, _guest for guests
|
||||||
|
```
|
||||||
|
|
||||||
|
### AI Configuration
|
||||||
|
|
||||||
|
AI roles and system prompts are configurable:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const AI_CONFIG = {
|
||||||
|
DEFAULT_SYSTEM_PROMPT: '...',
|
||||||
|
MOCK_RESPONSE_DELAY: 500,
|
||||||
|
ROLES: [
|
||||||
|
{ id: 'reflective', name: 'Reflective Assistant', ... },
|
||||||
|
{ id: 'creative', name: 'Creative Spark', ... },
|
||||||
|
{ id: 'planner', name: 'Action Planner', ... },
|
||||||
|
{ id: 'empathetic', name: 'Empathetic Guide', ... },
|
||||||
|
]
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Code Style
|
||||||
|
|
||||||
|
- **TypeScript**: Strict mode enabled, all files typed
|
||||||
|
- **Components**: Functional components with hooks
|
||||||
|
- **Naming**: PascalCase for components, camelCase for functions/variables
|
||||||
|
- **Imports**: Absolute imports preferred (configured in tsconfig.json)
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
**Manual Testing:**
|
||||||
|
- Use Expo Go app on physical device for real-world testing
|
||||||
|
- Test biometric authentication on actual devices
|
||||||
|
- Verify encryption/decryption flows with real backend
|
||||||
|
|
||||||
|
**Mock Mode Testing:**
|
||||||
|
- Enable `NO_BACKEND_MODE` for UI/UX testing
|
||||||
|
- Mock responses simulate real API behavior
|
||||||
|
- Useful for rapid iteration without backend dependency
|
||||||
|
|
||||||
|
### Debugging
|
||||||
|
|
||||||
|
**API Debug Logging:**
|
||||||
|
- Enabled when `DEBUG_MODE = true`
|
||||||
|
- Logs all API requests/responses to console
|
||||||
|
- Includes request URLs, headers, and response data
|
||||||
|
|
||||||
|
**React Native Debugger:**
|
||||||
|
- Use React Native Debugger or Chrome DevTools
|
||||||
|
- Inspect component state and props
|
||||||
|
- Monitor network requests
|
||||||
|
|
||||||
|
### Building for Production
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build for iOS
|
||||||
|
eas build --platform ios
|
||||||
|
|
||||||
|
# Build for Android
|
||||||
|
eas build --platform android
|
||||||
|
|
||||||
|
# Build for Web
|
||||||
|
npx expo export:web
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** Requires Expo Application Services (EAS) account for native builds.
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
**Crypto API Not Available:**
|
||||||
|
- Ensure polyfills are loaded (`src/polyfills.ts`)
|
||||||
|
- Check that `crypto.subtle` is available in environment
|
||||||
|
- React Native requires polyfills for Web Crypto API
|
||||||
|
|
||||||
|
**AsyncStorage Errors:**
|
||||||
|
- Ensure `@react-native-async-storage/async-storage` is properly linked
|
||||||
|
- Check storage permissions on device
|
||||||
|
- Clear storage if corrupted: `AsyncStorage.clear()`
|
||||||
|
|
||||||
|
**Navigation Issues:**
|
||||||
|
- Ensure `NavigationContainer` wraps navigators
|
||||||
|
- Check that screens are properly registered
|
||||||
|
- Verify tab bar configuration matches screen names
|
||||||
|
|
||||||
## Design Philosophy
|
## Design Philosophy
|
||||||
|
|
||||||
- **Nautical Theme**: Captain's sanctum aesthetic with anchors, ship wheels, and ocean colors
|
### Nautical Theme
|
||||||
- **Emotional Balance**: Warm and secure feeling across different tabs
|
|
||||||
- **Privacy First**: Zero-knowledge architecture, local encryption
|
The application uses a consistent nautical/maritime aesthetic:
|
||||||
- **Elegant UI**: Mint gradients, serif typography, subtle shadows
|
|
||||||
|
- **Color Palette**: Teal (#459E9E) primary, ocean blues, mint gradients
|
||||||
|
- **Iconography**: Anchors, ship wheels, compasses, lighthouses
|
||||||
|
- **Terminology**: Captain, Fleet, Vault, Sentinel, Heritage
|
||||||
|
- **Typography**: Serif fonts for formal sections, sans-serif for UI
|
||||||
|
|
||||||
|
### User Experience Principles
|
||||||
|
|
||||||
|
1. **Privacy First**: Encryption happens locally, user controls keys
|
||||||
|
2. **Transparency**: Clear explanation of security mechanisms
|
||||||
|
3. **Accessibility**: Biometric auth for convenience, fallback options available
|
||||||
|
4. **Elegance**: Clean, modern UI with subtle animations
|
||||||
|
5. **Trust**: Visual indicators for security status and system health
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Private - All rights reserved.
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues, questions, or contributions, please contact the development team.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# 中文版
|
**Version**: 2.0.0
|
||||||
|
**Last Updated**: February 2026
|
||||||
[English Version](#sentinel-app)
|
|
||||||
|
|
||||||
## 数字遗产管理
|
|
||||||
|
|
||||||
Sentinel 是一款帮助用户安全管理数字遗产的移动应用程序。使用 React Native (Expo) 和 TypeScript 构建。
|
|
||||||
|
|
||||||
## 功能特性
|
|
||||||
|
|
||||||
### 🗞️ Flow - 船长日志
|
|
||||||
- 记录日常想法、情感和反思
|
|
||||||
- AI 推断情感状态追踪
|
|
||||||
- 将条目归档到加密保险库
|
|
||||||
- 支持文本、语音和图像条目
|
|
||||||
|
|
||||||
### 📦 Vault - 深海宝库
|
|
||||||
- 端到端加密资产存储
|
|
||||||
- 支持游戏账号、私钥、文档、照片、遗嘱
|
|
||||||
- 需要生物识别认证才能访问
|
|
||||||
- 零知识架构
|
|
||||||
|
|
||||||
### ⚓ Sentinel - 灯塔守望
|
|
||||||
- 死人开关监控系统
|
|
||||||
- 心跳确认机制
|
|
||||||
- 订阅和活动追踪
|
|
||||||
- 可配置的冷静期
|
|
||||||
|
|
||||||
### 🧭 Heritage - 舰队遗产
|
|
||||||
- 继承人管理与释放等级
|
|
||||||
- 可自定义释放顺序和时间
|
|
||||||
- 付款策略配置
|
|
||||||
- 法律文书风格界面
|
|
||||||
|
|
||||||
### ⛵ Me - 船长室
|
|
||||||
- 订阅和协议状态
|
|
||||||
- 哨兵配置
|
|
||||||
- 安全中心
|
|
||||||
- 数据导出和备份
|
|
||||||
- 社会责任计划
|
|
||||||
|
|
||||||
## 技术栈
|
|
||||||
|
|
||||||
- **框架**: React Native (Expo SDK 52)
|
|
||||||
- **语言**: TypeScript
|
|
||||||
- **导航**: React Navigation (底部标签)
|
|
||||||
- **图标**: @expo/vector-icons (Feather, Ionicons, FontAwesome5)
|
|
||||||
- **样式**: 自定义航海主题配渐变
|
|
||||||
- **状态管理**: React Context (AuthContext)
|
|
||||||
- **存储**: AsyncStorage 用于认证持久化
|
|
||||||
|
|
||||||
## 配置说明
|
|
||||||
|
|
||||||
应用使用位于 `src/config/index.ts` 的集中配置文件。
|
|
||||||
|
|
||||||
### 主要配置项
|
|
||||||
|
|
||||||
| 选项 | 说明 | 默认值 |
|
|
||||||
|------|------|--------|
|
|
||||||
| `NO_BACKEND_MODE` | 使用模拟数据而非真实后端 | `false` |
|
|
||||||
| `DEBUG_MODE` | 启用 API 调试日志 | `true` |
|
|
||||||
| `API_BASE_URL` | 后端 API 服务器地址 | `http://localhost:8000` |
|
|
||||||
| `API_TIMEOUT` | 请求超时时间(毫秒) | `30000` |
|
|
||||||
|
|
||||||
### API 端点
|
|
||||||
|
|
||||||
所有后端 API 路由定义在 `API_ENDPOINTS` 中:
|
|
||||||
- **AUTH**: `/login`, `/register`
|
|
||||||
- **ASSETS**: `/assets/get`, `/assets/create`, `/assets/claim`, `/assets/assign`
|
|
||||||
- **AI**: `/ai/proxy`
|
|
||||||
- **ADMIN**: `/admin/declare-guale`
|
|
||||||
|
|
||||||
### 环境配置
|
|
||||||
|
|
||||||
开发时,您可能需要修改配置文件中的 `API_BASE_URL` 以匹配您的后端服务器地址。
|
|
||||||
|
|
||||||
## 服务层
|
|
||||||
|
|
||||||
应用使用模块化的服务架构进行 API 通信:
|
|
||||||
|
|
||||||
- **AuthService**: 用户认证(登录、注册)
|
|
||||||
- **AIService**: AI 对话代理,支持文本和图片输入
|
|
||||||
- **AssetsService**: 数字资产管理
|
|
||||||
- **AdminService**: 管理员操作
|
|
||||||
|
|
||||||
## 运行项目
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 安装依赖
|
|
||||||
npm install
|
|
||||||
|
|
||||||
# 启动开发服务器
|
|
||||||
npx expo start
|
|
||||||
```
|
|
||||||
|
|
||||||
## 图标与品牌
|
|
||||||
|
|
||||||
Sentinel 品牌使用青色(#459E9E)背景上的航海锚与星星标志。
|
|
||||||
|
|
||||||
### 标志元素
|
|
||||||
- **锚**: 象征稳定性和锚定你的数字遗产
|
|
||||||
- **星星/指南针**: 代表对继承人的指引和方向
|
|
||||||
- **青色**: 唤起海洋深度和平静的安全感
|
|
||||||
|
|
||||||
### 生成图标
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 查看图标规格
|
|
||||||
node scripts/generate-icons.js
|
|
||||||
```
|
|
||||||
|
|
||||||
使用 `assets/logo.svg` 作为源文件并导出所需尺寸。
|
|
||||||
|
|
||||||
## 设计理念
|
|
||||||
|
|
||||||
- **航海主题**: 船长圣殿美学,配以锚、船舵和海洋色彩
|
|
||||||
- **情感平衡**: 不同标签页带来温暖而安全的感觉
|
|
||||||
- **隐私优先**: 零知识架构,本地加密
|
|
||||||
- **优雅界面**: 薄荷渐变、衬线字体、柔和阴影
|
|
||||||
|
|||||||
BIN
assets/images/icon.png
Normal file
BIN
assets/images/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 70 B |
15
metro.config.js
Normal file
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;
|
||||||
471
package-lock.json
generated
471
package-lock.json
generated
@@ -10,6 +10,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/metro-runtime": "~4.0.1",
|
"@expo/metro-runtime": "~4.0.1",
|
||||||
"@expo/vector-icons": "~14.0.4",
|
"@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-native-async-storage/async-storage": "^2.2.0",
|
||||||
"@react-navigation/bottom-tabs": "^6.6.1",
|
"@react-navigation/bottom-tabs": "^6.6.1",
|
||||||
"@react-navigation/native": "^6.1.18",
|
"@react-navigation/native": "^6.1.18",
|
||||||
@@ -19,6 +23,7 @@
|
|||||||
"expo": "~52.0.0",
|
"expo": "~52.0.0",
|
||||||
"expo-asset": "~11.0.5",
|
"expo-asset": "~11.0.5",
|
||||||
"expo-constants": "~17.0.8",
|
"expo-constants": "~17.0.8",
|
||||||
|
"expo-crypto": "~14.0.2",
|
||||||
"expo-font": "~13.0.4",
|
"expo-font": "~13.0.4",
|
||||||
"expo-haptics": "~14.0.0",
|
"expo-haptics": "~14.0.0",
|
||||||
"expo-image-picker": "^17.0.10",
|
"expo-image-picker": "^17.0.10",
|
||||||
@@ -32,7 +37,9 @@
|
|||||||
"react-native-safe-area-context": "4.12.0",
|
"react-native-safe-area-context": "4.12.0",
|
||||||
"react-native-screens": "~4.4.0",
|
"react-native-screens": "~4.4.0",
|
||||||
"react-native-view-shot": "^3.8.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": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.25.2",
|
"@babel/core": "^7.25.2",
|
||||||
@@ -2198,6 +2205,12 @@
|
|||||||
"node": ">=6.9.0"
|
"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": {
|
"node_modules/@egjs/hammerjs": {
|
||||||
"version": "2.0.17",
|
"version": "2.0.17",
|
||||||
"resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz",
|
"resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz",
|
||||||
@@ -3211,9 +3224,219 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@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": {
|
"node_modules/@noble/hashes": {
|
||||||
"version": "1.8.0",
|
"version": "1.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
|
"resolved": "https://registry.npmmirror.com/@noble/hashes/-/hashes-1.8.0.tgz",
|
||||||
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
|
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -3801,6 +4024,12 @@
|
|||||||
"@sinonjs/commons": "^3.0.0"
|
"@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": {
|
"node_modules/@types/babel__core": {
|
||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||||
@@ -3923,6 +4152,12 @@
|
|||||||
"integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==",
|
"integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@types/yargs": {
|
||||||
"version": "17.0.35",
|
"version": "17.0.35",
|
||||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz",
|
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz",
|
||||||
@@ -5082,6 +5317,15 @@
|
|||||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/convert-source-map": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||||
@@ -5221,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": {
|
"node_modules/decode-uri-component": {
|
||||||
"version": "0.2.2",
|
"version": "0.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
|
||||||
@@ -5579,6 +5832,21 @@
|
|||||||
"node": ">=6"
|
"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": {
|
"node_modules/exec-async": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/exec-async/-/exec-async-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/exec-async/-/exec-async-2.2.0.tgz",
|
||||||
@@ -5756,6 +6024,18 @@
|
|||||||
"react-native": "*"
|
"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": {
|
"node_modules/expo-font": {
|
||||||
"version": "13.0.4",
|
"version": "13.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/expo-font/-/expo-font-13.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/expo-font/-/expo-font-13.0.4.tgz",
|
||||||
@@ -6775,6 +7055,18 @@
|
|||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/is-number": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||||
@@ -7093,6 +7385,15 @@
|
|||||||
"integrity": "sha512-bF7vcQxbODoGK1imE2P9GS9aw4zD0Sd+Hni68IMZLj7zRnquH7dXUmMw9hDI5S/Jzt7q+IyTXN0rSg2GI0IKhQ==",
|
"integrity": "sha512-bF7vcQxbODoGK1imE2P9GS9aw4zD0Sd+Hni68IMZLj7zRnquH7dXUmMw9hDI5S/Jzt7q+IyTXN0rSg2GI0IKhQ==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
@@ -7213,6 +7514,65 @@
|
|||||||
"node": ">=6"
|
"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": {
|
"node_modules/leven": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
|
||||||
@@ -8299,6 +8659,15 @@
|
|||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/mz": {
|
||||||
"version": "2.7.0",
|
"version": "2.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
|
||||||
@@ -8713,6 +9082,49 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/p-try": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||||
@@ -9063,6 +9475,15 @@
|
|||||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
"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": {
|
"node_modules/progress": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
||||||
@@ -9534,6 +9955,22 @@
|
|||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/readline": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/readline/-/readline-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/readline/-/readline-1.3.0.tgz",
|
||||||
@@ -10080,6 +10517,12 @@
|
|||||||
"integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==",
|
"integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/sisteransi": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
|
||||||
@@ -10234,6 +10677,15 @@
|
|||||||
"node": ">=4"
|
"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": {
|
"node_modules/string-width": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||||
@@ -11011,6 +11463,12 @@
|
|||||||
"integrity": "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==",
|
"integrity": "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/walker": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
|
||||||
@@ -11415,6 +11873,15 @@
|
|||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
11
package.json
11
package.json
@@ -11,6 +11,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/metro-runtime": "~4.0.1",
|
"@expo/metro-runtime": "~4.0.1",
|
||||||
"@expo/vector-icons": "~14.0.4",
|
"@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-native-async-storage/async-storage": "^2.2.0",
|
||||||
"@react-navigation/bottom-tabs": "^6.6.1",
|
"@react-navigation/bottom-tabs": "^6.6.1",
|
||||||
"@react-navigation/native": "^6.1.18",
|
"@react-navigation/native": "^6.1.18",
|
||||||
@@ -20,6 +24,7 @@
|
|||||||
"expo": "~52.0.0",
|
"expo": "~52.0.0",
|
||||||
"expo-asset": "~11.0.5",
|
"expo-asset": "~11.0.5",
|
||||||
"expo-constants": "~17.0.8",
|
"expo-constants": "~17.0.8",
|
||||||
|
"expo-crypto": "~14.0.2",
|
||||||
"expo-font": "~13.0.4",
|
"expo-font": "~13.0.4",
|
||||||
"expo-haptics": "~14.0.0",
|
"expo-haptics": "~14.0.0",
|
||||||
"expo-image-picker": "^17.0.10",
|
"expo-image-picker": "^17.0.10",
|
||||||
@@ -29,11 +34,13 @@
|
|||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"react-native": "^0.76.9",
|
"react-native": "^0.76.9",
|
||||||
"react-native-gesture-handler": "~2.20.2",
|
"react-native-gesture-handler": "~2.20.2",
|
||||||
"react-native-view-shot": "^3.8.0",
|
|
||||||
"react-native-reanimated": "~3.16.1",
|
"react-native-reanimated": "~3.16.1",
|
||||||
"react-native-safe-area-context": "4.12.0",
|
"react-native-safe-area-context": "4.12.0",
|
||||||
"react-native-screens": "~4.4.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": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.25.2",
|
"@babel/core": "^7.25.2",
|
||||||
|
|||||||
@@ -62,20 +62,18 @@ export default function BiometricModal({
|
|||||||
Animated.sequence([
|
Animated.sequence([
|
||||||
Animated.timing(scanAnimation, {
|
Animated.timing(scanAnimation, {
|
||||||
toValue: 1,
|
toValue: 1,
|
||||||
duration: 800,
|
duration: 400,
|
||||||
useNativeDriver: true,
|
useNativeDriver: true,
|
||||||
}),
|
}),
|
||||||
Animated.timing(scanAnimation, {
|
Animated.timing(scanAnimation, {
|
||||||
toValue: 0,
|
toValue: 0,
|
||||||
duration: 800,
|
duration: 400,
|
||||||
useNativeDriver: true,
|
useNativeDriver: true,
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
{ iterations: 2 }
|
{ iterations: 1 }
|
||||||
).start(() => {
|
).start(() => {
|
||||||
setTimeout(() => {
|
|
||||||
onSuccess();
|
onSuccess();
|
||||||
}, 300);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -51,11 +51,13 @@ export const API_ENDPOINTS = {
|
|||||||
CREATE: '/assets/create',
|
CREATE: '/assets/create',
|
||||||
CLAIM: '/assets/claim',
|
CLAIM: '/assets/claim',
|
||||||
ASSIGN: '/assets/assign',
|
ASSIGN: '/assets/assign',
|
||||||
|
DELETE: '/assets/delete',
|
||||||
},
|
},
|
||||||
|
|
||||||
// AI Services
|
// AI Services
|
||||||
AI: {
|
AI: {
|
||||||
PROXY: '/ai/proxy',
|
PROXY: '/ai/proxy',
|
||||||
|
GET_ROLES: '/get_ai_roles',
|
||||||
},
|
},
|
||||||
|
|
||||||
// Admin Operations
|
// Admin Operations
|
||||||
@@ -67,9 +69,9 @@ export const API_ENDPOINTS = {
|
|||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Vault storage (user-isolated, multi-account)
|
// Vault storage (user-isolated, multi-account)
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// - AsyncStorage keys for vault state (S0 share, initialized flag).
|
// - AsyncStorage keys for vault state (S0 share, initialized flag, mnemonic part backup).
|
||||||
// - User-scoped: each account has its own keys so vault state is isolated.
|
// - User-scoped: each account has its own keys so vault/mnemonic state is isolated.
|
||||||
// - Store: use getVaultStorageKeys(userId) and write to INITIALIZED / SHARE_DEVICE.
|
// - 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).
|
// - 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).
|
// - Multi-account: same device, multiple users → each has independent vault (no cross-user leakage).
|
||||||
|
|
||||||
@@ -79,21 +81,30 @@ const VAULT_KEY_PREFIX = 'sentinel_vault';
|
|||||||
export const VAULT_STORAGE_KEYS = {
|
export const VAULT_STORAGE_KEYS = {
|
||||||
INITIALIZED: 'sentinel_vault_initialized',
|
INITIALIZED: 'sentinel_vault_initialized',
|
||||||
SHARE_DEVICE: 'sentinel_vault_s0',
|
SHARE_DEVICE: 'sentinel_vault_s0',
|
||||||
|
MNEMONIC_PART_LOCAL: 'sentinel_mnemonic_part_local',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns vault storage keys for the given user (user isolation).
|
* Returns vault storage keys for the given user (user isolation).
|
||||||
* - Use for: reading S0, writing S0 after mnemonic, clearing on Reset Vault State.
|
* - 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}).
|
* - userId null → guest namespace (_guest). userId set → per-user namespace (_u{userId}).
|
||||||
*/
|
*/
|
||||||
export function getVaultStorageKeys(userId: number | string | null): {
|
export function getVaultStorageKeys(userId: number | string | null): {
|
||||||
INITIALIZED: string;
|
INITIALIZED: string;
|
||||||
SHARE_DEVICE: string;
|
SHARE_DEVICE: string;
|
||||||
|
MNEMONIC_PART_LOCAL: string;
|
||||||
|
AES_KEY: string;
|
||||||
|
SHARE_SERVER: string;
|
||||||
|
SHARE_HEIR: string;
|
||||||
} {
|
} {
|
||||||
const suffix = userId != null ? `_u${userId}` : '_guest';
|
const suffix = userId != null ? `_u${userId}` : '_guest';
|
||||||
return {
|
return {
|
||||||
INITIALIZED: `${VAULT_KEY_PREFIX}_initialized${suffix}`,
|
INITIALIZED: `${VAULT_KEY_PREFIX}_initialized${suffix}`,
|
||||||
SHARE_DEVICE: `${VAULT_KEY_PREFIX}_s0${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}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,9 @@
|
|||||||
|
|
||||||
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
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 { authService } from '../services/auth.service';
|
||||||
|
import { aiService } from '../services/ai.service';
|
||||||
import { storageService } from '../services/storage.service';
|
import { storageService } from '../services/storage.service';
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -18,11 +19,13 @@ import { storageService } from '../services/storage.service';
|
|||||||
interface AuthContextType {
|
interface AuthContextType {
|
||||||
user: User | null;
|
user: User | null;
|
||||||
token: string | null;
|
token: string | null;
|
||||||
|
aiRoles: AIRole[];
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
isInitializing: boolean;
|
isInitializing: boolean;
|
||||||
signIn: (credentials: LoginRequest) => Promise<void>;
|
signIn: (credentials: LoginRequest) => Promise<void>;
|
||||||
signUp: (data: RegisterRequest) => Promise<void>;
|
signUp: (data: RegisterRequest) => Promise<void>;
|
||||||
signOut: () => void;
|
signOut: () => void;
|
||||||
|
refreshAIRoles: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Storage keys
|
// Storage keys
|
||||||
@@ -44,6 +47,7 @@ const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|||||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||||
const [user, setUser] = useState<User | null>(null);
|
const [user, setUser] = useState<User | null>(null);
|
||||||
const [token, setToken] = useState<string | null>(null);
|
const [token, setToken] = useState<string | null>(null);
|
||||||
|
const [aiRoles, setAIRoles] = useState<AIRole[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isInitializing, setIsInitializing] = useState(true);
|
const [isInitializing, setIsInitializing] = useState(true);
|
||||||
|
|
||||||
@@ -66,6 +70,8 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
setToken(storedToken);
|
setToken(storedToken);
|
||||||
setUser(JSON.parse(storedUser));
|
setUser(JSON.parse(storedUser));
|
||||||
console.log('[Auth] Restored session for user:', JSON.parse(storedUser).username);
|
console.log('[Auth] Restored session for user:', JSON.parse(storedUser).username);
|
||||||
|
// Fetch AI roles after restoring session
|
||||||
|
fetchAIRoles(storedToken);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Auth] Failed to load stored auth:', error);
|
console.error('[Auth] Failed to load stored auth:', error);
|
||||||
@@ -74,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
|
* Save authentication to AsyncStorage
|
||||||
*/
|
*/
|
||||||
@@ -114,6 +143,8 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
setToken(response.access_token);
|
setToken(response.access_token);
|
||||||
setUser(response.user);
|
setUser(response.user);
|
||||||
await saveAuth(response.access_token, response.user);
|
await saveAuth(response.access_token, response.user);
|
||||||
|
// Fetch AI roles immediately after login
|
||||||
|
await fetchAIRoles(response.access_token);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -143,7 +174,10 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
const signOut = () => {
|
const signOut = () => {
|
||||||
setUser(null);
|
setUser(null);
|
||||||
setToken(null);
|
setToken(null);
|
||||||
|
setAIRoles([]);
|
||||||
clearAuth();
|
clearAuth();
|
||||||
|
|
||||||
|
|
||||||
//storageService.clearAllData();
|
//storageService.clearAllData();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -152,11 +186,13 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
value={{
|
value={{
|
||||||
user,
|
user,
|
||||||
token,
|
token,
|
||||||
|
aiRoles,
|
||||||
isLoading,
|
isLoading,
|
||||||
isInitializing,
|
isInitializing,
|
||||||
signIn,
|
signIn,
|
||||||
signUp,
|
signUp,
|
||||||
signOut
|
signOut,
|
||||||
|
refreshAIRoles
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -6,9 +6,12 @@
|
|||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import * as bip39 from 'bip39';
|
import * as bip39 from 'bip39';
|
||||||
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import { useAuth } from '../context/AuthContext';
|
import { useAuth } from '../context/AuthContext';
|
||||||
import { assetsService } from '../services/assets.service';
|
import { assetsService } from '../services/assets.service';
|
||||||
import { createAssetPayload } from '../services/vault.service';
|
import { getVaultStorageKeys, DEBUG_MODE } from '../config';
|
||||||
|
import { SentinelVault } from '../utils/crypto_core';
|
||||||
|
import { storageService } from '../services/storage.service';
|
||||||
import {
|
import {
|
||||||
initialVaultAssets,
|
initialVaultAssets,
|
||||||
mapApiAssetsToVaultAssets,
|
mapApiAssetsToVaultAssets,
|
||||||
@@ -35,6 +38,10 @@ export interface UseVaultAssetsReturn {
|
|||||||
refreshAssets: () => Promise<void>;
|
refreshAssets: () => Promise<void>;
|
||||||
/** Create asset via POST /assets/create; on success refreshes list */
|
/** Create asset via POST /assets/create; on success refreshes list */
|
||||||
createAsset: (params: { title: string; content: string }) => Promise<CreateAssetResult>;
|
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 */
|
/** True while create request is in flight */
|
||||||
isSealing: boolean;
|
isSealing: boolean;
|
||||||
/** Error message from last create failure (non-401) */
|
/** Error message from last create failure (non-401) */
|
||||||
@@ -51,7 +58,7 @@ export interface UseVaultAssetsReturn {
|
|||||||
* Vault assets list + create. Fetches on unlock when token exists; keeps mock on error.
|
* Vault assets list + create. Fetches on unlock when token exists; keeps mock on error.
|
||||||
*/
|
*/
|
||||||
export function useVaultAssets(isUnlocked: boolean): UseVaultAssetsReturn {
|
export function useVaultAssets(isUnlocked: boolean): UseVaultAssetsReturn {
|
||||||
const { token, signOut } = useAuth();
|
const { user, token, signOut } = useAuth();
|
||||||
const [assets, setAssets] = useState<VaultAsset[]>(initialVaultAssets);
|
const [assets, setAssets] = useState<VaultAsset[]>(initialVaultAssets);
|
||||||
const [isSealing, setIsSealing] = useState(false);
|
const [isSealing, setIsSealing] = useState(false);
|
||||||
const [createError, setCreateError] = useState<string | null>(null);
|
const [createError, setCreateError] = useState<string | null>(null);
|
||||||
@@ -63,10 +70,14 @@ export function useVaultAssets(isUnlocked: boolean): UseVaultAssetsReturn {
|
|||||||
if (Array.isArray(list)) {
|
if (Array.isArray(list)) {
|
||||||
setAssets(mapApiAssetsToVaultAssets(list as ApiAsset[]));
|
setAssets(mapApiAssetsToVaultAssets(list as ApiAsset[]));
|
||||||
}
|
}
|
||||||
} catch {
|
} 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)
|
// Keep current assets (mock or previous fetch)
|
||||||
}
|
}
|
||||||
}, [token]);
|
}, [token, signOut]);
|
||||||
|
|
||||||
// Fetch list when unlocked and token exists
|
// Fetch list when unlocked and token exists
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -79,7 +90,13 @@ export function useVaultAssets(isUnlocked: boolean): UseVaultAssetsReturn {
|
|||||||
setAssets(mapApiAssetsToVaultAssets(list as ApiAsset[]));
|
setAssets(mapApiAssetsToVaultAssets(list as ApiAsset[]));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.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
|
// Keep initial (mock) assets
|
||||||
});
|
});
|
||||||
return () => {
|
return () => {
|
||||||
@@ -101,22 +118,45 @@ export function useVaultAssets(isUnlocked: boolean): UseVaultAssetsReturn {
|
|||||||
setIsSealing(true);
|
setIsSealing(true);
|
||||||
setCreateError(null);
|
setCreateError(null);
|
||||||
try {
|
try {
|
||||||
const wordList = bip39.wordlists.english;
|
const vaultKeys = getVaultStorageKeys(user?.id ?? null);
|
||||||
const payload = await createAssetPayload(
|
const [s1Str, aesKeyHex, s0Str, s2Str] = await Promise.all([
|
||||||
title.trim(),
|
AsyncStorage.getItem(vaultKeys.SHARE_SERVER),
|
||||||
content.trim(),
|
AsyncStorage.getItem(vaultKeys.AES_KEY),
|
||||||
wordList,
|
AsyncStorage.getItem(vaultKeys.SHARE_DEVICE),
|
||||||
'note',
|
AsyncStorage.getItem(vaultKeys.SHARE_HEIR),
|
||||||
0
|
]);
|
||||||
);
|
|
||||||
await assetsService.createAsset(
|
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: payload.title,
|
title: title.trim(),
|
||||||
private_key_shard: payload.private_key_shard,
|
private_key_shard: s1Str,
|
||||||
content_inner_encrypted: payload.content_inner_encrypted,
|
content_inner_encrypted,
|
||||||
},
|
},
|
||||||
token
|
token
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Backup plaintext content locally
|
||||||
|
if (createdAsset && createdAsset.id && user?.id) {
|
||||||
|
await storageService.saveAssetBackup(createdAsset.id, content, user.id);
|
||||||
|
}
|
||||||
await refreshAssets();
|
await refreshAssets();
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
@@ -143,9 +183,85 @@ export function useVaultAssets(isUnlocked: boolean): UseVaultAssetsReturn {
|
|||||||
setIsSealing(false);
|
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]
|
[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), []);
|
const clearCreateError = useCallback(() => setCreateError(null), []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -153,6 +269,8 @@ export function useVaultAssets(isUnlocked: boolean): UseVaultAssetsReturn {
|
|||||||
setAssets,
|
setAssets,
|
||||||
refreshAssets,
|
refreshAssets,
|
||||||
createAsset,
|
createAsset,
|
||||||
|
deleteAsset,
|
||||||
|
assignAsset,
|
||||||
isSealing,
|
isSealing,
|
||||||
createError,
|
createError,
|
||||||
clearCreateError,
|
clearCreateError,
|
||||||
|
|||||||
45
src/polyfills.ts
Normal file
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,11 +28,18 @@ import {
|
|||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import { Ionicons, Feather, FontAwesome5 } from '@expo/vector-icons';
|
import { Ionicons, Feather, FontAwesome5 } from '@expo/vector-icons';
|
||||||
import * as ImagePicker from 'expo-image-picker';
|
import * as ImagePicker from 'expo-image-picker';
|
||||||
|
import { AIRole } from '../types';
|
||||||
import { colors, typography, spacing, borderRadius, shadows } from '../theme/colors';
|
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 { useAuth } from '../context/AuthContext';
|
||||||
import { AI_CONFIG } from '../config';
|
import { AI_CONFIG, getVaultStorageKeys } from '../config';
|
||||||
import { storageService } from '../services/storage.service';
|
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
|
// Type Definitions
|
||||||
@@ -59,7 +66,7 @@ interface ChatSession {
|
|||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
export default function FlowScreen() {
|
export default function FlowScreen() {
|
||||||
const { token, user, signOut } = useAuth();
|
const { token, user, signOut, aiRoles, refreshAIRoles } = useAuth();
|
||||||
const scrollViewRef = useRef<ScrollView>(null);
|
const scrollViewRef = useRef<ScrollView>(null);
|
||||||
|
|
||||||
// Current conversation state
|
// Current conversation state
|
||||||
@@ -67,10 +74,11 @@ export default function FlowScreen() {
|
|||||||
const [newContent, setNewContent] = useState('');
|
const [newContent, setNewContent] = useState('');
|
||||||
const [isSending, setIsSending] = useState(false);
|
const [isSending, setIsSending] = useState(false);
|
||||||
const [isRecording, setIsRecording] = useState(false);
|
const [isRecording, setIsRecording] = useState(false);
|
||||||
const [selectedImage, setSelectedImage] = useState<string | null>(null);
|
/** Attached image for next send (uri + base64); user can add optional text then send together */
|
||||||
|
const [attachedImage, setAttachedImage] = useState<{ uri: string; base64: string } | null>(null);
|
||||||
|
|
||||||
// AI Role state
|
// AI Role state - start with null to detect first load
|
||||||
const [selectedRole, setSelectedRole] = useState(AI_CONFIG.ROLES[0]);
|
const [selectedRole, setSelectedRole] = useState<AIRole | null>(aiRoles[0] || null);
|
||||||
const [showRoleModal, setShowRoleModal] = useState(false);
|
const [showRoleModal, setShowRoleModal] = useState(false);
|
||||||
const [expandedRoleId, setExpandedRoleId] = useState<string | null>(null);
|
const [expandedRoleId, setExpandedRoleId] = useState<string | null>(null);
|
||||||
|
|
||||||
@@ -78,6 +86,18 @@ export default function FlowScreen() {
|
|||||||
const [showHistoryModal, setShowHistoryModal] = useState(false);
|
const [showHistoryModal, setShowHistoryModal] = useState(false);
|
||||||
const modalSlideAnim = useRef(new Animated.Value(0)).current;
|
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[]>([
|
const [chatHistory, setChatHistory] = useState<ChatSession[]>([
|
||||||
// Sample history data
|
// Sample history data
|
||||||
{
|
{
|
||||||
@@ -143,9 +163,9 @@ export default function FlowScreen() {
|
|||||||
// Load messages whenever role changes
|
// Load messages whenever role changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadRoleMessages = async () => {
|
const loadRoleMessages = async () => {
|
||||||
if (!user) return;
|
if (!user || !selectedRole) return;
|
||||||
try {
|
try {
|
||||||
const savedMessages = await storageService.getCurrentChat(selectedRole.id, user.id);
|
const savedMessages = await storageService.getCurrentChat(selectedRole?.id || '', user.id);
|
||||||
if (savedMessages) {
|
if (savedMessages) {
|
||||||
const formattedMessages = savedMessages.map((msg: any) => ({
|
const formattedMessages = savedMessages.map((msg: any) => ({
|
||||||
...msg,
|
...msg,
|
||||||
@@ -156,18 +176,42 @@ export default function FlowScreen() {
|
|||||||
setMessages([]);
|
setMessages([]);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to load messages for role ${selectedRole.id}:`, error);
|
if (selectedRole) {
|
||||||
|
console.error(`Failed to load messages for role ${selectedRole?.id}:`, error);
|
||||||
|
}
|
||||||
setMessages([]);
|
setMessages([]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadRoleMessages();
|
loadRoleMessages();
|
||||||
}, [selectedRole.id, user]);
|
}, [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
|
// Save current messages for the active role when they change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user && messages.length >= 0) { // Save even if empty to allow clearing
|
if (user && selectedRole && messages.length >= 0) { // Save even if empty to allow clearing
|
||||||
storageService.saveCurrentChat(selectedRole.id, messages, user.id);
|
storageService.saveCurrentChat(selectedRole?.id || '', messages, user.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (messages.length > 0) {
|
if (messages.length > 0) {
|
||||||
@@ -175,7 +219,7 @@ export default function FlowScreen() {
|
|||||||
scrollViewRef.current?.scrollToEnd({ animated: true });
|
scrollViewRef.current?.scrollToEnd({ animated: true });
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
}, [messages, selectedRole.id, user]);
|
}, [messages, selectedRole?.id, user]);
|
||||||
|
|
||||||
// Save history when it changes
|
// Save history when it changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -210,10 +254,12 @@ export default function FlowScreen() {
|
|||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle sending a message to AI
|
* Handle sending a message to AI (text-only via LangGraph, or image + optional text via vision API)
|
||||||
*/
|
*/
|
||||||
const handleSendMessage = async () => {
|
const handleSendMessage = async () => {
|
||||||
if (!newContent.trim() || isSending) return;
|
const hasText = !!newContent.trim();
|
||||||
|
const hasImage = !!attachedImage;
|
||||||
|
if ((!hasText && !hasImage) || isSending || !selectedRole) return;
|
||||||
|
|
||||||
// Check authentication
|
// Check authentication
|
||||||
if (!token) {
|
if (!token) {
|
||||||
@@ -225,11 +271,64 @@ export default function FlowScreen() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const userMessage = newContent.trim();
|
|
||||||
setIsSending(true);
|
setIsSending(true);
|
||||||
|
|
||||||
|
// --- Path: send with image (optional text) ---
|
||||||
|
if (hasImage && attachedImage) {
|
||||||
|
const imageUri = attachedImage.uri;
|
||||||
|
const imageBase64 = attachedImage.base64;
|
||||||
|
const userText = newContent.trim() || '请描述或分析这张图片';
|
||||||
|
setAttachedImage(null);
|
||||||
|
setNewContent('');
|
||||||
|
|
||||||
|
const userMsg: ChatMessage = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
role: 'user',
|
||||||
|
content: userText,
|
||||||
|
imageUri,
|
||||||
|
createdAt: new Date(),
|
||||||
|
};
|
||||||
|
setMessages(prev => [...prev, userMsg]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const aiResponse = await aiService.sendMessageWithImage(userText, imageBase64, token);
|
||||||
|
const aiMsg: ChatMessage = {
|
||||||
|
id: (Date.now() + 1).toString(),
|
||||||
|
role: 'assistant',
|
||||||
|
content: aiResponse,
|
||||||
|
createdAt: new Date(),
|
||||||
|
};
|
||||||
|
setMessages(prev => [...prev, aiMsg]);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('AI image request failed:', error);
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
const isAuthError =
|
||||||
|
errorMessage.includes('401') ||
|
||||||
|
errorMessage.includes('Unauthorized') ||
|
||||||
|
errorMessage.includes('credentials') ||
|
||||||
|
errorMessage.includes('validate');
|
||||||
|
if (isAuthError) {
|
||||||
|
signOut();
|
||||||
|
Alert.alert('Session Expired', 'Your login session has expired. Please login again.', [{ text: 'OK' }]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const errorMsg: ChatMessage = {
|
||||||
|
id: (Date.now() + 1).toString(),
|
||||||
|
role: 'assistant',
|
||||||
|
content: `⚠️ Error: ${errorMessage}`,
|
||||||
|
createdAt: new Date(),
|
||||||
|
};
|
||||||
|
setMessages(prev => [...prev, errorMsg]);
|
||||||
|
} finally {
|
||||||
|
setIsSending(false);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Path: text-only via LangGraph (unchanged) ---
|
||||||
|
const userMessage = newContent.trim();
|
||||||
setNewContent('');
|
setNewContent('');
|
||||||
|
|
||||||
// Add user message immediately
|
|
||||||
const userMsg: ChatMessage = {
|
const userMsg: ChatMessage = {
|
||||||
id: Date.now().toString(),
|
id: Date.now().toString(),
|
||||||
role: 'user',
|
role: 'user',
|
||||||
@@ -239,10 +338,15 @@ export default function FlowScreen() {
|
|||||||
setMessages(prev => [...prev, userMsg]);
|
setMessages(prev => [...prev, userMsg]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Call AI proxy with selected role's system prompt
|
const history: (HumanMessage | LangChainAIMessage | SystemMessage)[] = messages.map(msg => {
|
||||||
const aiResponse = await aiService.sendMessage(userMessage, token, selectedRole.systemPrompt);
|
if (msg.role === 'user') return new HumanMessage(msg.content);
|
||||||
|
return new LangChainAIMessage(msg.content);
|
||||||
|
});
|
||||||
|
const systemPrompt = new SystemMessage(selectedRole?.systemPrompt || '');
|
||||||
|
const currentMsg = new HumanMessage(userMessage);
|
||||||
|
const fullMessages = [systemPrompt, ...history, currentMsg];
|
||||||
|
const aiResponse = await langGraphService.execute(fullMessages, token);
|
||||||
|
|
||||||
// Add AI response
|
|
||||||
const aiMsg: ChatMessage = {
|
const aiMsg: ChatMessage = {
|
||||||
id: (Date.now() + 1).toString(),
|
id: (Date.now() + 1).toString(),
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
@@ -250,20 +354,15 @@ export default function FlowScreen() {
|
|||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
};
|
};
|
||||||
setMessages(prev => [...prev, aiMsg]);
|
setMessages(prev => [...prev, aiMsg]);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('AI request failed:', error);
|
console.error('AI request failed:', error);
|
||||||
|
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
|
||||||
// Handle authentication errors (401, credentials, unauthorized)
|
|
||||||
const isAuthError =
|
const isAuthError =
|
||||||
errorMessage.includes('401') ||
|
errorMessage.includes('401') ||
|
||||||
errorMessage.includes('credentials') ||
|
errorMessage.includes('credentials') ||
|
||||||
errorMessage.includes('Unauthorized') ||
|
errorMessage.includes('Unauthorized') ||
|
||||||
errorMessage.includes('Not authenticated') ||
|
errorMessage.includes('Not authenticated') ||
|
||||||
errorMessage.includes('validate');
|
errorMessage.includes('validate');
|
||||||
|
|
||||||
if (isAuthError) {
|
if (isAuthError) {
|
||||||
signOut();
|
signOut();
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
@@ -273,8 +372,6 @@ export default function FlowScreen() {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show error as AI message
|
|
||||||
const errorMsg: ChatMessage = {
|
const errorMsg: ChatMessage = {
|
||||||
id: (Date.now() + 1).toString(),
|
id: (Date.now() + 1).toString(),
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
@@ -296,17 +393,15 @@ export default function FlowScreen() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle image attachment - pick image and analyze with AI
|
* Handle image attachment - pick image and attach to next message (user can add text then send)
|
||||||
*/
|
*/
|
||||||
const handleAddImage = async () => {
|
const handleAddImage = async () => {
|
||||||
// Request permission
|
|
||||||
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
|
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
|
||||||
if (status !== 'granted') {
|
if (status !== 'granted') {
|
||||||
Alert.alert('Permission Required', 'Please grant permission to access photos');
|
Alert.alert('Permission Required', 'Please grant permission to access photos');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pick image
|
|
||||||
const result = await ImagePicker.launchImageLibraryAsync({
|
const result = await ImagePicker.launchImageLibraryAsync({
|
||||||
mediaTypes: ImagePicker.MediaTypeOptions.Images,
|
mediaTypes: ImagePicker.MediaTypeOptions.Images,
|
||||||
allowsEditing: true,
|
allowsEditing: true,
|
||||||
@@ -315,78 +410,11 @@ export default function FlowScreen() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!result.canceled && result.assets[0]) {
|
if (!result.canceled && result.assets[0]) {
|
||||||
const imageAsset = result.assets[0];
|
const asset = result.assets[0];
|
||||||
setSelectedImage(imageAsset.uri);
|
setAttachedImage({
|
||||||
|
uri: asset.uri,
|
||||||
// Check authentication
|
base64: asset.base64 || '',
|
||||||
if (!token) {
|
});
|
||||||
Alert.alert(
|
|
||||||
'Login Required',
|
|
||||||
'Please login to analyze images',
|
|
||||||
[{ text: 'OK', onPress: () => signOut() }]
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsSending(true);
|
|
||||||
|
|
||||||
// Add user message with image
|
|
||||||
const userMsg: ChatMessage = {
|
|
||||||
id: Date.now().toString(),
|
|
||||||
role: 'user',
|
|
||||||
content: 'Analyze this image',
|
|
||||||
imageUri: imageAsset.uri,
|
|
||||||
createdAt: new Date(),
|
|
||||||
};
|
|
||||||
setMessages(prev => [...prev, userMsg]);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Call AI with image (using base64)
|
|
||||||
const aiResponse = await aiService.sendMessageWithImage(
|
|
||||||
'Please describe and analyze this image in detail.',
|
|
||||||
imageAsset.base64 || '',
|
|
||||||
token
|
|
||||||
);
|
|
||||||
|
|
||||||
const aiMsg: ChatMessage = {
|
|
||||||
id: (Date.now() + 1).toString(),
|
|
||||||
role: 'assistant',
|
|
||||||
content: aiResponse,
|
|
||||||
createdAt: new Date(),
|
|
||||||
};
|
|
||||||
setMessages(prev => [...prev, aiMsg]);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('AI image analysis failed:', error);
|
|
||||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
||||||
|
|
||||||
// Handle authentication errors
|
|
||||||
const isAuthError =
|
|
||||||
errorMessage.includes('401') ||
|
|
||||||
errorMessage.includes('Unauthorized') ||
|
|
||||||
errorMessage.includes('credentials') ||
|
|
||||||
errorMessage.includes('validate');
|
|
||||||
|
|
||||||
if (isAuthError) {
|
|
||||||
signOut();
|
|
||||||
Alert.alert(
|
|
||||||
'Session Expired',
|
|
||||||
'Your login session has expired. Please login again.',
|
|
||||||
[{ text: 'OK' }]
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const errorMsg: ChatMessage = {
|
|
||||||
id: (Date.now() + 1).toString(),
|
|
||||||
role: 'assistant',
|
|
||||||
content: `⚠️ Error analyzing image: ${errorMessage}`,
|
|
||||||
createdAt: new Date(),
|
|
||||||
};
|
|
||||||
setMessages(prev => [...prev, errorMsg]);
|
|
||||||
} finally {
|
|
||||||
setIsSending(false);
|
|
||||||
setSelectedImage(null);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -408,8 +436,8 @@ export default function FlowScreen() {
|
|||||||
|
|
||||||
// Clear current messages and storage for this role
|
// Clear current messages and storage for this role
|
||||||
setMessages([]);
|
setMessages([]);
|
||||||
if (user) {
|
if (user && selectedRole) {
|
||||||
storageService.saveCurrentChat(selectedRole.id, [], user.id);
|
storageService.saveCurrentChat(selectedRole?.id || '', [], user.id);
|
||||||
}
|
}
|
||||||
closeHistoryModal();
|
closeHistoryModal();
|
||||||
};
|
};
|
||||||
@@ -453,6 +481,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
|
// Helper Functions
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -525,9 +659,9 @@ export default function FlowScreen() {
|
|||||||
<View style={styles.emptyIcon}>
|
<View style={styles.emptyIcon}>
|
||||||
<Feather name="feather" size={48} color={colors.nautical.seafoam} />
|
<Feather name="feather" size={48} color={colors.nautical.seafoam} />
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.emptyTitle}>Chatting with {selectedRole.name}</Text>
|
<Text style={styles.emptyTitle}>Chatting with {selectedRole?.name || 'AI'}</Text>
|
||||||
<Text style={styles.emptySubtitle}>
|
<Text style={styles.emptySubtitle}>
|
||||||
{selectedRole.description}
|
{selectedRole?.description || 'Loading AI Assistant...'}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@@ -585,17 +719,32 @@ export default function FlowScreen() {
|
|||||||
onPress={() => setShowRoleModal(true)}
|
onPress={() => setShowRoleModal(true)}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
|
{selectedRole && (
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name={selectedRole.icon as any}
|
name={(selectedRole?.icon || 'help-outline') as any}
|
||||||
size={16}
|
size={16}
|
||||||
color={colors.nautical.teal}
|
color={colors.nautical.teal}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
<Text style={styles.headerRoleText} numberOfLines={1}>
|
<Text style={styles.headerRoleText} numberOfLines={1}>
|
||||||
{selectedRole.name}
|
{selectedRole?.name || 'Loading...'}
|
||||||
</Text>
|
</Text>
|
||||||
<Ionicons name="chevron-down" size={14} color={colors.flow.textSecondary} />
|
<Ionicons name="chevron-down" size={14} color={colors.flow.textSecondary} />
|
||||||
</TouchableOpacity>
|
</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 */}
|
{/* History Button */}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.historyButton}
|
style={styles.historyButton}
|
||||||
@@ -638,21 +787,35 @@ export default function FlowScreen() {
|
|||||||
|
|
||||||
{/* Bottom Input Bar */}
|
{/* Bottom Input Bar */}
|
||||||
<View style={styles.inputBarContainer}>
|
<View style={styles.inputBarContainer}>
|
||||||
|
{/* Attached image preview (optional text then send) */}
|
||||||
|
{attachedImage && (
|
||||||
|
<View style={styles.attachedImageRow}>
|
||||||
|
<Image source={{ uri: attachedImage.uri }} style={styles.attachedImageThumb} resizeMode="cover" />
|
||||||
|
<Text style={styles.attachedImageHint} numberOfLines={1}>可输入文字后发送</Text>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.attachedImageRemove}
|
||||||
|
onPress={() => setAttachedImage(null)}
|
||||||
|
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
||||||
|
>
|
||||||
|
<Ionicons name="close-circle" size={24} color={colors.flow.textSecondary} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
<View style={styles.inputBar}>
|
<View style={styles.inputBar}>
|
||||||
{/* Image attachment button */}
|
{/* Image attachment button */}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.inputBarButton}
|
style={[styles.inputBarButton, attachedImage && styles.inputBarButtonActive]}
|
||||||
onPress={handleAddImage}
|
onPress={handleAddImage}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<Feather name="image" size={22} color={colors.flow.textSecondary} />
|
<Feather name="image" size={22} color={attachedImage ? colors.nautical.teal : colors.flow.textSecondary} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
{/* Text Input */}
|
{/* Text Input */}
|
||||||
<View style={styles.inputWrapper}>
|
<View style={styles.inputWrapper}>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={styles.inputBarText}
|
style={styles.inputBarText}
|
||||||
placeholder="Message..."
|
placeholder={attachedImage ? '输入对图片的说明(可选)...' : 'Message...'}
|
||||||
placeholderTextColor={colors.flow.textSecondary}
|
placeholderTextColor={colors.flow.textSecondary}
|
||||||
value={newContent}
|
value={newContent}
|
||||||
onChangeText={setNewContent}
|
onChangeText={setNewContent}
|
||||||
@@ -661,8 +824,8 @@ export default function FlowScreen() {
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Send or Voice button */}
|
{/* Send or Voice button: show send when has text or attached image */}
|
||||||
{newContent.trim() || isSending ? (
|
{newContent.trim() || attachedImage || isSending ? (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.sendButton, isSending && styles.sendButtonDisabled]}
|
style={[styles.sendButton, isSending && styles.sendButtonDisabled]}
|
||||||
onPress={handleSendMessage}
|
onPress={handleSendMessage}
|
||||||
@@ -776,34 +939,34 @@ export default function FlowScreen() {
|
|||||||
<Text style={styles.modalTitle}>Choose AI Assistant</Text>
|
<Text style={styles.modalTitle}>Choose AI Assistant</Text>
|
||||||
|
|
||||||
<ScrollView style={styles.roleList} showsVerticalScrollIndicator={false}>
|
<ScrollView style={styles.roleList} showsVerticalScrollIndicator={false}>
|
||||||
{AI_CONFIG.ROLES.map((role) => (
|
{aiRoles.map((role) => (
|
||||||
<View key={role.id} style={styles.roleItemContainer}>
|
<View key={role.id} style={styles.roleItemContainer}>
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
styles.roleItem,
|
styles.roleItem,
|
||||||
selectedRole.id === role.id && styles.roleItemActive
|
selectedRole?.id === role.id && styles.roleItemActive
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.roleSelectionArea}
|
style={styles.roleSelectionArea}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setSelectedRole(role as any);
|
setSelectedRole(role);
|
||||||
setShowRoleModal(false);
|
setShowRoleModal(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View style={[
|
<View style={[
|
||||||
styles.roleItemIcon,
|
styles.roleItemIcon,
|
||||||
selectedRole.id === role.id && styles.roleItemIconActive
|
selectedRole?.id === role.id && styles.roleItemIconActive
|
||||||
]}>
|
]}>
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name={role.icon as any}
|
name={role.icon as any}
|
||||||
size={20}
|
size={20}
|
||||||
color={selectedRole.id === role.id ? '#fff' : colors.nautical.teal}
|
color={selectedRole?.id === role.id ? '#fff' : colors.nautical.teal}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<Text style={[
|
<Text style={[
|
||||||
styles.roleItemName,
|
styles.roleItemName,
|
||||||
selectedRole.id === role.id && styles.roleItemNameActive
|
selectedRole?.id === role.id && styles.roleItemNameActive
|
||||||
]}>
|
]}>
|
||||||
{role.name}
|
{role.name}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -843,6 +1006,212 @@ export default function FlowScreen() {
|
|||||||
</View>
|
</View>
|
||||||
</TouchableWithoutFeedback>
|
</TouchableWithoutFeedback>
|
||||||
</Modal>
|
</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>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1129,6 +1498,33 @@ const styles = StyleSheet.create({
|
|||||||
paddingTop: spacing.sm,
|
paddingTop: spacing.sm,
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
},
|
},
|
||||||
|
attachedImageRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: colors.flow.cardBackground,
|
||||||
|
borderRadius: borderRadius.lg,
|
||||||
|
padding: spacing.sm,
|
||||||
|
marginBottom: spacing.sm,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.flow.cardBorder,
|
||||||
|
gap: spacing.sm,
|
||||||
|
},
|
||||||
|
attachedImageThumb: {
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
borderRadius: borderRadius.md,
|
||||||
|
},
|
||||||
|
attachedImageHint: {
|
||||||
|
flex: 1,
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
color: colors.flow.textSecondary,
|
||||||
|
},
|
||||||
|
attachedImageRemove: {
|
||||||
|
padding: spacing.xs,
|
||||||
|
},
|
||||||
|
inputBarButtonActive: {
|
||||||
|
backgroundColor: colors.nautical.paleAqua,
|
||||||
|
},
|
||||||
inputBar: {
|
inputBar: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'flex-end',
|
alignItems: 'flex-end',
|
||||||
@@ -1281,4 +1677,101 @@ const styles = StyleSheet.create({
|
|||||||
color: colors.flow.textSecondary,
|
color: colors.flow.textSecondary,
|
||||||
fontWeight: '600',
|
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',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -315,6 +315,7 @@ export default function MeScreen() {
|
|||||||
await AsyncStorage.multiRemove([
|
await AsyncStorage.multiRemove([
|
||||||
vaultKeys.INITIALIZED,
|
vaultKeys.INITIALIZED,
|
||||||
vaultKeys.SHARE_DEVICE,
|
vaultKeys.SHARE_DEVICE,
|
||||||
|
vaultKeys.MNEMONIC_PART_LOCAL,
|
||||||
]);
|
]);
|
||||||
setResetVaultFeedback({
|
setResetVaultFeedback({
|
||||||
status: 'success',
|
status: 'success',
|
||||||
|
|||||||
@@ -25,8 +25,10 @@ import { VaultAsset, VaultAssetType, Heir } from '../types';
|
|||||||
import BiometricModal from '../components/common/BiometricModal';
|
import BiometricModal from '../components/common/BiometricModal';
|
||||||
import { useAuth } from '../context/AuthContext';
|
import { useAuth } from '../context/AuthContext';
|
||||||
import { useVaultAssets } from '../hooks/useVaultAssets';
|
import { useVaultAssets } from '../hooks/useVaultAssets';
|
||||||
import { getVaultStorageKeys } from '../config';
|
import { getVaultStorageKeys, DEBUG_MODE } from '../config';
|
||||||
import { mnemonicToEntropy, splitSecret, serializeShare } from '../utils/sss';
|
import { mnemonicToEntropy, splitSecret, serializeShare } from '../utils/sss';
|
||||||
|
import { storageService } from '../services/storage.service';
|
||||||
|
import { SentinelVault } from '@/utils/crypto_core';
|
||||||
|
|
||||||
// Asset type configuration with nautical theme
|
// Asset type configuration with nautical theme
|
||||||
const assetTypeConfig: Record<VaultAssetType, { icon: string; iconType: 'ionicons' | 'feather' | 'material' | 'fontawesome5'; label: string }> = {
|
const assetTypeConfig: Record<VaultAssetType, { icon: string; iconType: 'ionicons' | 'feather' | 'material' | 'fontawesome5'; label: string }> = {
|
||||||
@@ -91,41 +93,6 @@ type HeirAssignment = {
|
|||||||
heir: Heir;
|
heir: Heir;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mock data
|
|
||||||
// const initialAssets: VaultAsset[] = [
|
|
||||||
// {
|
|
||||||
// id: '1',
|
|
||||||
// type: 'private_key',
|
|
||||||
// label: 'ETH Main Wallet Key',
|
|
||||||
// createdAt: new Date('2024-01-10'),
|
|
||||||
// updatedAt: new Date('2024-01-10'),
|
|
||||||
// isEncrypted: true,
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// id: '2',
|
|
||||||
// type: 'game_account',
|
|
||||||
// label: 'Steam Account Credentials',
|
|
||||||
// createdAt: new Date('2024-01-08'),
|
|
||||||
// updatedAt: new Date('2024-01-08'),
|
|
||||||
// isEncrypted: true,
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// id: '3',
|
|
||||||
// type: 'document',
|
|
||||||
// label: 'Insurance Policy Scan',
|
|
||||||
// createdAt: new Date('2024-01-05'),
|
|
||||||
// updatedAt: new Date('2024-01-05'),
|
|
||||||
// isEncrypted: true,
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// id: '4',
|
|
||||||
// type: 'will',
|
|
||||||
// label: 'Testament Draft v2',
|
|
||||||
// createdAt: new Date('2024-01-02'),
|
|
||||||
// updatedAt: new Date('2024-01-15'),
|
|
||||||
// isEncrypted: true,
|
|
||||||
// },
|
|
||||||
// ];
|
|
||||||
|
|
||||||
const renderAssetTypeIcon = (config: typeof assetTypeConfig[VaultAssetType], size: number, color: string) => {
|
const renderAssetTypeIcon = (config: typeof assetTypeConfig[VaultAssetType], size: number, color: string) => {
|
||||||
switch (config.iconType) {
|
switch (config.iconType) {
|
||||||
@@ -148,11 +115,15 @@ export default function VaultScreen() {
|
|||||||
setAssets,
|
setAssets,
|
||||||
refreshAssets,
|
refreshAssets,
|
||||||
createAsset: createVaultAsset,
|
createAsset: createVaultAsset,
|
||||||
|
deleteAsset: deleteVaultAsset,
|
||||||
|
assignAsset: assignVaultAsset,
|
||||||
isSealing,
|
isSealing,
|
||||||
createError: addError,
|
createError: addError,
|
||||||
clearCreateError: clearAddError,
|
clearCreateError: clearAddError,
|
||||||
} = useVaultAssets(isUnlocked);
|
} = useVaultAssets(isUnlocked);
|
||||||
const [showAddModal, setShowAddModal] = useState(false);
|
const [showAddModal, setShowAddModal] = useState(false);
|
||||||
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||||
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
const [selectedType, setSelectedType] = useState<VaultAssetType>('custom');
|
const [selectedType, setSelectedType] = useState<VaultAssetType>('custom');
|
||||||
const [newLabel, setNewLabel] = useState('');
|
const [newLabel, setNewLabel] = useState('');
|
||||||
const [showUploadSuccess, setShowUploadSuccess] = useState(false);
|
const [showUploadSuccess, setShowUploadSuccess] = useState(false);
|
||||||
@@ -169,8 +140,15 @@ export default function VaultScreen() {
|
|||||||
const [showAddBiometric, setShowAddBiometric] = useState(false);
|
const [showAddBiometric, setShowAddBiometric] = useState(false);
|
||||||
const [accountProvider, setAccountProvider] = useState<'bank' | 'steam' | 'facebook' | 'custom'>('bank');
|
const [accountProvider, setAccountProvider] = useState<'bank' | 'steam' | 'facebook' | 'custom'>('bank');
|
||||||
const [showMnemonic, setShowMnemonic] = useState(false);
|
const [showMnemonic, setShowMnemonic] = useState(false);
|
||||||
|
const [showAssignModal, setShowAssignModal] = useState(false);
|
||||||
|
const [showAssignErrorModal, setShowAssignErrorModal] = useState(false);
|
||||||
|
const [assignErrorMessage, setAssignErrorMessage] = useState('');
|
||||||
|
const [isAssigning, setIsAssigning] = useState(false);
|
||||||
|
const [heirEmail, setHeirEmail] = useState('');
|
||||||
const [showLegacyAssignCta, setShowLegacyAssignCta] = useState(false);
|
const [showLegacyAssignCta, setShowLegacyAssignCta] = useState(false);
|
||||||
const [hasS0, setHasS0] = useState<boolean | null>(null);
|
const [hasS0, setHasS0] = useState<boolean | null>(null);
|
||||||
|
const [backupContent, setBackupContent] = useState<string | null>(null);
|
||||||
|
const [isFetchingBackup, setIsFetchingBackup] = useState(false);
|
||||||
const [mnemonicWords, setMnemonicWords] = useState<string[]>([]);
|
const [mnemonicWords, setMnemonicWords] = useState<string[]>([]);
|
||||||
const [mnemonicParts, setMnemonicParts] = useState<string[][]>([]);
|
const [mnemonicParts, setMnemonicParts] = useState<string[][]>([]);
|
||||||
const [mnemonicStep, setMnemonicStep] = useState<1 | 2 | 3 | 4 | 5>(1);
|
const [mnemonicStep, setMnemonicStep] = useState<1 | 2 | 3 | 4 | 5>(1);
|
||||||
@@ -210,7 +188,7 @@ export default function VaultScreen() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isUnlocked) return;
|
if (isUnlocked) return;
|
||||||
const timer = setTimeout(() => setShowBiometric(true), 500);
|
const timer = setTimeout(() => setShowBiometric(true), 100);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [isUnlocked, hasS0]);
|
}, [isUnlocked, hasS0]);
|
||||||
|
|
||||||
@@ -218,7 +196,7 @@ export default function VaultScreen() {
|
|||||||
if (isUnlocked) {
|
if (isUnlocked) {
|
||||||
Animated.timing(fadeAnim, {
|
Animated.timing(fadeAnim, {
|
||||||
toValue: 1,
|
toValue: 1,
|
||||||
duration: 600,
|
duration: 200,
|
||||||
useNativeDriver: true,
|
useNativeDriver: true,
|
||||||
}).start();
|
}).start();
|
||||||
}
|
}
|
||||||
@@ -230,12 +208,12 @@ export default function VaultScreen() {
|
|||||||
Animated.sequence([
|
Animated.sequence([
|
||||||
Animated.timing(pulseAnim, {
|
Animated.timing(pulseAnim, {
|
||||||
toValue: 1.05,
|
toValue: 1.05,
|
||||||
duration: 1500,
|
duration: 500,
|
||||||
useNativeDriver: true,
|
useNativeDriver: true,
|
||||||
}),
|
}),
|
||||||
Animated.timing(pulseAnim, {
|
Animated.timing(pulseAnim, {
|
||||||
toValue: 1,
|
toValue: 1,
|
||||||
duration: 1500,
|
duration: 500,
|
||||||
useNativeDriver: true,
|
useNativeDriver: true,
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
@@ -258,7 +236,7 @@ export default function VaultScreen() {
|
|||||||
setProgressIndex(0);
|
setProgressIndex(0);
|
||||||
progressAnim.setValue(0);
|
progressAnim.setValue(0);
|
||||||
setTimeout(() => setShowMnemonic(true), 200);
|
setTimeout(() => setShowMnemonic(true), 200);
|
||||||
AsyncStorage.setItem('sentinel_mnemonic_part_local', parts[0].join(' ')).catch(() => {
|
AsyncStorage.setItem(vaultKeys.MNEMONIC_PART_LOCAL, parts[0].join(' ')).catch(() => {
|
||||||
// Best-effort local store; UI remains available
|
// Best-effort local store; UI remains available
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -380,9 +358,19 @@ export default function VaultScreen() {
|
|||||||
const entropy = mnemonicToEntropy(mnemonicWords, wordList);
|
const entropy = mnemonicToEntropy(mnemonicWords, wordList);
|
||||||
const shares = splitSecret(entropy);
|
const shares = splitSecret(entropy);
|
||||||
const s0 = shares[0]; // device share (S0)
|
const s0 = shares[0]; // device share (S0)
|
||||||
|
const s1 = shares[1]; // server share (S1)
|
||||||
|
const s2 = shares[2]; // heir share (S2)
|
||||||
// S0 is stored in AsyncStorage under user-scoped key — app-level storage, not hardware TEE/SE
|
// S0 is stored in AsyncStorage under user-scoped key — app-level storage, not hardware TEE/SE
|
||||||
|
const vault = new SentinelVault()
|
||||||
|
|
||||||
|
const aes_key = await vault.deriveKey(mnemonicWords.join(' '))
|
||||||
|
|
||||||
await AsyncStorage.setItem(vaultKeys.SHARE_DEVICE, serializeShare(s0));
|
await AsyncStorage.setItem(vaultKeys.SHARE_DEVICE, serializeShare(s0));
|
||||||
|
await AsyncStorage.setItem(vaultKeys.SHARE_SERVER, serializeShare(s1));
|
||||||
await AsyncStorage.setItem(vaultKeys.INITIALIZED, '1');
|
await AsyncStorage.setItem(vaultKeys.INITIALIZED, '1');
|
||||||
|
await AsyncStorage.setItem(vaultKeys.AES_KEY, aes_key.toString('hex'));
|
||||||
|
await AsyncStorage.setItem(vaultKeys.SHARE_HEIR, serializeShare(s2));
|
||||||
|
|
||||||
setHasS0(true);
|
setHasS0(true);
|
||||||
setShowMnemonic(false);
|
setShowMnemonic(false);
|
||||||
setShowBiometric(true);
|
setShowBiometric(true);
|
||||||
@@ -486,6 +474,10 @@ export default function VaultScreen() {
|
|||||||
setSelectedAsset(asset);
|
setSelectedAsset(asset);
|
||||||
setShowDetail(true);
|
setShowDetail(true);
|
||||||
setShowKeyPreview(false);
|
setShowKeyPreview(false);
|
||||||
|
|
||||||
|
if (DEBUG_MODE) {
|
||||||
|
console.log('[DEBUG] Vault Asset Details:', JSON.stringify(asset.rawData, null, 2));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCloseDetail = () => {
|
const handleCloseDetail = () => {
|
||||||
@@ -493,6 +485,93 @@ export default function VaultScreen() {
|
|||||||
setSelectedAsset(null);
|
setSelectedAsset(null);
|
||||||
setShowKeyPreview(false);
|
setShowKeyPreview(false);
|
||||||
setShowGuardedBiometric(false);
|
setShowGuardedBiometric(false);
|
||||||
|
setBackupContent(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFetchBackup = async () => {
|
||||||
|
if (!selectedAsset || !user?.id) return;
|
||||||
|
|
||||||
|
setIsFetchingBackup(true);
|
||||||
|
try {
|
||||||
|
const content = await storageService.getAssetBackup(Number(selectedAsset.id), user.id);
|
||||||
|
if (content) {
|
||||||
|
setBackupContent(content);
|
||||||
|
} else {
|
||||||
|
if (typeof Alert !== 'undefined' && Alert.alert) {
|
||||||
|
Alert.alert('No Backup Found', 'No local plaintext backup found for this treasure.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fetch backup error:', error);
|
||||||
|
if (typeof Alert !== 'undefined' && Alert.alert) {
|
||||||
|
Alert.alert('Error', 'Failed to retrieve local backup.');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setIsFetchingBackup(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteAsset = async () => {
|
||||||
|
if (!selectedAsset || isDeleting) return;
|
||||||
|
|
||||||
|
setIsDeleting(true);
|
||||||
|
try {
|
||||||
|
const result = await deleteVaultAsset(Number(selectedAsset.id));
|
||||||
|
if (result.success) {
|
||||||
|
setShowDeleteConfirm(false);
|
||||||
|
handleCloseDetail();
|
||||||
|
if (typeof Alert !== 'undefined' && Alert.alert) {
|
||||||
|
Alert.alert('Success', 'Treasure removed from the vault.');
|
||||||
|
}
|
||||||
|
} else if (result.isUnauthorized) {
|
||||||
|
setShowDeleteConfirm(false);
|
||||||
|
handleCloseDetail();
|
||||||
|
if (typeof Alert !== 'undefined' && Alert.alert) {
|
||||||
|
Alert.alert('Unauthorized', 'Your session has expired. Please sign in again.');
|
||||||
|
}
|
||||||
|
} else if (result.error && typeof Alert !== 'undefined' && Alert.alert) {
|
||||||
|
Alert.alert('Failed', result.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Delete error:', error);
|
||||||
|
if (typeof Alert !== 'undefined' && Alert.alert) {
|
||||||
|
Alert.alert('Error', 'An unexpected error occurred during deletion.');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setIsDeleting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAssignHeir = async () => {
|
||||||
|
if (!selectedAsset || isAssigning) return;
|
||||||
|
if (!heirEmail.trim() || !heirEmail.includes('@')) {
|
||||||
|
Alert.alert('Invalid Email', 'Please enter a valid email address.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsAssigning(true);
|
||||||
|
try {
|
||||||
|
const result = await assignVaultAsset(Number(selectedAsset.id), heirEmail.trim());
|
||||||
|
if (result.success) {
|
||||||
|
setShowAssignModal(false);
|
||||||
|
setHeirEmail('');
|
||||||
|
Alert.alert('Success', `Asset assigned to ${heirEmail.trim()}`);
|
||||||
|
} else if (result.isUnauthorized) {
|
||||||
|
setShowAssignModal(false);
|
||||||
|
if (typeof Alert !== 'undefined' && Alert.alert) {
|
||||||
|
Alert.alert('Unauthorized', 'Your session has expired. Please sign in again.');
|
||||||
|
}
|
||||||
|
} else if (result.error) {
|
||||||
|
setAssignErrorMessage(result.error);
|
||||||
|
setShowAssignErrorModal(true);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Assign error:', error);
|
||||||
|
setAssignErrorMessage('An unexpected error occurred during assignment.');
|
||||||
|
setShowAssignErrorModal(true);
|
||||||
|
} finally {
|
||||||
|
setIsAssigning(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleGuardedAccess = () => {
|
const handleGuardedAccess = () => {
|
||||||
@@ -502,6 +581,7 @@ export default function VaultScreen() {
|
|||||||
const handleGuardedSuccess = () => {
|
const handleGuardedSuccess = () => {
|
||||||
setShowGuardedBiometric(false);
|
setShowGuardedBiometric(false);
|
||||||
setShowKeyPreview(true);
|
setShowKeyPreview(true);
|
||||||
|
handleFetchBackup();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddVerification = () => {
|
const handleAddVerification = () => {
|
||||||
@@ -1437,6 +1517,12 @@ export default function VaultScreen() {
|
|||||||
<Text style={styles.metaLabel}>{detailMetaLabel}</Text>
|
<Text style={styles.metaLabel}>{detailMetaLabel}</Text>
|
||||||
<Text style={styles.metaValue}>{detailMetaValue}</Text>
|
<Text style={styles.metaValue}>{detailMetaValue}</Text>
|
||||||
</View>
|
</View>
|
||||||
|
{selectedAsset?.heirEmail ? (
|
||||||
|
<View style={[styles.metaCard, { width: '100%' }]}>
|
||||||
|
<Text style={styles.metaLabel}>ASSIGNED HEIR</Text>
|
||||||
|
<Text style={styles.metaValue}>{selectedAsset.heirEmail}</Text>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.actionGroup}>
|
<View style={styles.actionGroup}>
|
||||||
@@ -1449,10 +1535,30 @@ export default function VaultScreen() {
|
|||||||
<MaterialCommunityIcons name="file-lock" size={18} color={colors.vault.primary} />
|
<MaterialCommunityIcons name="file-lock" size={18} color={colors.vault.primary} />
|
||||||
<Text style={styles.actionText}>Export Cipher Pack</Text>
|
<Text style={styles.actionText}>Export Cipher Pack</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity style={styles.actionRow} activeOpacity={0.8}>
|
<TouchableOpacity style={styles.actionRow} activeOpacity={0.7}>
|
||||||
<MaterialCommunityIcons name="refresh" size={18} color={colors.vault.primary} />
|
<Ionicons name="notifications-outline" size={18} color={colors.vault.text} />
|
||||||
<Text style={styles.actionText}>Reset Sentinel Timer</Text>
|
<Text style={styles.actionText}>Reset Sentinel Timer</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.actionRow, styles.assignActionRow]}
|
||||||
|
onPress={() => setShowAssignModal(true)}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
<Ionicons name="person-add-outline" size={18} color={colors.vault.text} />
|
||||||
|
<Text style={styles.actionText}>
|
||||||
|
{selectedAsset?.heirEmail ? 'Change Heir' : 'Assign Heir'}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.actionRow, styles.deleteActionRow]}
|
||||||
|
onPress={() => setShowDeleteConfirm(true)}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
<Feather name="trash-2" size={18} color={colors.vault.warning} />
|
||||||
|
<Text style={[styles.actionText, styles.deleteActionText]}>Delete Treasure</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.guardCard}>
|
<View style={styles.guardCard}>
|
||||||
@@ -1463,6 +1569,7 @@ export default function VaultScreen() {
|
|||||||
<Text style={styles.guardText}>
|
<Text style={styles.guardText}>
|
||||||
Plaintext access requires biometric verification and a memory rehearsal step.
|
Plaintext access requires biometric verification and a memory rehearsal step.
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.guardButton}
|
style={styles.guardButton}
|
||||||
onPress={handleGuardedAccess}
|
onPress={handleGuardedAccess}
|
||||||
@@ -1470,10 +1577,13 @@ export default function VaultScreen() {
|
|||||||
>
|
>
|
||||||
<Text style={styles.guardButtonText}>Begin Verification</Text>
|
<Text style={styles.guardButtonText}>Begin Verification</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
{showKeyPreview && (
|
{showKeyPreview && (
|
||||||
<View style={styles.previewCard}>
|
<View style={styles.previewCard}>
|
||||||
<Text style={styles.previewLabel}>MNEMONIC SHARD (MASKED)</Text>
|
<Text style={styles.previewLabel}>LOCAL PLAINTEXT BACKUP</Text>
|
||||||
<Text style={styles.previewValue}>ocean-anchored-ember-veil</Text>
|
<Text style={styles.previewValue}>
|
||||||
|
{isFetchingBackup ? 'Fetching content...' : (backupContent || 'No local backup found for this treasure')}
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
@@ -1498,6 +1608,155 @@ export default function VaultScreen() {
|
|||||||
isDark
|
isDark
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
{/* Delete Confirmation Modal */}
|
||||||
|
<Modal
|
||||||
|
visible={showDeleteConfirm}
|
||||||
|
animationType="fade"
|
||||||
|
transparent
|
||||||
|
onRequestClose={() => setShowDeleteConfirm(false)}
|
||||||
|
>
|
||||||
|
<View style={styles.modalOverlay}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.modalOverlayDismiss}
|
||||||
|
activeOpacity={1}
|
||||||
|
onPress={() => setShowDeleteConfirm(false)}
|
||||||
|
/>
|
||||||
|
<View style={styles.deleteConfirmContent}>
|
||||||
|
<View style={styles.deleteIconContainer}>
|
||||||
|
<Feather name="alert-triangle" size={32} color={colors.vault.warning} />
|
||||||
|
</View>
|
||||||
|
<Text style={styles.deleteTitle}>Remove Treasure?</Text>
|
||||||
|
<Text style={styles.deleteMessage}>
|
||||||
|
This action cannot be undone. The treasure will be permanently shredded from the deep vault.
|
||||||
|
</Text>
|
||||||
|
<View style={styles.deleteButtons}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.deleteCancelButton}
|
||||||
|
onPress={() => setShowDeleteConfirm(false)}
|
||||||
|
disabled={isDeleting}
|
||||||
|
>
|
||||||
|
<Text style={styles.deleteCancelText}>Cancel</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.deleteConfirmButton}
|
||||||
|
onPress={handleDeleteAsset}
|
||||||
|
disabled={isDeleting}
|
||||||
|
>
|
||||||
|
{isDeleting ? (
|
||||||
|
<Text style={styles.deleteConfirmText}>Shredding...</Text>
|
||||||
|
) : (
|
||||||
|
<Text style={styles.deleteConfirmText}>Confirm Delete</Text>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* Assign Heir Modal */}
|
||||||
|
<Modal
|
||||||
|
visible={showAssignModal}
|
||||||
|
animationType="slide"
|
||||||
|
transparent
|
||||||
|
onRequestClose={() => setShowAssignModal(false)}
|
||||||
|
>
|
||||||
|
<View style={styles.modalOverlay}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.modalOverlayDismiss}
|
||||||
|
activeOpacity={1}
|
||||||
|
onPress={() => setShowAssignModal(false)}
|
||||||
|
/>
|
||||||
|
<KeyboardAvoidingView
|
||||||
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||||
|
style={styles.assignModalContainer}
|
||||||
|
>
|
||||||
|
<View style={styles.assignModalContent}>
|
||||||
|
<View style={styles.assignModalHeader}>
|
||||||
|
<View style={styles.assignIconGlow}>
|
||||||
|
<Ionicons name="person-add" size={32} color={colors.vault.primary} />
|
||||||
|
</View>
|
||||||
|
<Text style={styles.assignTitle}>Designate Heir</Text>
|
||||||
|
<Text style={styles.assignSubtitle}>
|
||||||
|
Enter the email address of the person who should inherit this treasure.
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.assignInputContainer}>
|
||||||
|
<Text style={styles.inputLabel}>HEIR EMAIL ADDRESS</Text>
|
||||||
|
<TextInput
|
||||||
|
style={styles.assignInput}
|
||||||
|
value={heirEmail}
|
||||||
|
onChangeText={setHeirEmail}
|
||||||
|
placeholder="name@example.com"
|
||||||
|
placeholderTextColor={colors.vault.textSecondary}
|
||||||
|
keyboardType="email-address"
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect={false}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.assignButtons}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.assignCancelButton}
|
||||||
|
onPress={() => {
|
||||||
|
setShowAssignModal(false);
|
||||||
|
setHeirEmail('');
|
||||||
|
}}
|
||||||
|
disabled={isAssigning}
|
||||||
|
>
|
||||||
|
<Text style={styles.assignCancelText}>Cancel</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.assignConfirmButton}
|
||||||
|
onPress={handleAssignHeir}
|
||||||
|
disabled={isAssigning || !heirEmail.trim()}
|
||||||
|
>
|
||||||
|
<LinearGradient
|
||||||
|
colors={[colors.vault.primary, colors.vault.secondary]}
|
||||||
|
style={styles.assignConfirmGradient}
|
||||||
|
>
|
||||||
|
{isAssigning ? (
|
||||||
|
<Text style={styles.assignConfirmText}>Assigning...</Text>
|
||||||
|
) : (
|
||||||
|
<Text style={styles.assignConfirmText}>Confirm Heir</Text>
|
||||||
|
)}
|
||||||
|
</LinearGradient>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</KeyboardAvoidingView>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* Assign Heir Error Modal */}
|
||||||
|
<Modal
|
||||||
|
visible={showAssignErrorModal}
|
||||||
|
animationType="fade"
|
||||||
|
transparent
|
||||||
|
onRequestClose={() => setShowAssignErrorModal(false)}
|
||||||
|
>
|
||||||
|
<View style={styles.modalOverlay}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.modalOverlayDismiss}
|
||||||
|
activeOpacity={1}
|
||||||
|
onPress={() => setShowAssignErrorModal(false)}
|
||||||
|
/>
|
||||||
|
<View style={styles.errorModalContent}>
|
||||||
|
<View style={styles.errorIconContainer}>
|
||||||
|
<MaterialCommunityIcons name="alert-circle-outline" size={32} color={colors.vault.warning} />
|
||||||
|
</View>
|
||||||
|
<Text style={styles.errorTitle}>Assignment Failed</Text>
|
||||||
|
<Text style={styles.errorMessage}>{assignErrorMessage}</Text>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.errorCloseButton}
|
||||||
|
onPress={() => setShowAssignErrorModal(false)}
|
||||||
|
>
|
||||||
|
<Text style={styles.errorCloseButtonText}>Dismiss</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1766,6 +2025,73 @@ const styles = StyleSheet.create({
|
|||||||
backgroundColor: 'rgba(26, 58, 74, 0.8)',
|
backgroundColor: 'rgba(26, 58, 74, 0.8)',
|
||||||
justifyContent: 'flex-end',
|
justifyContent: 'flex-end',
|
||||||
},
|
},
|
||||||
|
modalOverlayDismiss: {
|
||||||
|
...StyleSheet.absoluteFillObject,
|
||||||
|
},
|
||||||
|
deleteConfirmContent: {
|
||||||
|
backgroundColor: colors.vault.cardBackground,
|
||||||
|
marginHorizontal: spacing.lg,
|
||||||
|
borderRadius: borderRadius.xl,
|
||||||
|
padding: spacing.xl,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.vault.cardBorder,
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: spacing.xxl + spacing.lg,
|
||||||
|
},
|
||||||
|
deleteIconContainer: {
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
borderRadius: 32,
|
||||||
|
backgroundColor: `${colors.vault.warning}20`,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginBottom: spacing.lg,
|
||||||
|
},
|
||||||
|
deleteTitle: {
|
||||||
|
fontSize: typography.fontSize.lg,
|
||||||
|
color: colors.vault.text,
|
||||||
|
fontWeight: '700',
|
||||||
|
marginBottom: spacing.sm,
|
||||||
|
fontFamily: typography.fontFamily.serif,
|
||||||
|
},
|
||||||
|
deleteMessage: {
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
color: colors.vault.textSecondary,
|
||||||
|
textAlign: 'center',
|
||||||
|
lineHeight: typography.fontSize.sm * 1.5,
|
||||||
|
marginBottom: spacing.xl,
|
||||||
|
},
|
||||||
|
deleteButtons: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
gap: spacing.md,
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
deleteCancelButton: {
|
||||||
|
flex: 1,
|
||||||
|
paddingVertical: spacing.md,
|
||||||
|
borderRadius: borderRadius.lg,
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.08)',
|
||||||
|
alignItems: 'center',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
},
|
||||||
|
deleteCancelText: {
|
||||||
|
color: colors.vault.textSecondary,
|
||||||
|
fontSize: typography.fontSize.base,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
deleteConfirmButton: {
|
||||||
|
flex: 2,
|
||||||
|
paddingVertical: spacing.md,
|
||||||
|
borderRadius: borderRadius.lg,
|
||||||
|
backgroundColor: colors.vault.warning,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
deleteConfirmText: {
|
||||||
|
color: colors.vault.text,
|
||||||
|
fontSize: typography.fontSize.base,
|
||||||
|
fontWeight: '700',
|
||||||
|
},
|
||||||
modalContent: {
|
modalContent: {
|
||||||
backgroundColor: colors.nautical.cream,
|
backgroundColor: colors.nautical.cream,
|
||||||
borderTopLeftRadius: borderRadius.xxl,
|
borderTopLeftRadius: borderRadius.xxl,
|
||||||
@@ -2570,8 +2896,204 @@ const styles = StyleSheet.create({
|
|||||||
borderColor: colors.sentinel.cardBorder,
|
borderColor: colors.sentinel.cardBorder,
|
||||||
},
|
},
|
||||||
mnemonicSecondaryText: {
|
mnemonicSecondaryText: {
|
||||||
color: colors.sentinel.text,
|
|
||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
letterSpacing: typography.letterSpacing.wide,
|
letterSpacing: typography.letterSpacing.wide,
|
||||||
},
|
},
|
||||||
|
deleteActionRow: {
|
||||||
|
backgroundColor: 'rgba(229, 115, 115, 0.08)',
|
||||||
|
borderColor: 'rgba(229, 115, 115, 0.2)',
|
||||||
|
marginTop: spacing.sm,
|
||||||
|
},
|
||||||
|
deleteActionText: {
|
||||||
|
color: colors.vault.warning,
|
||||||
|
},
|
||||||
|
confirmModalOverlay: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: 'rgba(11, 20, 24, 0.85)',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: spacing.xl,
|
||||||
|
},
|
||||||
|
confirmModalContent: {
|
||||||
|
width: '100%',
|
||||||
|
backgroundColor: colors.vault.cardBackground,
|
||||||
|
borderRadius: borderRadius.xl,
|
||||||
|
padding: spacing.lg,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.vault.cardBorder,
|
||||||
|
gap: spacing.md,
|
||||||
|
},
|
||||||
|
confirmModalTitle: {
|
||||||
|
fontSize: typography.fontSize.lg,
|
||||||
|
color: colors.vault.text,
|
||||||
|
fontWeight: '700',
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
confirmModalText: {
|
||||||
|
fontSize: typography.fontSize.base,
|
||||||
|
color: colors.vault.textSecondary,
|
||||||
|
textAlign: 'center',
|
||||||
|
lineHeight: 22,
|
||||||
|
},
|
||||||
|
confirmModalButtons: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
gap: spacing.md,
|
||||||
|
marginTop: spacing.sm,
|
||||||
|
},
|
||||||
|
confirmModalButton: {
|
||||||
|
flex: 1,
|
||||||
|
paddingVertical: spacing.md,
|
||||||
|
borderRadius: borderRadius.lg,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
confirmCancelButton: {
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
},
|
||||||
|
confirmDeleteButton: {
|
||||||
|
backgroundColor: colors.vault.warning,
|
||||||
|
},
|
||||||
|
confirmCancelText: {
|
||||||
|
color: colors.vault.textSecondary,
|
||||||
|
},
|
||||||
|
assignActionRow: {
|
||||||
|
// Optional specific styling
|
||||||
|
},
|
||||||
|
assignModalContainer: {
|
||||||
|
backgroundColor: colors.vault.cardBackground,
|
||||||
|
marginHorizontal: spacing.lg,
|
||||||
|
borderRadius: borderRadius.xl,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.vault.cardBorder,
|
||||||
|
overflow: 'hidden',
|
||||||
|
marginBottom: spacing.xxl,
|
||||||
|
},
|
||||||
|
assignModalContent: {
|
||||||
|
padding: spacing.xl,
|
||||||
|
},
|
||||||
|
assignModalHeader: {
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: spacing.xl,
|
||||||
|
},
|
||||||
|
assignIconGlow: {
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
borderRadius: 32,
|
||||||
|
backgroundColor: `${colors.vault.primary}15`,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginBottom: spacing.md,
|
||||||
|
},
|
||||||
|
assignTitle: {
|
||||||
|
fontSize: typography.fontSize.lg,
|
||||||
|
color: colors.vault.text,
|
||||||
|
fontWeight: '700',
|
||||||
|
marginBottom: spacing.xs,
|
||||||
|
fontFamily: typography.fontFamily.serif,
|
||||||
|
},
|
||||||
|
assignSubtitle: {
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
color: colors.vault.textSecondary,
|
||||||
|
textAlign: 'center',
|
||||||
|
lineHeight: typography.fontSize.sm * 1.5,
|
||||||
|
},
|
||||||
|
assignInputContainer: {
|
||||||
|
marginBottom: spacing.xl,
|
||||||
|
},
|
||||||
|
inputLabel: {
|
||||||
|
fontSize: typography.fontSize.xs,
|
||||||
|
color: colors.vault.textSecondary,
|
||||||
|
fontWeight: '700',
|
||||||
|
letterSpacing: 1,
|
||||||
|
marginBottom: spacing.sm,
|
||||||
|
},
|
||||||
|
assignInput: {
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
borderRadius: borderRadius.lg,
|
||||||
|
padding: spacing.md,
|
||||||
|
fontSize: typography.fontSize.base,
|
||||||
|
color: colors.vault.text,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
},
|
||||||
|
assignButtons: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
gap: spacing.md,
|
||||||
|
},
|
||||||
|
assignCancelButton: {
|
||||||
|
flex: 1,
|
||||||
|
paddingVertical: spacing.md,
|
||||||
|
borderRadius: borderRadius.lg,
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.08)',
|
||||||
|
alignItems: 'center',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
},
|
||||||
|
assignCancelText: {
|
||||||
|
color: colors.vault.textSecondary,
|
||||||
|
fontSize: typography.fontSize.base,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
assignConfirmButton: {
|
||||||
|
flex: 2,
|
||||||
|
borderRadius: borderRadius.lg,
|
||||||
|
overflow: 'hidden',
|
||||||
|
},
|
||||||
|
assignConfirmGradient: {
|
||||||
|
paddingVertical: spacing.md,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
assignConfirmText: {
|
||||||
|
color: colors.vault.text,
|
||||||
|
fontSize: typography.fontSize.base,
|
||||||
|
fontWeight: '700',
|
||||||
|
},
|
||||||
|
errorModalContent: {
|
||||||
|
backgroundColor: colors.vault.cardBackground,
|
||||||
|
marginHorizontal: spacing.lg,
|
||||||
|
borderRadius: borderRadius.xl,
|
||||||
|
padding: spacing.xl,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.vault.cardBorder,
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: spacing.xxl,
|
||||||
|
},
|
||||||
|
errorIconContainer: {
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
borderRadius: 32,
|
||||||
|
backgroundColor: `${colors.vault.warning}15`,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginBottom: spacing.lg,
|
||||||
|
},
|
||||||
|
errorTitle: {
|
||||||
|
fontSize: typography.fontSize.lg,
|
||||||
|
color: colors.vault.text,
|
||||||
|
fontWeight: '700',
|
||||||
|
marginBottom: spacing.sm,
|
||||||
|
fontFamily: typography.fontFamily.serif,
|
||||||
|
},
|
||||||
|
errorMessage: {
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
color: colors.vault.textSecondary,
|
||||||
|
textAlign: 'center',
|
||||||
|
lineHeight: typography.fontSize.sm * 1.5,
|
||||||
|
marginBottom: spacing.xl,
|
||||||
|
},
|
||||||
|
errorCloseButton: {
|
||||||
|
width: '100%',
|
||||||
|
paddingVertical: spacing.md,
|
||||||
|
borderRadius: borderRadius.lg,
|
||||||
|
backgroundColor: colors.vault.warning,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
errorCloseButtonText: {
|
||||||
|
color: colors.vault.text,
|
||||||
|
fontSize: typography.fontSize.base,
|
||||||
|
fontWeight: '700',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import {
|
|||||||
getApiHeaders,
|
getApiHeaders,
|
||||||
logApiDebug,
|
logApiDebug,
|
||||||
} from '../config';
|
} from '../config';
|
||||||
|
import { AIRole } from '../types';
|
||||||
|
import { trimInternalMessages } from '../utils/token_utils';
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Type Definitions
|
// Type Definitions
|
||||||
@@ -219,10 +221,13 @@ export const aiService = {
|
|||||||
const errorText = await response.text();
|
const errorText = await response.text();
|
||||||
logApiDebug('AI Image Error Response', errorText);
|
logApiDebug('AI Image Error Response', errorText);
|
||||||
|
|
||||||
let errorDetail = 'AI image request failed';
|
let errorDetail: string = 'AI image request failed';
|
||||||
try {
|
try {
|
||||||
const errorData = JSON.parse(errorText);
|
const errorData = JSON.parse(errorText);
|
||||||
errorDetail = errorData.detail || errorDetail;
|
const d = errorData.detail;
|
||||||
|
if (typeof d === 'string') errorDetail = d;
|
||||||
|
else if (Array.isArray(d) && d[0]?.msg) errorDetail = d.map((e: { msg?: string }) => e.msg).join('; ');
|
||||||
|
else if (d && typeof d === 'object') errorDetail = JSON.stringify(d);
|
||||||
} catch {
|
} catch {
|
||||||
errorDetail = errorText || errorDetail;
|
errorDetail = errorText || errorDetail;
|
||||||
}
|
}
|
||||||
@@ -241,4 +246,86 @@ export const aiService = {
|
|||||||
throw error;
|
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;
|
author_id: number;
|
||||||
private_key_shard: string;
|
private_key_shard: string;
|
||||||
content_outer_encrypted: string;
|
content_outer_encrypted: string;
|
||||||
|
heir_email?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AssetCreate {
|
export interface AssetCreate {
|
||||||
@@ -45,7 +46,7 @@ export interface AssetClaimResponse {
|
|||||||
|
|
||||||
export interface AssetAssign {
|
export interface AssetAssign {
|
||||||
asset_id: number;
|
asset_id: number;
|
||||||
heir_name: string;
|
heir_email: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -59,6 +60,7 @@ const MOCK_ASSETS: Asset[] = [
|
|||||||
author_id: MOCK_CONFIG.USER.id,
|
author_id: MOCK_CONFIG.USER.id,
|
||||||
private_key_shard: 'mock_shard_1',
|
private_key_shard: 'mock_shard_1',
|
||||||
content_outer_encrypted: 'mock_encrypted_content_1',
|
content_outer_encrypted: 'mock_encrypted_content_1',
|
||||||
|
heir_email: 'heir@example.com',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
@@ -217,7 +219,7 @@ export const assetsService = {
|
|||||||
logApiDebug('Assign Asset', 'Using mock mode');
|
logApiDebug('Assign Asset', 'Using mock mode');
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
resolve({ message: `Asset assigned to ${assignment.heir_name}` });
|
resolve({ message: `Asset assigned to ${assignment.heir_email}` });
|
||||||
}, MOCK_CONFIG.RESPONSE_DELAY);
|
}, MOCK_CONFIG.RESPONSE_DELAY);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -245,4 +247,44 @@ export const assetsService = {
|
|||||||
throw error;
|
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;
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
96
src/services/langgraph.service.ts
Normal file
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();
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -14,6 +14,7 @@ import AsyncStorage from '@react-native-async-storage/async-storage';
|
|||||||
const STORAGE_KEYS = {
|
const STORAGE_KEYS = {
|
||||||
CHAT_HISTORY: '@sentinel:chat_history',
|
CHAT_HISTORY: '@sentinel:chat_history',
|
||||||
CURRENT_MESSAGES: '@sentinel:current_messages',
|
CURRENT_MESSAGES: '@sentinel:current_messages',
|
||||||
|
ASSET_BACKUP: '@sentinel:asset_backup',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -115,6 +116,32 @@ export const storageService = {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error clearing storage data:', 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ export interface VaultAsset {
|
|||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
isEncrypted: boolean;
|
isEncrypted: boolean;
|
||||||
|
heirEmail?: string;
|
||||||
|
rawData?: any; // For debug logging
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sentinel Types
|
// Sentinel Types
|
||||||
@@ -102,3 +104,13 @@ export interface LoginResponse {
|
|||||||
token_type: string;
|
token_type: string;
|
||||||
user: User;
|
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
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
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
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);
|
||||||
|
}
|
||||||
76
src/utils/token_utils.ts
Normal file
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;
|
||||||
|
}
|
||||||
@@ -14,8 +14,13 @@ export interface ApiAsset {
|
|||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
type?: string;
|
type?: string;
|
||||||
|
author_id?: number;
|
||||||
|
private_key_shard?: string;
|
||||||
|
content_outer_encrypted?: string;
|
||||||
created_at?: string;
|
created_at?: string;
|
||||||
updated_at?: string;
|
updated_at?: string;
|
||||||
|
heir_id?: number;
|
||||||
|
heir_email?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
@@ -31,6 +36,8 @@ export const VAULT_ASSET_TYPES: VaultAssetType[] = [
|
|||||||
'custom',
|
'custom',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const initialVaultAssets: VaultAsset[] = [];
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// Mapping
|
// Mapping
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
@@ -50,6 +57,8 @@ export function mapApiAssetToVaultAsset(api: ApiAsset): VaultAsset {
|
|||||||
createdAt: api.created_at ? new Date(api.created_at) : new Date(),
|
createdAt: api.created_at ? new Date(api.created_at) : new Date(),
|
||||||
updatedAt: api.updated_at ? new Date(api.updated_at) : new Date(),
|
updatedAt: api.updated_at ? new Date(api.updated_at) : new Date(),
|
||||||
isEncrypted: true,
|
isEncrypted: true,
|
||||||
|
heirEmail: api.heir_email,
|
||||||
|
rawData: api,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,41 +69,3 @@ export function mapApiAssetsToVaultAssets(apiList: ApiAsset[]): VaultAsset[] {
|
|||||||
return apiList.map(mapApiAssetToVaultAsset);
|
return apiList.map(mapApiAssetToVaultAsset);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
// Mock / initial data (fallback when API is unavailable)
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
export const initialVaultAssets: VaultAsset[] = [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
type: 'private_key',
|
|
||||||
label: 'ETH Main Wallet Key',
|
|
||||||
createdAt: new Date('2024-01-10'),
|
|
||||||
updatedAt: new Date('2024-01-10'),
|
|
||||||
isEncrypted: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
type: 'game_account',
|
|
||||||
label: 'Steam Account Credentials',
|
|
||||||
createdAt: new Date('2024-01-08'),
|
|
||||||
updatedAt: new Date('2024-01-08'),
|
|
||||||
isEncrypted: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
type: 'document',
|
|
||||||
label: 'Insurance Policy Scan',
|
|
||||||
createdAt: new Date('2024-01-05'),
|
|
||||||
updatedAt: new Date('2024-01-05'),
|
|
||||||
isEncrypted: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '4',
|
|
||||||
type: 'will',
|
|
||||||
label: 'Testament Draft v2',
|
|
||||||
createdAt: new Date('2024-01-02'),
|
|
||||||
updatedAt: new Date('2024-01-15'),
|
|
||||||
isEncrypted: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|||||||
Reference in New Issue
Block a user