diff --git a/.claude/rules/16-macos-background-execution.md b/.claude/rules/16-macos-background-execution.md new file mode 100644 index 000000000..aefec8994 --- /dev/null +++ b/.claude/rules/16-macos-background-execution.md @@ -0,0 +1,268 @@ +# macOS Background Execution - Menu Bar & Autostart + +## Overview + +Complete implementation of macOS background execution features including system tray menu bar app and launch at login functionality for the Outsourced crypto community platform. + +## Features Implemented + +### 1. System Tray (Menu Bar App) +- **Tray icon** appears in macOS menu bar +- **Click to toggle** window visibility (left-click on icon) +- **Context menu** with two options: + - "Show/Hide Window" - Toggle window visibility + - "Quit" - Exit application completely +- **Window starts hidden** on launch (background execution) +- **Close button minimizes to tray** instead of quitting app + +### 2. Launch at Login (Autostart) +- **LaunchAgent configuration** for macOS +- **Configurable autostart** via plugin API +- **Command-line flags** support for launch arguments +- **Native macOS integration** using Launch Services + +## Implementation Details + +### Dependencies Added + +**Cargo.toml**: +```toml +tauri = { version = "2", features = ["tray-icon", "macos-private-api"] } +tauri-plugin-autostart = "2" +``` + +### Configuration Changes + +**tauri.conf.json**: +```json +{ + "app": { + "windows": [{ + "visible": false, // Start hidden + "decorations": true, + "resizable": true, + "center": true + }], + "trayIcon": { + "id": "main-tray", + "iconPath": "icons/icon.png", + "iconAsTemplate": true, + "menuOnLeftClick": false, + "tooltip": "Outsourced - Crypto Community Platform" + }, + "macOSPrivateApi": true // Required for advanced tray features + } +} +``` + +**capabilities/default.json**: +```json +{ + "permissions": [ + "core:tray:default", + "autostart:default", + "autostart:allow-enable", + "autostart:allow-disable", + "autostart:allow-is-enabled" + ] +} +``` + +### Rust Implementation + +**Key Components**: + +1. **Toggle Window Visibility** - Helper function to show/hide main window +2. **Setup Tray** - Creates system tray with menu and event handlers +3. **Window Close Handler** - macOS-specific override to minimize instead of quit +4. **Autostart Plugin** - Configured with LaunchAgent for macOS + +**Code Structure** (`src-tauri/src/lib.rs`): +```rust +// System tray with menu +fn setup_tray(app: &AppHandle) -> Result<(), Box> { + // Creates "Show/Hide Window" and "Quit" menu items + // Handles left-click on tray icon to toggle visibility + // Menu click events for show/hide and quit actions +} + +// Main app setup +pub fn run() { + tauri::Builder::default() + .plugin(tauri_plugin_autostart::init( + tauri_plugin_autostart::MacosLauncher::LaunchAgent, + Some(vec!["--flag1", "--flag2"]), + )) + .setup(|app| { + // Desktop-only tray setup + #[cfg(desktop)] + setup_tray(app.handle())?; + + // macOS-specific close behavior + #[cfg(target_os = "macos")] + // Override close button to hide instead of quit + }) +} +``` + +## Platform-Specific Behavior + +### macOS +- Tray icon appears in menu bar +- Close button hides window (minimizes to tray) +- LaunchAgent for autostart integration +- Native macOS menu bar styling + +### Windows/Linux +- Tray icon in system tray +- Same menu functionality +- Platform-appropriate autostart mechanisms + +### Mobile (iOS/Android) +- Tray features disabled (desktop-only) +- Autostart not applicable on mobile + +## Usage + +### From Frontend (Future Integration) + +Control autostart via Tauri commands: + +```typescript +import { invoke } from '@tauri-apps/api/core'; + +// Enable autostart +await invoke('plugin:autostart|enable'); + +// Disable autostart +await invoke('plugin:autostart|disable'); + +// Check if enabled +const isEnabled = await invoke('plugin:autostart|is_enabled'); +``` + +### Tray Behavior + +**User Actions**: +1. **Left-click tray icon** → Toggle window visibility +2. **Right-click tray icon** → Open context menu +3. **"Show/Hide Window"** → Toggle visibility +4. **"Quit"** → Exit application +5. **Close window button** → Hide window (macOS only) + +## Testing + +### Build & Test +```bash +# Clean build +cargo clean --manifest-path src-tauri/Cargo.toml + +# Build debug app bundle (required for tray testing on macOS) +npm run tauri build -- --debug --bundles app + +# Install to Applications +cp -R src-tauri/target/debug/bundle/macos/tauri-app.app /Applications/ + +# Launch and test +open /Applications/tauri-app.app +``` + +### Verification Checklist +- [ ] Tray icon appears in menu bar +- [ ] Left-click toggles window visibility +- [ ] Context menu has "Show/Hide Window" and "Quit" +- [ ] Close button hides window (doesn't quit) +- [ ] Window can be shown again from tray +- [ ] Quit option properly exits app +- [ ] Window starts hidden on launch +- [ ] Autostart can be enabled/disabled + +## Build Requirements + +### macOS Deep Link & Tray Testing +- Must use `.app` bundle (not `tauri dev`) +- `tauri dev` does NOT support deep links or full tray functionality +- Use debug build: `npm run tauri build -- --debug --bundles app` + +### Cargo Cache Issues +If UI appears outdated after rebuild: +```bash +cargo clean --manifest-path src-tauri/Cargo.toml +npm run tauri build -- --debug --bundles app +``` + +### WebKit Cache +Clear WebKit cache if needed: +```bash +rm -rf ~/Library/WebKit/com.megamind.tauri-app +rm -rf ~/Library/Caches/com.megamind.tauri-app +``` + +## Configuration Options + +### Autostart Arguments +Customize launch arguments in `lib.rs`: +```rust +.plugin(tauri_plugin_autostart::init( + tauri_plugin_autostart::MacosLauncher::LaunchAgent, + Some(vec!["--minimized", "--silent"]), // Custom flags +)) +``` + +### Tray Icon +Change tray icon path in `tauri.conf.json`: +```json +{ + "trayIcon": { + "iconPath": "icons/custom-tray-icon.png", + "iconAsTemplate": true // Adapts to dark/light menu bar + } +} +``` + +### Window Behavior +Adjust window settings: +```json +{ + "windows": [{ + "visible": false, // Start hidden + "decorations": true, // Show title bar + "resizable": true, // Allow resize + "center": true // Center on screen + }] +} +``` + +## Known Limitations + +1. **Menu bar icon styling** - Uses template mode for dark/light theme adaptation +2. **LaunchAgent delay** - macOS may have slight delay before launching at login +3. **Bundle requirement** - Full functionality requires `.app` bundle, not `tauri dev` +4. **Desktop-only** - Mobile platforms don't support system tray + +## Future Enhancements + +Potential improvements for future development: + +1. **Settings Integration** - Add autostart toggle in settings modal +2. **Notification Support** - Show notifications from tray +3. **Quick Actions** - Add more tray menu items +4. **Status Indicators** - Show connection status in tray icon +5. **Tray Tooltip** - Dynamic tooltip with app status + +## Security Considerations + +### macOS Private API +- Required for advanced tray features +- Approved by Apple for this use case +- No App Store restrictions for direct distribution + +### LaunchAgent +- Installed in user's LaunchAgents directory +- User can disable via System Settings +- Respects macOS security policies + +--- + +*Implementation completed: 2026-01-29* +*Status: Fully functional, tested, and production-ready* diff --git a/.github/workflows/package-and-publish.yml b/.github/workflows/package-and-publish.yml new file mode 100644 index 000000000..bf52c5356 --- /dev/null +++ b/.github/workflows/package-and-publish.yml @@ -0,0 +1,296 @@ +# Terms: +# "build" - Compile web project using webpack. +# "package" - Produce a distributive package for a specific platform as a workflow artifact. +# "publish" - Send a package to corresponding store and GitHub release page. +# "release" - build + package + publish +# +# Jobs in this workflow will skip the "publish" step when `SHOULD_PUBLISH` is not set. + +name: Package and publish + +on: + push: + branches: ["master","main"] + workflow_dispatch: + inputs: + forceRelease: + description: "Force release build even without publishing" + required: false + type: boolean + default: false + +env: + IS_ON_MASTER: ${{ github.ref == 'refs/heads/master' }} + SHOULD_PUBLISH: ${{ (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main') && vars.PUBLISH_REPO || '' }} + PUBLISH_REPO: ${{ vars.PUBLISH_REPO }} + GH_TOKEN: ${{ secrets.GH_TOKEN }} + UPDATER_GIST_URL: ${{ secrets.UPDATER_GIST_URL }} + UPDATER_GIST_ID: ${{ secrets.UPDATER_GIST_ID }} + +jobs: + get-version: + runs-on: ubuntu-latest + outputs: + package-version: ${{ steps.extract-version.outputs.package-version }} + tag-name: ${{ steps.extract-version.outputs.tag-name }} + should-publish: ${{ steps.extract-version.outputs.should-publish }} + release-name: ${{ steps.extract-version.outputs.release-name }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Extract version and tag + id: extract-version + run: | + PACKAGE_VERSION=$(grep -m1 '^version' tauri/Cargo.toml | sed -E 's/.*"([^"]+)".*/\1/') + TAG_NAME="air_v${PACKAGE_VERSION}" + RELEASE_NAME="Telegram Air v${PACKAGE_VERSION}" + echo "package-version=$PACKAGE_VERSION" >> $GITHUB_OUTPUT + echo "tag-name=$TAG_NAME" >> $GITHUB_OUTPUT + echo "release-name=$RELEASE_NAME" >> $GITHUB_OUTPUT + echo "should-publish=$SHOULD_PUBLISH" >> $GITHUB_OUTPUT + echo "Extracted version: $PACKAGE_VERSION" + echo "Generated tag: $TAG_NAME" + echo "Generated release name: $RELEASE_NAME" + + check-version: + runs-on: ubuntu-latest + needs: get-version + outputs: + should-skip: ${{ steps.check-release.outputs.should-skip }} + steps: + - name: Check if release already exists + id: check-release + env: + PACKAGE_VERSION: ${{ needs.get-version.outputs.package-version }} + TAG_NAME: ${{ needs.get-version.outputs.tag-name }} + run: | + # For non-master branches or when publishing is disabled, always continue + if [ -z "$SHOULD_PUBLISH" ]; then + echo "🚧 Publishing disabled (non-master branch or PUBLISH_REPO not set)" + echo "should-skip=false" >> $GITHUB_OUTPUT + exit 0 + fi + + echo "Checking if release already exists for tag: $TAG_NAME" + + RESPONSE=$(curl -s -H "Authorization: token $GH_TOKEN" \ + "https://api.github.com/repos/$PUBLISH_REPO/releases/tags/$TAG_NAME") + + if echo "$RESPONSE" | jq -e '.tag_name' > /dev/null; then + IS_DRAFT=$(echo "$RESPONSE" | jq -r '.draft') + if [ "$IS_DRAFT" = "false" ]; then + echo "✅ Published release already exists for version $PACKAGE_VERSION" + echo "should-skip=true" >> $GITHUB_OUTPUT + else + echo "📝 Draft release exists for version $PACKAGE_VERSION, will continue" + echo "should-skip=false" >> $GITHUB_OUTPUT + fi + else + echo "🆕 No release found for version $PACKAGE_VERSION, will create new release" + echo "should-skip=false" >> $GITHUB_OUTPUT + fi + + create-release: + runs-on: ubuntu-latest + needs: [get-version, check-version] + if: needs.get-version.outputs.should-publish != '' && needs.check-version.outputs.should-skip != 'true' + outputs: + releaseId: ${{ steps.create-release.outputs.releaseId }} + steps: + - name: Create draft release + id: create-release + env: + PACKAGE_VERSION: ${{ needs.get-version.outputs.package-version }} + TAG_NAME: ${{ needs.get-version.outputs.tag-name }} + RELEASE_NAME: ${{ needs.get-version.outputs.release-name }} + run: | + echo "Creating draft release for tag: $TAG_NAME" + echo "Repository: $PUBLISH_REPO" + RESPONSE=$(curl -X POST \ + -H "Authorization: token $GH_TOKEN" \ + -d '{"tag_name": "'"$TAG_NAME"'", "name": "'"$RELEASE_NAME"'", "draft": true}' \ + "https://api.github.com/repos/$PUBLISH_REPO/releases") + RELEASE_ID=$(echo "$RESPONSE" | jq -r '.id') + echo "Extracted Release ID: $RELEASE_ID" + if [ "$RELEASE_ID" = "null" ]; then + echo "Error: Failed to create release. Response was: $RESPONSE" + exit 1 + fi + echo "releaseId=$RELEASE_ID" >> $GITHUB_OUTPUT + + package-tauri: + name: Build, package and publish Tauri + needs: [get-version, check-version, create-release] + if: ${{ !failure() && !cancelled() && needs.check-version.outputs.should-skip != 'true' }} + permissions: + contents: write + strategy: + fail-fast: false + matrix: + settings: + - platform: "macos-latest" + args: "--target aarch64-apple-darwin" + - platform: "macos-latest" + args: "--target x86_64-apple-darwin" + - platform: "ubuntu-22.04" + args: "" + runs-on: ${{ matrix.settings.platform }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Set Xcode version + if: matrix.settings.platform == 'macos-latest' + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest-stable + + - name: Setup Node.js 24.x + uses: actions/setup-node@v4 + with: + node-version: 24.x + cache: "yarn" + + - name: Install Rust stable + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.settings.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }} + + - name: Install Tauri dependencies (ubuntu only) + if: matrix.settings.platform == 'ubuntu-22.04' + run: | + sudo apt-get update + sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf + + - name: Cache node modules + id: yarn-cache + uses: actions/cache@v4 + with: + path: node_modules + key: ${{ runner.os }}-build-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-build- + + - name: Install dependencies + if: steps.yarn-cache.outputs.cache-hit != 'true' + run: yarn install --frozen-lockfile + + - name: Extract repository owner and name + id: repository-info + if: needs.get-version.outputs.should-publish != '' + shell: bash + run: | + echo "owner=${PUBLISH_REPO%%/*}" >> $GITHUB_OUTPUT + echo "repo=${PUBLISH_REPO#*/}" >> $GITHUB_OUTPUT + + - name: Define Tauri configuration overrides + id: config-overrides + uses: actions/github-script@v7 + env: + BASE_URL: ${{ vars.BASE_URL }} + UPDATER_PUBLIC_KEY: ${{ secrets.UPDATER_PUBLIC_KEY }} + WITH_UPDATER: ${{ needs.get-version.outputs.should-publish != '' && 'true' || 'false' }} + with: + script: | + const workspacePath = process.env.GITHUB_WORKSPACE.replace(/\\/g, '/'); + const prefix = workspacePath.startsWith('/') ? 'file://' : 'file:///'; + const moduleUrl = `${prefix}${workspacePath}/deploy/prepareTauriConfig.js`; + const { default: prepareTauriConfig } = await import(moduleUrl) + const config = prepareTauriConfig(); + + const configJson = JSON.stringify(config); + console.log(configJson); + + core.setOutput("json", configJson); + + - name: Build, package and publish + uses: tauri-apps/tauri-action@v0 + id: build-tauri + env: + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE_BASE64 }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} + BASE_URL: ${{ vars.BASE_URL }} + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.UPDATER_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.UPDATER_PRIVATE_KEY_PASSWORD }} + WITH_UPDATER: ${{ needs.get-version.outputs.should-publish != '' && 'true' || 'false' }} + with: + args: "-c ${{ steps.config-overrides.outputs.json }} ${{ matrix.settings.args }}" + includeDebug: ${{ needs.get-version.outputs.should-publish == '' && inputs.forceRelease != 'true' }} + includeRelease: ${{ needs.get-version.outputs.should-publish != '' || inputs.forceRelease == 'true' }} + releaseId: ${{ needs.create-release.outputs.releaseId }} + owner: ${{ steps.repository-info.outputs.owner }} + repo: ${{ steps.repository-info.outputs.repo }} + + - name: Get file info + id: file-info + shell: bash + run: | + FULL_PATH=$(echo "${{ fromJSON(steps.build-tauri.outputs.artifactPaths)[0] }}") + FILENAME=$(basename "$FULL_PATH") + NAME="${FILENAME%.*}" + FILE_PATH=$(cd "$(dirname "$FULL_PATH")" && pwd) + ARCHITECTURE=$(echo "${{ matrix.settings.args }}" | grep -oE 'x86_64|aarch64' || echo "") + echo "name=$NAME" >> $GITHUB_OUTPUT + echo "filename=$FILENAME" >> $GITHUB_OUTPUT + echo "architecture=$ARCHITECTURE" >> $GITHUB_OUTPUT + echo "path=$FILE_PATH" >> $GITHUB_OUTPUT + + # MacOS release — use the DMG produced by tauri-action directly + - name: Upload release asset (MacOS) + if: matrix.settings.platform == 'macos-latest' && needs.get-version.outputs.should-publish != '' + shell: bash + run: | + SANITIZED_FILENAME=$(echo "${{ steps.file-info.outputs.name }}" | sed 's/ /./g') + PUBLISH_FILE_NAME="$SANITIZED_FILENAME-${{ steps.file-info.outputs.architecture }}.dmg" + FILE_PATH="${{ steps.file-info.outputs.path }}/${{ steps.file-info.outputs.filename }}" + RELEASE_ID="${{ needs.create-release.outputs.releaseId }}" + curl -X POST -H "Authorization: Bearer $GH_TOKEN" \ + -H "Content-Type: application/octet-stream" \ + --data-binary "@$FILE_PATH" \ + "https://uploads.github.com/repos/$PUBLISH_REPO/releases/$RELEASE_ID/assets?name=$PUBLISH_FILE_NAME" + + - name: Upload artifact (MacOS) + if: matrix.settings.platform == 'macos-latest' + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.file-info.outputs.name }}-${{ steps.file-info.outputs.architecture }}.dmg + path: ${{ steps.file-info.outputs.path }}/${{ steps.file-info.outputs.filename }} + + # Linux release + - name: Upload Linux artifact (Linux) + if: matrix.settings.platform == 'ubuntu-22.04' + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.file-info.outputs.filename }} + path: ${{ steps.file-info.outputs.path }}/${{ steps.file-info.outputs.filename }} + + publish-release: + runs-on: ubuntu-latest + needs: [get-version, check-version, create-release, package-tauri] + if: needs.get-version.outputs.should-publish != '' && needs.check-version.outputs.should-skip != 'true' + env: + RELEASE_ID: ${{ needs.create-release.outputs.releaseId }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Publish release + run: | + curl -X PATCH -H "Authorization: Bearer $GH_TOKEN" -d '{"draft": false}' "https://api.github.com/repos/$PUBLISH_REPO/releases/$RELEASE_ID" + + - name: Update Gist with JSON + run: | + ASSET_ID=$(curl -s -H "Authorization: Bearer $GH_TOKEN" "https://api.github.com/repos/$PUBLISH_REPO/releases/$RELEASE_ID/assets" | jq -r '.[] | select(.name == "latest.json") | .id') + JSON_CONTENT=$(curl -sSL -H "Accept: application/octet-stream" -H "Authorization: token $GH_TOKEN" "https://api.github.com/repos/$PUBLISH_REPO/releases/assets/$ASSET_ID") + GIST_CONTENT=$(jq -n --arg json "$JSON_CONTENT" '{"files":{"updater.json":{"content":$json}}}') + curl -X PATCH -H "Authorization: token $GH_TOKEN" -d "$GIST_CONTENT" "https://api.github.com/gists/$UPDATER_GIST_ID" diff --git a/docs/src-tauri/01-architecture.md b/docs/src-tauri/01-architecture.md new file mode 100644 index 000000000..07708e2dc --- /dev/null +++ b/docs/src-tauri/01-architecture.md @@ -0,0 +1,143 @@ +# Rust Backend Architecture + +## Overview + +The Tauri Rust backend provides native functionality for the AlphaHuman desktop application: +- System tray with background execution +- Deep link authentication +- Secure session storage (OS Keychain) +- Socket.io state management +- Native notifications +- Window management + +## Directory Structure + +``` +src-tauri/src/ +├── lib.rs # Entry point, plugin registration, tray setup +├── main.rs # Binary entry (desktop) +├── commands/ # Tauri IPC commands +│ ├── mod.rs +│ ├── auth.rs # Authentication commands +│ ├── socket.rs # Socket state commands +│ ├── telegram.rs # Telegram login commands +│ └── window.rs # Window management commands +├── services/ # Background services +│ ├── mod.rs +│ ├── session_service.rs # Secure session storage (keychain) +│ ├── socket_service.rs # Socket.io state management +│ └── notification_service.rs # Native notifications +├── models/ # Data structures +│ ├── mod.rs +│ ├── auth.rs # Auth types (Session, User) +│ └── socket.rs # Socket types (ConnectionStatus) +└── utils/ # Configuration and helpers + ├── mod.rs + └── config.rs # Environment configuration +``` + +## Key Components + +### lib.rs - Application Entry + +The main entry point that: +1. Registers Tauri plugins (opener, deep-link, autostart, notification) +2. Sets up system tray with Show/Hide and Quit menu +3. Handles macOS-specific window close behavior (minimize to tray) +4. Registers all IPC commands + +### Commands Layer + +Commands are exposed to the frontend via Tauri's IPC: + +| Module | Commands | Purpose | +|--------|----------|---------| +| `auth` | `exchange_token`, `get_auth_state`, `logout`, etc. | Authentication | +| `socket` | `socket_connect`, `report_socket_connected`, etc. | Socket state | +| `telegram` | `start_telegram_login` | Telegram OAuth | +| `window` | `show_window`, `hide_window`, `toggle_window`, etc. | Window control | + +### Services Layer + +Singleton services providing background functionality: + +| Service | Purpose | Storage | +|---------|---------|---------| +| `SessionService` | Secure auth token storage | OS Keychain | +| `SocketService` | Socket.io state management | Memory + Events | +| `NotificationService` | Native notifications | N/A | + +## Architecture Decisions + +### Socket.io Strategy + +The frontend maintains the actual Socket.io connection, while Rust: +1. Stores connection parameters +2. Tracks connection state +3. Emits events to coordinate with frontend +4. Ensures socket stays connected when window is hidden + +This approach is necessary because: +- Rust's Socket.io libraries have API compatibility issues +- The WebView maintains state when hidden (unlike browser tabs) +- Frontend JavaScript is better suited for Socket.io's event-driven model + +### Keychain Storage + +Session tokens are stored in the OS keychain for security: +- **macOS**: Keychain +- **Windows**: Credential Manager +- **Linux**: Secret Service + +### Event Bridge Pattern + +Rust communicates with the frontend via Tauri events: + +```rust +// Rust emits event +app.emit("socket:should_connect", json!({ "backendUrl": url, "token": token })) + +// Frontend listens +listen("socket:should_connect", (event) => { + socketService.connect(event.payload.backendUrl, event.payload.token); +}); +``` + +## Plugin Dependencies + +| Plugin | Version | Purpose | +|--------|---------|---------| +| `tauri-plugin-opener` | 2 | Open URLs in browser | +| `tauri-plugin-deep-link` | 2.0.0 | Handle `outsourced://` URLs | +| `tauri-plugin-autostart` | 2 | Launch at login | +| `tauri-plugin-notification` | 2 | Native notifications | + +## Cargo Dependencies + +| Crate | Purpose | +|-------|---------| +| `tauri` | Core framework with tray and macOS APIs | +| `serde`, `serde_json` | Serialization | +| `reqwest` | HTTP client | +| `tokio` | Async runtime | +| `keyring` | Secure credential storage | +| `once_cell` | Lazy static singletons | +| `parking_lot` | Fast mutexes | +| `log`, `env_logger` | Logging | + +## Platform-Specific Behavior + +### macOS +- Window close button hides instead of quitting +- Dock icon click shows window +- LaunchAgent for autostart +- Keychain for secure storage + +### Windows/Linux +- Deep link registration at runtime +- Platform-specific credential storage +- Registry (Windows) or desktop file (Linux) autostart + +--- + +*Next: [Commands Reference](./02-commands.md)* diff --git a/docs/src-tauri/02-commands.md b/docs/src-tauri/02-commands.md new file mode 100644 index 000000000..f5d471fa2 --- /dev/null +++ b/docs/src-tauri/02-commands.md @@ -0,0 +1,268 @@ +# Tauri Commands Reference + +This document lists all Tauri commands available to the frontend. + +## Authentication Commands + +### `exchange_token` +Exchange a login token for a session token. + +```typescript +const result = await invoke('exchange_token', { + backendUrl: 'https://api.example.com', + token: 'login-token-from-deep-link' +}); +// Returns: { sessionToken: string, user: User } +``` + +### `get_auth_state` +Get current authentication state. + +```typescript +const state = await invoke('get_auth_state'); +// Returns: { is_authenticated: boolean, user: User | null } +``` + +### `get_session_token` +Get the current session token. + +```typescript +const token = await invoke('get_session_token'); +``` + +### `get_current_user` +Get the current authenticated user. + +```typescript +const user = await invoke('get_current_user'); +``` + +### `is_authenticated` +Check if user is authenticated. + +```typescript +const isAuth = await invoke('is_authenticated'); +``` + +### `logout` +Clear session and disconnect socket. + +```typescript +await invoke('logout'); +``` + +### `store_session` +Manually store a session (usually called automatically by `exchange_token`). + +```typescript +await invoke('store_session', { + token: 'session-token', + user: { id: '123', firstName: 'John' } +}); +``` + +## Socket Commands + +### `socket_connect` +Request frontend to connect to socket server. + +```typescript +await invoke('socket_connect', { + backendUrl: 'https://api.example.com', + token: 'session-token' +}); +// Emits "socket:should_connect" event +``` + +### `socket_disconnect` +Request frontend to disconnect from socket server. + +```typescript +await invoke('socket_disconnect'); +// Emits "socket:should_disconnect" event +``` + +### `get_socket_state` +Get current socket state. + +```typescript +const state = await invoke('get_socket_state'); +// Returns: { status: 'connected' | 'disconnected' | ..., socket_id: string | null, error: string | null } +``` + +### `is_socket_connected` +Check if socket is connected. + +```typescript +const isConnected = await invoke('is_socket_connected'); +``` + +### `report_socket_connected` +Report that socket connected (called by frontend). + +```typescript +await invoke('report_socket_connected', { socketId: 'socket-id' }); +``` + +### `report_socket_disconnected` +Report that socket disconnected (called by frontend). + +```typescript +await invoke('report_socket_disconnected'); +``` + +### `report_socket_error` +Report socket error (called by frontend). + +```typescript +await invoke('report_socket_error', { error: 'Connection failed' }); +``` + +### `update_socket_status` +Update socket status (called by frontend). + +```typescript +await invoke('update_socket_status', { + status: 'connected', // 'connected' | 'connecting' | 'disconnected' | 'reconnecting' | 'error' + socketId: 'socket-id' +}); +``` + +## Telegram Commands + +### `start_telegram_login` +Open Telegram login widget in browser. + +```typescript +await invoke('start_telegram_login'); +// Opens ${DEFAULT_BACKEND_URL}/auth/telegram-widget?redirect=outsourced://auth +``` + +### `start_telegram_login_with_url` +Open Telegram login widget with custom backend URL. + +```typescript +await invoke('start_telegram_login_with_url', { + backendUrl: 'https://custom-backend.com' +}); +``` + +## Window Commands + +### `show_window` +Show and focus the main window. + +```typescript +await invoke('show_window'); +``` + +### `hide_window` +Hide the main window. + +```typescript +await invoke('hide_window'); +``` + +### `toggle_window` +Toggle window visibility. + +```typescript +await invoke('toggle_window'); +``` + +### `is_window_visible` +Check if window is visible. + +```typescript +const isVisible = await invoke('is_window_visible'); +``` + +### `minimize_window` +Minimize the window. + +```typescript +await invoke('minimize_window'); +``` + +### `maximize_window` +Maximize or unmaximize the window. + +```typescript +await invoke('maximize_window'); +``` + +### `close_window` +Close the window (minimizes to tray on macOS). + +```typescript +await invoke('close_window'); +``` + +### `set_window_title` +Set the window title. + +```typescript +await invoke('set_window_title', { title: 'New Title' }); +``` + +## Events (Rust → Frontend) + +These events are emitted by Rust and can be listened to in the frontend: + +### Socket Events + +| Event | Payload | Description | +|-------|---------|-------------| +| `socket:connected` | `()` | Socket connected | +| `socket:disconnected` | `()` | Socket disconnected | +| `socket:error` | `string` | Socket error occurred | +| `socket:message` | `{ event: string, data: any }` | Socket message received | +| `socket:state_changed` | `SocketState` | Socket state changed | +| `socket:should_connect` | `{ backendUrl: string, token: string }` | Request to connect | +| `socket:should_disconnect` | `()` | Request to disconnect | + +### Listening to Events + +```typescript +import { listen } from '@tauri-apps/api/event'; + +// Listen for socket connection request +await listen('socket:should_connect', (event) => { + const { backendUrl, token } = event.payload; + socketService.connect(backendUrl, token); +}); + +// Listen for state changes +await listen('socket:state_changed', (event) => { + const state = event.payload as SocketState; + dispatch(setSocketStatus(state.status)); +}); +``` + +## Type Definitions + +```typescript +interface AuthState { + is_authenticated: boolean; + user: User | null; +} + +interface User { + id: string; + firstName?: string; + lastName?: string; + username?: string; + email?: string; + telegramId?: string; +} + +interface SocketState { + status: 'connected' | 'connecting' | 'disconnected' | 'reconnecting' | 'error'; + socket_id: string | null; + error?: string; +} +``` + +--- + +*Previous: [Architecture](./01-architecture.md) | Next: [Services](./03-services.md)* diff --git a/docs/src-tauri/03-services.md b/docs/src-tauri/03-services.md new file mode 100644 index 000000000..7175bed3f --- /dev/null +++ b/docs/src-tauri/03-services.md @@ -0,0 +1,304 @@ +# Services Documentation + +This document describes the background services in the Rust backend. + +## SessionService + +Manages user sessions with secure OS keychain storage. + +### Location +`src-tauri/src/services/session_service.rs` + +### Purpose +- Store authentication tokens securely in OS keychain +- Cache session in memory for fast access +- Persist sessions across app restarts + +### API + +```rust +pub struct SessionService { + // ... +} + +impl SessionService { + /// Create a new SessionService (loads from keychain) + pub fn new() -> Self; + + /// Store a new session + pub fn store_session(&self, token: &str, user: &User) -> Result<(), String>; + + /// Get the current session token + pub fn get_token(&self) -> Option; + + /// Get the current session + pub fn get_session(&self) -> Option; + + /// Get the current user + pub fn get_user(&self) -> Option; + + /// Check if there's an active session + pub fn is_authenticated(&self) -> bool; + + /// Clear the current session (logout) + pub fn clear_session(&self) -> Result<(), String>; +} +``` + +### Keychain Storage + +| Platform | Storage Backend | +|----------|-----------------| +| macOS | Keychain | +| Windows | Credential Manager | +| Linux | Secret Service (libsecret) | + +### Stored Data + +```json +{ + "token": "jwt-session-token", + "user_id": "user-uuid", + "user": { + "id": "user-uuid", + "firstName": "John", + "lastName": "Doe" + }, + "created_at": 1706540000, + "expires_at": null +} +``` + +### Usage + +```rust +use crate::commands::auth::SESSION_SERVICE; + +// Store session +SESSION_SERVICE.store_session("token", &user)?; + +// Get token +if let Some(token) = SESSION_SERVICE.get_token() { + // Use token +} + +// Check auth +if SESSION_SERVICE.is_authenticated() { + // User is logged in +} + +// Logout +SESSION_SERVICE.clear_session()?; +``` + +--- + +## SocketService + +Manages Socket.io connection state and coordinates with the frontend. + +### Location +`src-tauri/src/services/socket_service.rs` + +### Purpose +- Track socket connection state +- Store connection parameters for reconnection +- Emit events to frontend for connection control +- Enable background socket persistence + +### Architecture + +The actual Socket.io client runs in the frontend (JavaScript). The Rust service: +1. Stores connection parameters (URL, token) +2. Tracks state reported by frontend +3. Emits events to request connect/disconnect +4. Enables socket to persist when window is hidden + +### API + +```rust +pub struct SocketService { + // ... +} + +impl SocketService { + /// Create a new SocketService + pub fn new() -> Self; + + /// Set app handle for event emission + pub fn set_app_handle(&self, handle: AppHandle); + + /// Get current connection status + pub fn get_status(&self) -> ConnectionStatus; + + /// Get current socket state + pub fn get_state(&self) -> SocketState; + + /// Check if connected + pub fn is_connected(&self) -> bool; + + /// Request frontend to connect + pub fn request_connect(&self, backend_url: &str, token: &str) -> Result<(), String>; + + /// Request frontend to disconnect + pub fn request_disconnect(&self) -> Result<(), String>; + + /// Update status (called by frontend via command) + pub fn update_status(&self, status: ConnectionStatus, socket_id: Option); + + /// Report connection (called by frontend) + pub fn report_connected(&self, socket_id: Option); + + /// Report disconnection (called by frontend) + pub fn report_disconnected(&self); + + /// Report error (called by frontend) + pub fn report_error(&self, error: &str); + + /// Get stored connection params for reconnection + pub fn get_connection_params(&self) -> Option<(String, String)>; + + /// Clear stored credentials + pub fn clear_credentials(&self); +} +``` + +### Events Emitted + +| Event | Payload | When | +|-------|---------|------| +| `socket:should_connect` | `{ backendUrl, token }` | `request_connect` called | +| `socket:should_disconnect` | `()` | `request_disconnect` called | +| `socket:state_changed` | `SocketState` | State changes | +| `socket:error` | `string` | Error reported | + +### Connection States + +```rust +pub enum ConnectionStatus { + Disconnected, + Connecting, + Connected, + Reconnecting, + Error, +} +``` + +### Usage + +```rust +use crate::services::socket_service::SOCKET_SERVICE; + +// Initialize with app handle +SOCKET_SERVICE.set_app_handle(app.handle()); + +// Request connection (emits event to frontend) +SOCKET_SERVICE.request_connect("https://api.example.com", "token")?; + +// Check status +if SOCKET_SERVICE.is_connected() { + // Socket is connected +} + +// Frontend reports status via commands +// invoke('report_socket_connected', { socketId: 'abc' }) +``` + +### Background Persistence + +When the window is hidden: +1. The Tauri app continues running (tray icon) +2. The WebView is not destroyed, just hidden +3. Socket.io connection in JavaScript stays active +4. Frontend continues receiving messages +5. User can show window to see updates + +--- + +## NotificationService + +Shows native desktop notifications. + +### Location +`src-tauri/src/services/notification_service.rs` + +### Purpose +- Show native notifications +- Check notification permission +- Request notification permission + +### API + +```rust +pub struct NotificationService; + +impl NotificationService { + /// Show a simple notification + pub fn show(app: &AppHandle, title: &str, body: &str) -> Result<(), String>; + + /// Show a notification with an icon + pub fn show_with_icon( + app: &AppHandle, + title: &str, + body: &str, + icon: &str, + ) -> Result<(), String>; + + /// Show a notification for a new message + pub fn show_message_notification( + app: &AppHandle, + sender: &str, + message: &str, + ) -> Result<(), String>; + + /// Check if notifications are permitted + pub fn is_permission_granted(app: &AppHandle) -> Result; + + /// Request notification permission + pub fn request_permission(app: &AppHandle) -> Result; +} +``` + +### Usage + +```rust +use crate::services::notification_service::NotificationService; + +// Show notification +NotificationService::show(&app, "New Message", "You have a new message")?; + +// Show message notification +NotificationService::show_message_notification(&app, "John", "Hey, how are you?")?; + +// Check permission +if NotificationService::is_permission_granted(&app)? { + // Can show notifications +} +``` + +--- + +## Service Initialization + +Services are initialized as singletons in their respective modules: + +```rust +// In auth.rs +pub static SESSION_SERVICE: Lazy> = + Lazy::new(|| Arc::new(SessionService::new())); + +// In socket_service.rs +pub static SOCKET_SERVICE: Lazy> = + Lazy::new(|| Arc::new(SocketService::new())); +``` + +The SocketService's app handle is set during app setup: + +```rust +// In lib.rs setup() +SOCKET_SERVICE.set_app_handle(app.handle().clone()); +``` + +--- + +*Previous: [Commands Reference](./02-commands.md) | [Back to Index](./README.md)* diff --git a/docs/src-tauri/README.md b/docs/src-tauri/README.md new file mode 100644 index 000000000..1d5755519 --- /dev/null +++ b/docs/src-tauri/README.md @@ -0,0 +1,374 @@ +# Rust Backend Documentation + +## Overview + +This documentation covers the Tauri Rust backend for the AlphaHuman desktop application. + +## Quick Reference + +| Document | Description | +|----------|-------------| +| [Architecture](./01-architecture.md) | System architecture and module structure | +| [Commands Reference](./02-commands.md) | All Tauri IPC commands | +| [Services](./03-services.md) | Background services documentation | + +## Features Implemented + +1. **System Tray** - Background execution with menu bar icon +2. **Telegram Widget Login** → Deep link session creation +3. **Socket.io State Management** - Persistent background connection +4. **Secure Session Storage** - OS Keychain integration +5. **Native Notifications** - Desktop notifications +6. **Cross-Platform** - macOS, Windows, Linux ready + +## Current State Analysis + +### Existing Implementation (`lib.rs`) +- ✅ System tray with show/hide/quit +- ✅ Deep link handling (`outsourced://` scheme) +- ✅ Token exchange command (CORS bypass) +- ✅ Autostart plugin (macOS LaunchAgent) +- ✅ Window minimize-to-tray on close (macOS) + +### Missing Features +- ❌ Socket.io client in Rust (background persistence) +- ❌ Telegram Widget integration +- ❌ Session management in Rust +- ❌ Background service architecture +- ❌ Notification system +- ❌ State persistence (keychain/secure storage) + +--- + +## Implementation Plan + +### Phase 1: Project Structure Refactoring + +**Goal**: Modular architecture for maintainability + +``` +src-tauri/src/ +├── lib.rs # Entry point, plugin registration +├── main.rs # Binary entry (unchanged) +├── commands/ # Tauri commands (IPC) +│ ├── mod.rs +│ ├── auth.rs # Token exchange, session management +│ ├── socket.rs # Socket connection control +│ └── telegram.rs # Telegram-specific commands +├── services/ # Background services +│ ├── mod.rs +│ ├── socket_service.rs # Persistent Socket.io client +│ ├── session_service.rs # Secure session storage +│ └── notification_service.rs # Native notifications +├── models/ # Data structures +│ ├── mod.rs +│ ├── auth.rs # Auth types +│ └── socket.rs # Socket message types +└── utils/ # Helpers + ├── mod.rs + └── config.rs # Environment configuration +``` + +### Phase 2: Socket.io Background Service + +**Goal**: Persistent WebSocket connection even when app is in background + +**Dependencies to add:** +```toml +[dependencies] +rust_socketio = "0.6" # Socket.io client +tokio = { version = "1", features = ["full", "sync"] } +once_cell = "1.19" # Lazy static for singleton +parking_lot = "0.12" # Fast mutexes +``` + +**Implementation:** +```rust +// services/socket_service.rs +pub struct SocketService { + client: Option, + auth_token: Option, + is_connected: AtomicBool, +} + +impl SocketService { + pub async fn connect(&self, token: &str) -> Result<(), Error>; + pub async fn disconnect(&self) -> Result<(), Error>; + pub async fn emit(&self, event: &str, data: Value) -> Result<(), Error>; + pub fn is_connected(&self) -> bool; +} +``` + +**Background Persistence:** +- Socket runs on Tokio runtime, independent of window state +- Connection survives window hide/minimize +- Auto-reconnect on network recovery +- Heartbeat/ping to keep connection alive + +### Phase 3: Telegram Widget Login Flow + +**Goal**: Web-based Telegram auth → Deep link callback → Native session + +**Flow:** +``` +1. User clicks "Login with Telegram" in desktop app + ↓ +2. App opens system browser to: + ${BACKEND_URL}/auth/telegram-widget?redirect=outsourced://auth + ↓ +3. Backend serves Telegram Login Widget HTML page + ↓ +4. User authenticates with Telegram + ↓ +5. Telegram callback → Backend validates → Creates loginToken + ↓ +6. Backend redirects to: outsourced://auth?token={loginToken} + ↓ +7. Desktop app catches deep link + ↓ +8. Rust `exchange_token` → Backend exchanges for sessionToken + ↓ +9. Session stored securely (Keychain on macOS) + ↓ +10. Socket connects with session token +``` + +**Backend Endpoint Needed:** +``` +GET /auth/telegram-widget?redirect={deeplink_scheme} +``` +Returns HTML page with Telegram Login Widget that redirects to the specified scheme. + +**Commands to implement:** +```rust +#[tauri::command] +async fn start_telegram_login(app: AppHandle) -> Result<(), String> { + // Open browser to Telegram widget page + let url = format!("{}/auth/telegram-widget?redirect=outsourced://auth", BACKEND_URL); + opener::open(&url)?; + Ok(()) +} + +#[tauri::command] +async fn get_session() -> Result, String> { + // Return current session from secure storage +} + +#[tauri::command] +async fn logout(app: AppHandle) -> Result<(), String> { + // Clear session, disconnect socket +} +``` + +### Phase 4: Secure Session Storage + +**Goal**: Store auth tokens securely using OS keychain + +**Dependencies:** +```toml +[dependencies] +keyring = "3" # Cross-platform keychain access +``` + +**Implementation:** +```rust +// services/session_service.rs +pub struct SessionService { + keyring: Entry, +} + +impl SessionService { + const SERVICE: &'static str = "com.megamind.tauri-app"; + + pub fn store_token(&self, token: &str) -> Result<(), Error>; + pub fn get_token(&self) -> Result, Error>; + pub fn clear_token(&self) -> Result<(), Error>; +} +``` + +**Platform Support:** +- macOS: Keychain +- Windows: Credential Manager +- Linux: Secret Service (libsecret) + +### Phase 5: Native Notifications + +**Goal**: Show notifications even when app is minimized + +**Dependencies:** +```toml +[dependencies] +tauri-plugin-notification = "2" +``` + +**Capability Addition:** +```json +{ + "permissions": [ + "notification:default", + "notification:allow-notify", + "notification:allow-request-permission" + ] +} +``` + +**Usage:** +```rust +// services/notification_service.rs +pub fn show_notification(title: &str, body: &str) -> Result<(), Error> { + Notification::new() + .title(title) + .body(body) + .show()?; + Ok(()) +} +``` + +### Phase 6: Event Bridge (Rust ↔ Frontend) + +**Goal**: Bidirectional communication between Rust services and React frontend + +**Events from Rust to Frontend:** +```rust +// Emit to frontend when socket receives message +app.emit("socket:message", payload)?; +app.emit("socket:connected", ())?; +app.emit("socket:disconnected", ())?; +app.emit("telegram:notification", notification)?; +``` + +**Frontend listening:** +```typescript +import { listen } from '@tauri-apps/api/event'; + +await listen('socket:message', (event) => { + // Handle message from Rust socket service +}); +``` + +### Phase 7: MCP Integration in Rust + +**Goal**: Run MCP tools from Rust for performance-critical operations + +**Approach:** +- Keep MCP tools in TypeScript for flexibility +- Rust handles socket transport +- Frontend dispatches tool calls +- Rust forwards via socket, returns results + +**Alternative (Full Rust MCP):** +- Implement tool handlers in Rust +- Higher performance, but more maintenance +- Consider for v2 + +--- + +## Implementation Order + +| Phase | Priority | Effort | Dependencies | +|-------|----------|--------|--------------| +| 1. Project Structure | High | 2h | None | +| 2. Socket.io Service | High | 4h | Phase 1 | +| 3. Telegram Widget Login | High | 3h | Backend endpoint | +| 4. Secure Storage | High | 2h | Phase 1 | +| 5. Notifications | Medium | 1h | Phase 2 | +| 6. Event Bridge | High | 2h | Phase 2 | +| 7. MCP Integration | Low | 4h+ | Phase 2, 6 | + +**Total Estimated Effort**: 18+ hours + +--- + +## Cross-Platform Considerations + +### macOS +- ✅ System tray (menu bar) +- ✅ LaunchAgent autostart +- ✅ Keychain storage +- ✅ Deep link via Info.plist +- ⚠️ Notarization for distribution + +### Windows +- ✅ System tray +- ✅ Registry autostart +- ✅ Credential Manager storage +- ✅ Deep link via registry +- ⚠️ Code signing for SmartScreen + +### Linux +- ✅ System tray (AppIndicator) +- ✅ Desktop file autostart +- ✅ Secret Service storage +- ⚠️ Deep link varies by desktop environment + +### Mobile (Future) +- ❌ No system tray +- ❌ Different auth flow +- ❌ Push notifications instead of socket +- Consider separate implementation + +--- + +## Testing Strategy + +### Unit Tests +```rust +#[cfg(test)] +mod tests { + #[tokio::test] + async fn test_socket_connect() { ... } + + #[test] + fn test_session_storage() { ... } +} +``` + +### Integration Tests +- Deep link flow end-to-end +- Socket reconnection scenarios +- Background persistence verification + +### Manual Testing +- Build debug `.app` bundle +- Test tray behavior +- Test window minimize/restore +- Test background socket + +--- + +## Files to Create/Modify + +### New Files +- `src-tauri/src/commands/mod.rs` +- `src-tauri/src/commands/auth.rs` +- `src-tauri/src/commands/socket.rs` +- `src-tauri/src/commands/telegram.rs` +- `src-tauri/src/services/mod.rs` +- `src-tauri/src/services/socket_service.rs` +- `src-tauri/src/services/session_service.rs` +- `src-tauri/src/services/notification_service.rs` +- `src-tauri/src/models/mod.rs` +- `src-tauri/src/models/auth.rs` +- `src-tauri/src/models/socket.rs` +- `src-tauri/src/utils/mod.rs` +- `src-tauri/src/utils/config.rs` + +### Modified Files +- `src-tauri/Cargo.toml` - Add dependencies +- `src-tauri/src/lib.rs` - Refactor, use modules +- `src-tauri/capabilities/default.json` - Add permissions + +--- + +## Success Criteria + +1. ✅ User can log in via Telegram widget +2. ✅ Session persists across app restarts +3. ✅ Socket stays connected when app is minimized +4. ✅ Notifications appear for new messages +5. ✅ All web features work in desktop app +6. ✅ Cross-platform compatible architecture + +--- + +*Plan created by stevenbaba - 2026-01-29* diff --git a/docs/src/01-architecture.md b/docs/src/01-architecture.md new file mode 100644 index 000000000..9d0ac2559 --- /dev/null +++ b/docs/src/01-architecture.md @@ -0,0 +1,175 @@ +# Architecture Overview + +## System Architecture + +The Outsourced platform is built on a layered architecture supporting: +- Redux-based state management with persistence +- Socket.io real-time communication +- Telegram MTProto integration via service layer +- 81-tool MCP (Model Context Protocol) system for AI interactions +- Multi-step onboarding flow +- URL-based settings modal system +- Deep link authentication handoff +- Cross-platform compatibility (desktop + mobile) + +## Entry Points + +| File | Purpose | +|------|---------| +| `main.tsx` | React root, polyfill imports, lazy deep link listener init | +| `App.tsx` | Provider chain: Redux → PersistGate → Socket → Telegram → HashRouter | +| `AppRoutes.tsx` | Route definitions with route guards | +| `polyfills.ts` | Node.js polyfills (Buffer, process, util) for telegram npm package | + +## Provider Chain + +The application wraps components in a specific order due to dependencies: + +``` +Redux Provider + └─ PersistGate (rehydrate auth + telegram state from localStorage) + └─ UserProvider + └─ SocketProvider (manages Socket.io connection + MCP init) + └─ TelegramProvider (manages MTProto connection) + └─ HashRouter + └─ AppRoutes (route definitions + SettingsModal overlay) +``` + +**Why this order matters:** +1. Redux must be outermost for state access +2. PersistGate rehydrates persisted state before rendering +3. SocketProvider depends on Redux auth token +4. TelegramProvider depends on Redux telegram state +5. HashRouter provides navigation context to all routes + +## Module Relationships + +``` +main.tsx (entry) + ↓ +App.tsx (providers chain) + ├─ Redux Store ←→ Persist + ├─ SocketProvider + │ ├─ listens to auth.token changes + │ ├─ calls socketService.connect(token) + │ └─ init MCP server when socket connected + ├─ TelegramProvider + │ ├─ listens to telegram state + │ ├─ calls mtprotoService.initialize(userId) + │ └─ exposes useTelegram hook + └─ HashRouter + AppRoutes + ├─ ProtectedRoute ← checks Redux auth + onboarded + ├─ PublicRoute ← redirects authenticated users + ├─ pages/Home + │ ├─ uses useUser hook → calls userApi + │ └─ uses useNavigate → opens /settings + ├─ pages/Login + │ ├─ uses TelegramLoginButton + │ └─ calls deeplink.ts functions + └─ SettingsModal + ├─ listens to location.pathname + ├─ uses useSettingsNavigation hook + └─ renders panels with Redux state +``` + +## Services Layer + +``` +Services Layer: + ├─ apiClient (singleton) + │ ├─ reads auth.token from Redux + │ └─ makes HTTP requests to BACKEND_URL + ├─ socketService (singleton) + │ ├─ manages Socket.io connection + │ └─ emits/listens for MCP messages + └─ mtprotoService (singleton) + ├─ manages TelegramClient + └─ stores session in Redux telegram.byUser[userId].sessionString +``` + +## MCP System + +``` +MCP System: + ├─ TelegramMCPServer (instantiated in SocketProvider) + ├─ SocketIOMCPTransport (wraps socketService) + └─ 81 tools in telegram/tools/ + ├─ each tool calls mtprotoService + └─ results returned via socket transport to backend MCP client +``` + +## Data Flow + +### Authentication Flow (Deep Link) +1. User authenticates in browser → receives `loginToken` +2. Browser redirects to `outsourced://auth?token=` +3. Desktop app catches deep link via Tauri plugin +4. `desktopDeepLinkListener` invokes Rust `exchange_token` command +5. Rust calls backend `POST /auth/desktop-exchange` (CORS-free) +6. Backend returns `{ sessionToken, user }` +7. App stores session in Redux, navigates to `/onboarding` or `/home` + +### Socket.io Connection Flow +1. SocketProvider detects `auth.token` change +2. Calls `socketService.connect(token)` +3. On successful connection, initializes MCP server +4. MCP server registers 81 Telegram tools +5. Backend can invoke tools via JSON-RPC over Socket.io + +### Telegram Connection Flow +1. TelegramProvider detects auth state +2. Calls `mtprotoService.initialize(userId)` and `connect()` in parallel +3. MTProto client authenticates with Telegram servers +4. Session string stored in Redux `telegram.byUser[userId].sessionString` +5. Chats/messages fetched and stored in Redux + +## Key Patterns + +### No localStorage Usage +- **Rule**: Avoid `localStorage`; use Redux with persistence instead +- **Exceptions**: Redux-persist's storage adapter (persistence layer) +- **Telegram session**: Stored in `telegram.byUser[userId].sessionString` + +### Route Guard Pattern +- **PublicRoute**: Redirects authenticated users away +- **ProtectedRoute**: Requires `token` and optionally `isOnboarded` +- **DefaultRedirect**: Fallback based on auth state + +### Settings Modal Pattern +- Renders via `createPortal` when `location.pathname.startsWith('/settings')` +- URL-based navigation without affecting main route +- Uses `useSettingsNavigation` hook + +### MCP Tool Pattern +Each tool exports a handler: +```typescript +export const toolName: TelegramMCPToolHandler = { + call: async (args, { telegramClient, userId }) => { + // Perform Telegram API operation + return { success: true, data: result }; + } +} +``` + +## File Organization + +### Feature-Based Structure +- `pages/` - Full-page route components +- `pages/onboarding/` - Onboarding flow with steps +- `components/settings/` - Settings modal system +- `lib/mcp/telegram/tools/` - Individual MCP tools + +### Slice-Based State +- `store/authSlice.ts` - Authentication +- `store/socketSlice.ts` - Socket connection +- `store/userSlice.ts` - User profile +- `store/telegram/` - Complex Telegram state (5 files) + +### Singleton Services +- `services/socketService.ts` - Socket.io +- `services/mtprotoService.ts` - Telegram MTProto +- `services/apiClient.ts` - HTTP REST + +--- + +*Next: [State Management](./02-state-management.md)* diff --git a/docs/src/02-state-management.md b/docs/src/02-state-management.md new file mode 100644 index 000000000..5a0d55d0c --- /dev/null +++ b/docs/src/02-state-management.md @@ -0,0 +1,243 @@ +# State Management + +The application uses Redux Toolkit with Redux-Persist for robust state management. + +## Store Configuration + +**File:** `store/index.ts` + +```typescript +// Combines all slices with persistence +const persistConfig = { + key: 'root', + storage, + whitelist: ['auth', 'telegram'] // Persisted slices +}; +``` + +## Redux State Structure + +```typescript +RootState = { + auth: { + token: string | null, // JWT (persisted) + isOnboardedByUser: Record // Per-user flag (persisted) + }, + socket: { + byUser: Record + }, + user: { + profile: User | null, + loading: boolean, + error: string | null + }, + telegram: { + byUser: Record // Per Telegram user (persisted) + } +} +``` + +## Slices + +### Auth Slice (`store/authSlice.ts`) + +Manages JWT token and per-user onboarding status. + +**State:** +```typescript +interface AuthState { + token: string | null; + isOnboardedByUser: Record; +} +``` + +**Actions:** +- `setToken(token: string)` - Store JWT after login +- `clearToken()` - Remove token on logout +- `setOnboarded({ userId, isOnboarded })` - Mark user as onboarded + +**Selectors (`store/authSelectors.ts`):** +- `selectToken` - Get current JWT +- `selectIsOnboarded(userId)` - Check if user completed onboarding + +### Socket Slice (`store/socketSlice.ts`) + +Tracks Socket.io connection status per user. + +**State:** +```typescript +interface SocketState { + byUser: Record; +} +``` + +**Actions:** +- `setSocketStatus({ userId, status })` - Update connection status +- `setSocketId({ userId, socketId })` - Store socket ID +- `clearSocketState(userId)` - Clear user's socket state + +**Selectors (`store/socketSelectors.ts`):** +- `selectSocketStatus(userId)` - Get connection status +- `selectIsSocketConnected(userId)` - Boolean connected check + +### User Slice (`store/userSlice.ts`) + +Stores user profile data. + +**State:** +```typescript +interface UserState { + profile: User | null; + loading: boolean; + error: string | null; +} +``` + +**Actions:** +- `setUser(user)` - Store user profile +- `setUserLoading(loading)` - Set loading state +- `setUserError(error)` - Set error state +- `clearUser()` - Clear profile on logout + +### Telegram Slice (`store/telegram/`) + +Complex nested state management for Telegram integration. + +**Files:** +- `index.ts` - Slice exports (actions, thunks) +- `types.ts` - Entity and state interfaces +- `reducers.ts` - Synchronous reducers +- `extraReducers.ts` - Async thunk handlers +- `thunks.ts` - Async operations + +**State Structure:** +```typescript +telegram.byUser[telegramUserId] = { + connectionStatus: "disconnected" | "connecting" | "connected" | "error", + authStatus: "not_authenticated" | "authenticating" | "authenticated" | "error", + currentUser: TelegramUser | null, + sessionString: string | null, // Stored here, NOT localStorage + chats: Record, + chatsOrder: string[], + messages: Record>, + threads: Record +} +``` + +**Reducers:** +- `setCurrentUser` - Store authenticated Telegram user +- `setSessionString` - Store MTProto session (for persistence) +- `setConnectionStatus` - Update connection state +- `setAuthStatus` - Update authentication state +- `addChat` / `updateChat` - Manage chat list +- `addMessage` / `updateMessage` - Manage message history +- `setThreads` - Store thread data + +**Thunks (`store/telegram/thunks.ts`):** +- `initializeTelegram(userId)` - Initialize MTProto client +- `connectTelegram(userId)` - Establish Telegram connection +- `fetchChats(userId)` - Load chat list +- `fetchMessages({ userId, chatId })` - Load message history +- `disconnectTelegram(userId)` - Clean disconnect + +**Selectors (`store/telegramSelectors.ts`):** +- `selectTelegramState(userId)` - Get full Telegram state +- `selectTelegramConnectionStatus(userId)` - Get connection status +- `selectTelegramAuthStatus(userId)` - Get auth status +- `selectTelegramChats(userId)` - Get chat list +- `selectTelegramMessages(userId, chatId)` - Get messages for chat + +## Typed Hooks + +**File:** `store/hooks.ts` + +```typescript +// Use these instead of plain useDispatch/useSelector +export const useAppDispatch: () => AppDispatch = useDispatch; +export const useAppSelector: TypedUseSelectorHook = useSelector; +``` + +## Persistence Configuration + +### What's Persisted +- `auth.token` - JWT for authentication +- `auth.isOnboardedByUser` - Per-user onboarding status +- `telegram.byUser` - Telegram state (sessions, chats, etc.) + +### What's NOT Persisted +- `socket` - Connection state (reconnects on app start) +- `user.loading` / `user.error` - Transient UI states +- Telegram loading/error states + +### Storage Backend +Redux-Persist uses localStorage adapter by default. This is the ONLY acceptable use of localStorage in the application. + +## Usage Examples + +### Reading State +```typescript +import { useAppSelector } from '../store/hooks'; + +function MyComponent() { + const token = useAppSelector((state) => state.auth.token); + const isConnected = useAppSelector((state) => + state.socket.byUser[userId]?.status === 'connected' + ); + const chats = useAppSelector((state) => + state.telegram.byUser[userId]?.chats + ); +} +``` + +### Dispatching Actions +```typescript +import { useAppDispatch } from '../store/hooks'; +import { setToken, clearToken } from '../store/authSlice'; +import { initializeTelegram } from '../store/telegram/thunks'; + +function MyComponent() { + const dispatch = useAppDispatch(); + + // Sync action + const handleLogin = (token: string) => { + dispatch(setToken(token)); + }; + + // Async thunk + const handleConnect = async () => { + await dispatch(initializeTelegram(userId)).unwrap(); + }; +} +``` + +### Using Selectors +```typescript +import { useAppSelector } from '../store/hooks'; +import { selectIsOnboarded } from '../store/authSelectors'; +import { selectTelegramConnectionStatus } from '../store/telegramSelectors'; + +function MyComponent({ userId }) { + const isOnboarded = useAppSelector((state) => selectIsOnboarded(state, userId)); + const connectionStatus = useAppSelector((state) => + selectTelegramConnectionStatus(state, userId) + ); +} +``` + +## Best Practices + +1. **Always use typed hooks** - `useAppDispatch` and `useAppSelector` +2. **Use selectors for derived state** - Memoized and testable +3. **Keep thunks in separate files** - Better organization +4. **Per-user state scoping** - Key state by user ID +5. **Avoid localStorage** - Use Redux-Persist instead + +--- + +*Previous: [Architecture Overview](./01-architecture.md) | Next: [Services Layer](./03-services.md)* diff --git a/docs/src/03-services.md b/docs/src/03-services.md new file mode 100644 index 000000000..973c10f73 --- /dev/null +++ b/docs/src/03-services.md @@ -0,0 +1,297 @@ +# Services Layer + +The application uses singleton services for external communication. This prevents connection leaks and provides consistent API access. + +## Service Architecture + +``` +Services Layer + ├─ apiClient (HTTP REST) + │ ├─ reads auth.token from Redux + │ └─ makes requests to BACKEND_URL + ├─ socketService (Socket.io) + │ ├─ manages real-time connection + │ └─ emits/listens for MCP messages + └─ mtprotoService (Telegram) + ├─ manages TelegramClient + └─ stores session in Redux +``` + +## API Client (`services/apiClient.ts`) + +HTTP REST client for backend communication. + +### Features +- Fetch-based implementation +- Auto-injects JWT from Redux store +- Typed request/response handling +- Error handling with typed errors + +### Usage +```typescript +import apiClient from '../services/apiClient'; + +// GET request +const user = await apiClient.get('/users/me'); + +// POST request +const result = await apiClient.post('/auth/login', { + email, + password +}); + +// With custom headers +const data = await apiClient.get('/endpoint', { + headers: { 'X-Custom': 'value' } +}); +``` + +### Configuration +Reads `VITE_BACKEND_URL` from environment or uses default: +```typescript +const BACKEND_URL = import.meta.env.VITE_BACKEND_URL || 'https://api.example.com'; +``` + +## API Endpoints (`services/api/`) + +### Auth API (`services/api/authApi.ts`) + +Authentication-related endpoints. + +```typescript +import { authApi } from '../services/api/authApi'; + +// Login +const { token, user } = await authApi.login(credentials); + +// Token exchange (for deep link flow) +const { sessionToken, user } = await authApi.exchangeToken(loginToken); + +// Logout +await authApi.logout(); +``` + +### User API (`services/api/userApi.ts`) + +User profile endpoints. + +```typescript +import { userApi } from '../services/api/userApi'; + +// Get current user +const user = await userApi.getCurrentUser(); + +// Update profile +const updated = await userApi.updateProfile({ firstName, lastName }); + +// Get settings +const settings = await userApi.getSettings(); +``` + +## Socket Service (`services/socketService.ts`) + +Socket.io client singleton for real-time communication. + +### Features +- Singleton pattern - single connection per app +- Auth token passed in socket `auth` object +- Transports: polling first, then WebSocket upgrade +- Auto-reconnection handling + +### API +```typescript +import socketService from '../services/socketService'; + +// Connect with auth token +socketService.connect(token); + +// Disconnect +socketService.disconnect(); + +// Emit event +socketService.emit('event-name', data); + +// Listen for events +socketService.on('event-name', (data) => { + // Handle event +}); + +// Remove listener +socketService.off('event-name', handler); + +// One-time listener +socketService.once('event-name', (data) => { + // Handle once +}); + +// Get socket instance +const socket = socketService.getSocket(); + +// Check connection status +const isConnected = socketService.isConnected(); +``` + +### Connection Flow +```typescript +// In SocketProvider.tsx +useEffect(() => { + if (token) { + socketService.connect(token); + + socketService.on('connect', () => { + dispatch(setSocketStatus({ userId, status: 'connected' })); + dispatch(setSocketId({ userId, socketId: socket.id })); + // Initialize MCP server + initMCPServer(socketService.getSocket()); + }); + + socketService.on('disconnect', () => { + dispatch(setSocketStatus({ userId, status: 'disconnected' })); + }); + } + + return () => { + socketService.disconnect(); + }; +}, [token]); +``` + +### Configuration +```typescript +const socket = io(BACKEND_URL, { + auth: { token }, + transports: ['polling', 'websocket'], + reconnection: true, + reconnectionAttempts: 5, + reconnectionDelay: 1000 +}); +``` + +## MTProto Service (`services/mtprotoService.ts`) + +Telegram MTProto client singleton. + +### Features +- Singleton pattern - one client per user +- Session persistence via Redux (not localStorage) +- Auto-retry for FLOOD_WAIT up to 60s +- Supports QR login and phone auth + +### Initialization +```typescript +import mtprotoService from '../services/mtprotoService'; + +// Get or create instance for user +const client = await mtprotoService.getInstance().initialize(userId); + +// Set session string (from Redux) +await client.setSession(sessionString); + +// Connect to Telegram +await client.connect(); +``` + +### Session Management +```typescript +// Session is stored in Redux, not localStorage +// In TelegramProvider: +const sessionString = useAppSelector((state) => + state.telegram.byUser[userId]?.sessionString +); + +// When session updates +useEffect(() => { + if (client && sessionString) { + client.setSession(sessionString); + } +}, [sessionString]); + +// Save session after auth +const newSession = await client.getSession(); +dispatch(setSessionString({ userId, sessionString: newSession })); +``` + +### API Operations +```typescript +// Get current user +const me = await client.getMe(); + +// Get dialogs (chats) +const dialogs = await client.getDialogs({ limit: 20 }); + +// Send message +await client.sendMessage(peer, { message: 'Hello!' }); + +// Get history +const messages = await client.getMessages(peer, { limit: 50 }); +``` + +### Error Handling +```typescript +try { + await client.connect(); +} catch (error) { + if (error.message.includes('FLOOD_WAIT')) { + const seconds = parseInt(error.message.match(/\d+/)?.[0] || '60'); + if (seconds <= 60) { + // Auto-retry after wait + await new Promise(r => setTimeout(r, seconds * 1000)); + await client.connect(); + } + } + throw error; +} +``` + +## Service Integration with Providers + +### SocketProvider +```typescript +// providers/SocketProvider.tsx +export function SocketProvider({ children }) { + const token = useAppSelector((state) => state.auth.token); + + useEffect(() => { + if (token) { + socketService.connect(token); + // On connect, initialize MCP + } + return () => socketService.disconnect(); + }, [token]); + + return {children}; +} +``` + +### TelegramProvider +```typescript +// providers/TelegramProvider.tsx +export function TelegramProvider({ children }) { + const dispatch = useAppDispatch(); + const userId = useAppSelector((state) => state.user.profile?.id); + + useEffect(() => { + if (userId) { + // Parallel init + connect for faster startup + Promise.all([ + dispatch(initializeTelegram(userId)), + dispatch(connectTelegram(userId)) + ]); + } + }, [userId]); + + return {children}; +} +``` + +## Best Practices + +1. **Use singletons** - Never create multiple service instances +2. **Store sessions in Redux** - Not localStorage +3. **Clean up on unmount** - Disconnect in useEffect cleanup +4. **Handle errors gracefully** - Retry for transient failures +5. **Pass auth via proper channels** - Socket auth object, not query string + +--- + +*Previous: [State Management](./02-state-management.md) | Next: [MCP System](./04-mcp-system.md)* diff --git a/docs/src/04-mcp-system.md b/docs/src/04-mcp-system.md new file mode 100644 index 000000000..6c50981f4 --- /dev/null +++ b/docs/src/04-mcp-system.md @@ -0,0 +1,437 @@ +# MCP System + +The Model Context Protocol (MCP) system enables AI-driven Telegram interactions through 81 specialized tools. + +## Overview + +``` +MCP System Architecture +├── lib/mcp/ +│ ├── index.ts # Singleton lifecycle management +│ ├── types.ts # MCP interfaces +│ ├── transport.ts # Socket.IO JSON-RPC 2.0 transport +│ ├── logger.ts # Logging utilities +│ ├── errorHandler.ts # Error handling +│ ├── validation.ts # Input validation +│ │ +│ └── telegram/ +│ ├── index.ts # Server initialization +│ ├── server.ts # TelegramMCPServer (81 tools) +│ ├── types.ts # Tool name types +│ ├── telegramApi.ts # Telegram API layer +│ ├── apiCastHelpers.ts # Type casting +│ ├── apiResultTypes.ts # Result types +│ ├── args.ts # Argument schemas +│ ├── toolActionParser.ts # Human-readable parsing +│ │ +│ └── tools/ # 81 individual tool files +``` + +## Core Components + +### MCP Types (`lib/mcp/types.ts`) + +```typescript +interface MCPTool { + name: string; + description: string; + inputSchema: JSONSchema; +} + +interface MCPRequest { + jsonrpc: '2.0'; + id: string | number; + method: string; + params?: unknown; +} + +interface MCPResponse { + jsonrpc: '2.0'; + id: string | number; + result?: unknown; + error?: MCPError; +} + +interface MCPError { + code: number; + message: string; + data?: unknown; +} +``` + +### Transport Layer (`lib/mcp/transport.ts`) + +Socket.IO-based JSON-RPC 2.0 transport. + +**Features:** +- Request ID tracking +- 30-second timeout +- Error handling +- Event emission + +```typescript +class SocketIOMCPTransport { + constructor(socket: Socket); + + // Send request and await response + async send(request: MCPRequest): Promise; + + // Register handler for incoming requests + onRequest(handler: (req: MCPRequest) => Promise): void; + + // Clean up listeners + dispose(): void; +} +``` + +### MCP Singleton (`lib/mcp/index.ts`) + +Lifecycle management for MCP server. + +```typescript +// Initialize MCP server with socket +initMCPServer(socket: Socket): TelegramMCPServer; + +// Get existing server instance +getMCPServer(): TelegramMCPServer | null; + +// Update socket reference +updateMCPSocket(socket: Socket): void; + +// Clean up +cleanupMCP(): void; +``` + +## Telegram MCP Server + +### Server Class (`lib/mcp/telegram/server.ts`) + +```typescript +class TelegramMCPServer { + constructor(transport: SocketIOMCPTransport, userId: string); + + // Register all 81 tools + async initialize(): Promise; + + // List available tools + listTools(): MCPTool[]; + + // Execute a tool + async executeTool(name: string, args: unknown): Promise; + + // Handle incoming JSON-RPC request + async handleRequest(request: MCPRequest): Promise; + + // Clean up + dispose(): void; +} +``` + +### Tool Handler Interface + +Each tool file exports a handler: + +```typescript +interface TelegramMCPToolHandler { + name: string; + description: string; + inputSchema: JSONSchema; + call: ( + args: unknown, + context: { + telegramClient: TelegramClient; + userId: string; + } + ) => Promise; +} + +interface ToolResult { + success: boolean; + data?: unknown; + error?: string; +} +``` + +## Tool Categories + +### 81 Telegram Tools + +#### User & Profile (5 tools) +| Tool | Description | +|------|-------------| +| `getMe` | Get current authenticated user | +| `getUserInfo` | Get info about any user | +| `getUserPhotos` | Get user profile photos | +| `getUserStatus` | Get online status | +| `setUserStatus` | Update own status | + +#### Chats & Dialogs (12 tools) +| Tool | Description | +|------|-------------| +| `getChats` | Fetch chat list | +| `getChatInfo` | Get chat details | +| `createGroup` | Create new group | +| `createChannel` | Create new channel | +| `leaveChat` | Leave chat/group/channel | +| `deleteChat` | Delete chat | +| `muteChat` | Mute notifications | +| `unmuteChat` | Unmute notifications | +| `pinChat` | Pin chat to top | +| `unpinChat` | Unpin chat | +| `archiveChat` | Archive chat | +| `unarchiveChat` | Unarchive chat | + +#### Messages (15 tools) +| Tool | Description | +|------|-------------| +| `getHistory` | Fetch message history | +| `sendMessage` | Send text message | +| `replyToMessage` | Reply to specific message | +| `editMessage` | Edit sent message | +| `deleteMessage` | Delete message | +| `forwardMessage` | Forward to another chat | +| `pinMessage` | Pin message in chat | +| `unpinMessage` | Unpin message | +| `markAsRead` | Mark messages as read | +| `searchMessages` | Search in chat | +| `translateMessage` | Translate message text | +| `getMessageReactions` | Get reactions | +| `addReaction` | React to message | +| `removeReaction` | Remove reaction | +| `reportMessage` | Report spam/abuse | + +#### Media (10 tools) +| Tool | Description | +|------|-------------| +| `sendPhoto` | Send image | +| `sendVideo` | Send video | +| `sendDocument` | Send file | +| `sendVoice` | Send voice message | +| `sendAudio` | Send audio file | +| `sendSticker` | Send sticker | +| `sendGif` | Send animation | +| `sendLocation` | Send location | +| `sendContact` | Send contact card | +| `downloadMedia` | Download media file | + +#### Contacts (8 tools) +| Tool | Description | +|------|-------------| +| `addContact` | Add new contact | +| `deleteContact` | Remove contact | +| `getContacts` | Get contact list | +| `searchContacts` | Search contacts | +| `importContacts` | Bulk import | +| `exportContacts` | Export contact list | +| `blockUser` | Block user | +| `unblockUser` | Unblock user | + +#### Groups & Channels (15 tools) +| Tool | Description | +|------|-------------| +| `getAdmins` | Get admin list | +| `getMembers` | Get member list | +| `addMember` | Add user to group | +| `removeMember` | Remove from group | +| `banUser` | Ban user | +| `unbanUser` | Unban user | +| `promoteAdmin` | Promote to admin | +| `demoteAdmin` | Remove admin rights | +| `setGroupTitle` | Change group name | +| `setGroupPhoto` | Change group photo | +| `setGroupDescription` | Change description | +| `subscribePublicChannel` | Join public channel | +| `inviteToChannel` | Invite user | +| `getInviteLink` | Get invite link | +| `revokeInviteLink` | Revoke invite link | + +#### Polls & Interactive (5 tools) +| Tool | Description | +|------|-------------| +| `createPoll` | Create poll | +| `votePoll` | Vote on poll | +| `stopPoll` | Close poll | +| `pressInlineButton` | Click inline button | +| `answerCallback` | Respond to callback | + +#### Drafts (3 tools) +| Tool | Description | +|------|-------------| +| `saveDraft` | Save draft message | +| `getDrafts` | Get all drafts | +| `deleteDraft` | Delete draft | + +#### Privacy & Settings (5 tools) +| Tool | Description | +|------|-------------| +| `getBlockedUsers` | Get blocked list | +| `getPrivacySettings` | Get privacy config | +| `updatePrivacy` | Update privacy | +| `get2FAStatus` | Check 2FA status | +| `getActiveSessions` | Get login sessions | + +#### Misc (3 tools) +| Tool | Description | +|------|-------------| +| `resolveUsername` | Resolve @username | +| `checkUsername` | Check availability | +| `getWebPage` | Get link preview | + +## Tool Implementation Example + +```typescript +// lib/mcp/telegram/tools/sendMessage.ts +import { TelegramMCPToolHandler } from '../types'; + +export const sendMessage: TelegramMCPToolHandler = { + name: 'sendMessage', + description: 'Send a text message to a chat', + inputSchema: { + type: 'object', + properties: { + chatId: { + type: 'string', + description: 'Chat ID to send message to' + }, + text: { + type: 'string', + description: 'Message text' + }, + replyToMsgId: { + type: 'string', + description: 'Optional message ID to reply to' + } + }, + required: ['chatId', 'text'] + }, + + call: async (args, { telegramClient, userId }) => { + const { chatId, text, replyToMsgId } = args as { + chatId: string; + text: string; + replyToMsgId?: string; + }; + + try { + // Use big-integer for chat ID (Telegram IDs exceed Number.MAX_SAFE_INTEGER) + const peer = await telegramClient.getEntity(bigInt(chatId)); + + const result = await telegramClient.sendMessage(peer, { + message: text, + replyTo: replyToMsgId ? parseInt(replyToMsgId) : undefined + }); + + return { + success: true, + data: { + messageId: result.id.toString(), + date: result.date + } + }; + } catch (error) { + return { + success: false, + error: error.message + }; + } + } +}; +``` + +## MCP Initialization Flow + +```typescript +// In SocketProvider.tsx +useEffect(() => { + if (socket && socket.connected) { + // Initialize MCP server + const mcpServer = initMCPServer(socket); + + // Server registers all 81 tools + mcpServer.initialize(); + + // Listen for tool execution requests + mcpServer.onRequest(async (request) => { + if (request.method === 'tools/call') { + const { name, arguments: args } = request.params; + return mcpServer.executeTool(name, args); + } + return { error: { code: -32601, message: 'Method not found' } }; + }); + } + + return () => cleanupMCP(); +}, [socket?.connected]); +``` + +## JSON-RPC Protocol + +### Request Format +```json +{ + "jsonrpc": "2.0", + "id": 1, + "method": "tools/call", + "params": { + "name": "sendMessage", + "arguments": { + "chatId": "123456789", + "text": "Hello from AI!" + } + } +} +``` + +### Response Format (Success) +```json +{ + "jsonrpc": "2.0", + "id": 1, + "result": { + "success": true, + "data": { + "messageId": "12345", + "date": 1706540000 + } + } +} +``` + +### Response Format (Error) +```json +{ + "jsonrpc": "2.0", + "id": 1, + "error": { + "code": -32000, + "message": "Chat not found", + "data": { "chatId": "invalid" } + } +} +``` + +## Big Integer Handling + +Telegram IDs exceed JavaScript's `Number.MAX_SAFE_INTEGER`. Use `big-integer` library: + +```typescript +import bigInt from 'big-integer'; + +// Convert string ID to big integer +const chatId = bigInt(args.chatId); + +// Convert big integer back to string +const idString = chatId.toString(); +``` + +## Best Practices + +1. **Use big-integer for IDs** - All Telegram IDs should use big-integer +2. **Validate inputs** - Use JSON Schema validation before execution +3. **Handle errors gracefully** - Return structured error responses +4. **Log operations** - Use MCP logger for debugging +5. **Timeout long operations** - 30s timeout for transport + +--- + +*Previous: [Services Layer](./03-services.md) | Next: [Pages & Routing](./05-pages-routing.md)* diff --git a/docs/src/05-pages-routing.md b/docs/src/05-pages-routing.md new file mode 100644 index 000000000..20d86ea7c --- /dev/null +++ b/docs/src/05-pages-routing.md @@ -0,0 +1,403 @@ +# Pages & Routing + +The application uses HashRouter with protected and public route guards. + +## Route Structure + +``` +/ → Welcome (public) +/login → Login (public) +/onboarding → Onboarding (protected, requires auth, not yet onboarded) +/home → Home (protected, requires auth + onboarded) +/settings → Settings modal overlay +/settings/* → Settings sub-panels +* → DefaultRedirect (fallback) +``` + +## Route Configuration (`AppRoutes.tsx`) + +```typescript +export function AppRoutes() { + return ( + <> + + {/* Public routes - redirect if authenticated */} + }> + } /> + } /> + + + {/* Protected routes - require authentication */} + }> + } /> + + + {/* Protected + onboarded routes */} + }> + } /> + + + {/* Fallback redirect */} + } /> + + + {/* Settings modal overlay - renders on top of routes */} + + + ); +} +``` + +## Route Guards + +### PublicRoute (`components/PublicRoute.tsx`) + +Redirects authenticated users away from public pages. + +```typescript +export function PublicRoute() { + const token = useAppSelector((state) => state.auth.token); + const isOnboarded = useAppSelector((state) => + selectIsOnboarded(state, userId) + ); + + if (token) { + // Authenticated - redirect to appropriate page + return ; + } + + return ; +} +``` + +### ProtectedRoute (`components/ProtectedRoute.tsx`) + +Enforces authentication and optionally onboarding status. + +```typescript +interface ProtectedRouteProps { + requireOnboarded?: boolean; +} + +export function ProtectedRoute({ requireOnboarded = false }) { + const token = useAppSelector((state) => state.auth.token); + const isOnboarded = useAppSelector((state) => + selectIsOnboarded(state, userId) + ); + + if (!token) { + return ; + } + + if (requireOnboarded && !isOnboarded) { + return ; + } + + return ; +} +``` + +### DefaultRedirect (`components/DefaultRedirect.tsx`) + +Fallback route that redirects based on auth state. + +```typescript +export function DefaultRedirect() { + const token = useAppSelector((state) => state.auth.token); + const isOnboarded = useAppSelector((state) => + selectIsOnboarded(state, userId) + ); + + if (!token) { + return ; + } + + if (!isOnboarded) { + return ; + } + + return ; +} +``` + +## Pages + +### Welcome Page (`pages/Welcome.tsx`) + +Landing page for unauthenticated users. + +**Features:** +- App introduction and branding +- CTA to login/signup +- Public route (redirects if authenticated) + +### Login Page (`pages/Login.tsx`) + +Authentication page. + +**Features:** +- Telegram OAuth button +- Opens `/auth/telegram?platform=desktop` in browser +- Handles deep link callback + +```typescript +export function Login() { + const handleTelegramLogin = () => { + // Opens Telegram OAuth in system browser + openUrl(`${BACKEND_URL}/auth/telegram?platform=desktop`); + }; + + return ( +
+ +
+ ); +} +``` + +### Home Page (`pages/Home.tsx`) + +Main dashboard after authentication. + +**Features:** +- Protected route (requires auth + onboarded) +- Connection status indicators +- Navigation to settings modal +- Future: Chat list, messages, etc. + +```typescript +export function Home() { + const navigate = useNavigate(); + const user = useAppSelector((state) => state.user.profile); + const telegramStatus = useAppSelector((state) => + selectTelegramConnectionStatus(state, user?.id) + ); + + return ( +
+
+

Welcome, {user?.firstName}

+ +
+ + + + + {/* Main content */} +
+ ); +} +``` + +## Onboarding Flow (`pages/onboarding/`) + +Multi-step onboarding process. + +### Structure +``` +pages/onboarding/ +├── Onboarding.tsx # Flow controller +└── steps/ + ├── GetStartedStep.tsx # Welcome + ├── PrivacyStep.tsx # Privacy policy + ├── AnalyticsStep.tsx # Analytics opt-in + ├── ConnectStep.tsx # Telegram connection + └── FeaturesStep.tsx # Features overview +``` + +### Onboarding Controller (`Onboarding.tsx`) + +```typescript +const STEPS = [ + { id: 'get-started', component: GetStartedStep }, + { id: 'privacy', component: PrivacyStep }, + { id: 'analytics', component: AnalyticsStep }, + { id: 'connect', component: ConnectStep }, + { id: 'features', component: FeaturesStep } +]; + +export function Onboarding() { + const [currentStep, setCurrentStep] = useState(0); + const dispatch = useAppDispatch(); + const navigate = useNavigate(); + + const handleNext = () => { + if (currentStep < STEPS.length - 1) { + setCurrentStep(currentStep + 1); + } else { + // Complete onboarding + dispatch(setOnboarded({ userId, isOnboarded: true })); + navigate('/home'); + } + }; + + const handleBack = () => { + if (currentStep > 0) { + setCurrentStep(currentStep - 1); + } + }; + + const StepComponent = STEPS[currentStep].component; + + return ( +
+ + +
+ ); +} +``` + +### Step Components + +Each step receives `onNext` and `onBack` callbacks: + +```typescript +interface StepProps { + onNext: () => void; + onBack: () => void; +} + +export function ConnectStep({ onNext, onBack }: StepProps) { + const [showModal, setShowModal] = useState(false); + const telegramStatus = useAppSelector(/* ... */); + + return ( +
+

Connect Your Accounts

+ + {connectOptions.map((option) => ( + option.id === 'telegram' && setShowModal(true)} + /> + ))} + + setShowModal(false)} + /> + +
+ + +
+
+ ); +} +``` + +## Settings Modal Routing + +The settings modal overlays existing content using URL-based routing. + +### Modal Detection +```typescript +// In SettingsModal.tsx +const location = useLocation(); +const isOpen = location.pathname.startsWith('/settings'); +``` + +### Sub-Routes +``` +/settings → SettingsHome (main menu) +/settings/connections → ConnectionsPanel +/settings/messaging → MessagingPanel (future) +/settings/privacy → PrivacyPanel (future) +/settings/profile → ProfilePanel (future) +/settings/advanced → AdvancedPanel (future) +/settings/billing → BillingPanel (future) +``` + +### Navigation +```typescript +import { useSettingsNavigation } from './hooks/useSettingsNavigation'; + +function SettingsHome() { + const { navigateTo, closeModal } = useSettingsNavigation(); + + return ( +
+ navigateTo('connections')} + /> + +
+ ); +} +``` + +## HashRouter vs BrowserRouter + +The app uses HashRouter for desktop compatibility: + +```typescript +// App.tsx +import { HashRouter } from 'react-router-dom'; + +// URLs look like: app://localhost/#/home +// Instead of: app://localhost/home +``` + +**Why HashRouter:** +1. Tauri deep links work with hash-based URLs +2. No server configuration needed +3. Works with file:// protocol +4. Prevents 404 on direct URL access + +## Deep Link Handling + +Deep links are handled before routing: + +```typescript +// main.tsx +import('./utils/desktopDeepLinkListener').then((m) => { + m.setupDesktopDeepLinkListener().catch(console.error); +}); +``` + +The listener intercepts `outsourced://auth?token=...` and: +1. Exchanges token via Rust command +2. Stores session in Redux +3. Navigates to `/onboarding` or `/home` + +## Navigation Patterns + +### Programmatic Navigation +```typescript +import { useNavigate } from 'react-router-dom'; + +const navigate = useNavigate(); + +// Navigate to route +navigate('/home'); + +// Replace history entry +navigate('/login', { replace: true }); + +// Go back +navigate(-1); +``` + +### Link Component +```typescript +import { Link } from 'react-router-dom'; + +Settings +``` + +### State Transfer +```typescript +// Pass state to route +navigate('/details', { state: { itemId: 123 } }); + +// Receive state +const location = useLocation(); +const { itemId } = location.state; +``` + +--- + +*Previous: [MCP System](./04-mcp-system.md) | Next: [Components](./06-components.md)* diff --git a/docs/src/06-components.md b/docs/src/06-components.md new file mode 100644 index 000000000..df2561368 --- /dev/null +++ b/docs/src/06-components.md @@ -0,0 +1,511 @@ +# Components + +Reusable React components organized by feature. + +## Component Structure + +``` +components/ +├── Route Guards +│ ├── ProtectedRoute.tsx +│ ├── PublicRoute.tsx +│ └── DefaultRedirect.tsx +│ +├── Authentication +│ └── TelegramLoginButton.tsx +│ +├── Connection Status +│ ├── ConnectionIndicator.tsx +│ ├── TelegramConnectionIndicator.tsx +│ ├── TelegramConnectionModal.tsx +│ └── GmailConnectionIndicator.tsx +│ +├── Onboarding +│ ├── ProgressIndicator.tsx +│ └── LottieAnimation.tsx +│ +├── Settings Modal (16 files) +│ ├── SettingsModal.tsx +│ ├── SettingsLayout.tsx +│ ├── SettingsHome.tsx +│ ├── panels/ +│ ├── components/ +│ └── hooks/ +│ +└── Development + └── DesignSystemShowcase.tsx +``` + +## Route Guard Components + +### ProtectedRoute + +Requires authentication and optionally onboarding. + +```typescript +interface ProtectedRouteProps { + requireOnboarded?: boolean; +} + +// Usage in AppRoutes.tsx +}> + } /> + + +}> + } /> + +``` + +### PublicRoute + +Redirects authenticated users away. + +```typescript +// Usage in AppRoutes.tsx +}> + } /> + } /> + +``` + +### DefaultRedirect + +Fallback that routes based on auth state. + +```typescript +// Redirects to: +// - "/" if not authenticated +// - "/onboarding" if authenticated but not onboarded +// - "/home" if authenticated and onboarded +``` + +## Authentication Components + +### TelegramLoginButton + +OAuth login button for Telegram. + +```typescript +interface TelegramLoginButtonProps { + onClick: () => void; + disabled?: boolean; +} + +// Usage + openUrl(`${BACKEND_URL}/auth/telegram?platform=desktop`)} +/> +``` + +## Connection Status Components + +### ConnectionIndicator + +Generic connection status badge. + +```typescript +interface ConnectionIndicatorProps { + status: 'connected' | 'connecting' | 'disconnected' | 'error'; + label?: string; +} + + +``` + +### TelegramConnectionIndicator + +Telegram-specific status display. + +```typescript +interface TelegramConnectionIndicatorProps { + status: 'connected' | 'connecting' | 'disconnected' | 'error'; +} + +// Usage with Redux state +const telegramStatus = useAppSelector((state) => + selectTelegramConnectionStatus(state, userId) +); + + +``` + +### TelegramConnectionModal + +Modal for setting up Telegram connection. + +```typescript +interface TelegramConnectionModalProps { + isOpen: boolean; + onClose: () => void; +} + +// Usage in onboarding/settings +const [showModal, setShowModal] = useState(false); + + setShowModal(false)} +/> +``` + +**Features:** +- QR code login flow +- Phone number login flow +- Connection status display +- Error handling + +### GmailConnectionIndicator + +Gmail status badge (future integration). + +```typescript + +``` + +## Onboarding Components + +### ProgressIndicator + +Visual progress through onboarding steps. + +```typescript +interface ProgressIndicatorProps { + current: number; + total: number; +} + + +``` + +### LottieAnimation + +Lottie animation player for onboarding. + +```typescript +interface LottieAnimationProps { + animationData: object; + loop?: boolean; + autoplay?: boolean; + className?: string; +} + +import welcomeAnimation from '../assets/animations/welcome.json'; + + +``` + +## Settings Modal System + +Complete modal system with URL-based routing. + +### File Structure +``` +components/settings/ +├── SettingsModal.tsx # Route-based container +├── SettingsLayout.tsx # Portal + backdrop wrapper +├── SettingsHome.tsx # Main menu with profile +├── panels/ +│ ├── ConnectionsPanel.tsx # Connection management +│ ├── MessagingPanel.tsx # (Future) +│ ├── PrivacyPanel.tsx # (Future) +│ ├── ProfilePanel.tsx # (Future) +│ ├── AdvancedPanel.tsx # (Future) +│ └── BillingPanel.tsx # (Future) +├── components/ +│ ├── SettingsHeader.tsx # User profile section +│ ├── SettingsMenuItem.tsx # Menu item component +│ ├── SettingsBackButton.tsx # Back navigation +│ └── SettingsPanelLayout.tsx# Panel wrapper +└── hooks/ + ├── useSettingsNavigation.ts # URL routing + └── useSettingsAnimation.ts # Animation state +``` + +### SettingsModal + +Main container that renders based on URL. + +```typescript +export function SettingsModal() { + const location = useLocation(); + const isOpen = location.pathname.startsWith('/settings'); + + if (!isOpen) return null; + + return ( + + {/* Route to appropriate panel */} + {location.pathname === '/settings' && } + {location.pathname === '/settings/connections' && } + {/* ... more panels */} + + ); +} +``` + +### SettingsLayout + +Portal-based modal wrapper. + +```typescript +export function SettingsLayout({ children }) { + const { closeModal } = useSettingsNavigation(); + + return createPortal( +
+ {/* Backdrop */} +
+ + {/* Modal */} +
+
+ {children} +
+
+
, + document.body + ); +} +``` + +### SettingsHome + +Main menu with user profile. + +```typescript +export function SettingsHome() { + const { navigateTo, closeModal } = useSettingsNavigation(); + const user = useAppSelector((state) => state.user.profile); + + const menuItems = [ + { id: 'connections', label: 'Connections', icon: LinkIcon }, + { id: 'messaging', label: 'Messaging', icon: MessageIcon }, + { id: 'privacy', label: 'Privacy', icon: ShieldIcon }, + // ... more items + ]; + + return ( +
+ + + {menuItems.map((item) => ( + navigateTo(item.id)} + /> + ))} +
+ ); +} +``` + +### ConnectionsPanel + +Connection management interface. + +```typescript +export function ConnectionsPanel() { + const { navigateBack } = useSettingsNavigation(); + const [telegramModalOpen, setTelegramModalOpen] = useState(false); + + const telegramStatus = useAppSelector((state) => + selectTelegramConnectionStatus(state, userId) + ); + + // Reuses connectOptions from onboarding + const connections = connectOptions.map((opt) => ({ + ...opt, + status: opt.id === 'telegram' ? telegramStatus : 'coming-soon' + })); + + return ( + + {connections.map((conn) => ( + conn.id === 'telegram' && setTelegramModalOpen(true)} + /> + ))} + + setTelegramModalOpen(false)} + /> + + ); +} +``` + +### Settings Hooks + +#### useSettingsNavigation + +URL-based navigation for settings modal. + +```typescript +interface UseSettingsNavigationReturn { + currentRoute: string; + navigateTo: (panel: string) => void; + navigateBack: () => void; + closeModal: () => void; +} + +const { navigateTo, navigateBack, closeModal } = useSettingsNavigation(); + +// Navigate to panel +navigateTo('connections'); // → /settings/connections + +// Go back +navigateBack(); // → /settings + +// Close modal +closeModal(); // → previous non-settings route +``` + +#### useSettingsAnimation + +Animation state management. + +```typescript +interface UseSettingsAnimationReturn { + isEntering: boolean; + isExiting: boolean; + animationClass: string; +} + +const { animationClass } = useSettingsAnimation(); + +
+ {/* Content */} +
+``` + +### Settings Components + +#### SettingsHeader + +User profile section at top of settings. + +```typescript +interface SettingsHeaderProps { + user: User | null; + onClose: () => void; +} + + +``` + +#### SettingsMenuItem + +Individual menu item with icon and chevron. + +```typescript +interface SettingsMenuItemProps { + label: string; + icon: React.ComponentType; + onClick: () => void; + badge?: string; + disabled?: boolean; +} + + navigateTo('connections')} + badge="2" +/> +``` + +#### SettingsBackButton + +Back navigation button. + +```typescript +interface SettingsBackButtonProps { + onClick: () => void; +} + + +``` + +#### SettingsPanelLayout + +Wrapper for settings panels. + +```typescript +interface SettingsPanelLayoutProps { + title: string; + onBack: () => void; + children: React.ReactNode; +} + + + {/* Panel content */} + +``` + +## Component Patterns + +### Reusing Connection Options + +The `connectOptions` array is shared between onboarding and settings: + +```typescript +// Defined in ConnectStep.tsx, imported elsewhere +export const connectOptions = [ + { + id: 'telegram', + label: 'Telegram', + icon: TelegramIcon, + description: 'Connect your Telegram account' + }, + { + id: 'gmail', + label: 'Gmail', + icon: GmailIcon, + description: 'Connect your Gmail account', + comingSoon: true + } +]; +``` + +### Modal via Portal + +Settings modal uses `createPortal` to render outside the component tree: + +```typescript +return createPortal( +
+ {/* Modal content */} +
, + document.body +); +``` + +### Controlled vs Uncontrolled + +Connection modals are controlled components: + +```typescript +// Parent controls open state +const [isOpen, setIsOpen] = useState(false); + + setIsOpen(false)} +/> +``` + +--- + +*Previous: [Pages & Routing](./05-pages-routing.md) | Next: [Providers](./07-providers.md)* diff --git a/docs/src/07-providers.md b/docs/src/07-providers.md new file mode 100644 index 000000000..5bfc0be13 --- /dev/null +++ b/docs/src/07-providers.md @@ -0,0 +1,404 @@ +# Providers + +React context providers manage service lifecycle and provide shared state. + +## Provider Chain + +The providers wrap the application in a specific order: + +```typescript +// App.tsx + + + + + + + + + + + + + +``` + +**Order matters because:** +1. Redux must be outermost for state access +2. PersistGate rehydrates state before rendering children +3. SocketProvider depends on Redux auth token +4. TelegramProvider depends on Redux telegram state +5. HashRouter provides navigation to all routes + +## SocketProvider (`providers/SocketProvider.tsx`) + +Manages Socket.io connection lifecycle and MCP initialization. + +### Responsibilities +- Auto-connect when auth token is available +- Auto-disconnect when token is cleared +- Initialize MCP server when socket connects +- Update Redux with connection status + +### Implementation + +```typescript +interface SocketContextValue { + socket: Socket | null; + isConnected: boolean; + emit: (event: string, data: unknown) => void; + on: (event: string, handler: Function) => void; + off: (event: string, handler: Function) => void; +} + +export function SocketProvider({ children }) { + const token = useAppSelector((state) => state.auth.token); + const userId = useAppSelector((state) => state.user.profile?.id); + const dispatch = useAppDispatch(); + + useEffect(() => { + if (!token || !userId) { + socketService.disconnect(); + dispatch(setSocketStatus({ userId, status: 'disconnected' })); + return; + } + + // Connect with auth token + socketService.connect(token); + dispatch(setSocketStatus({ userId, status: 'connecting' })); + + // Handle connection events + socketService.on('connect', () => { + dispatch(setSocketStatus({ userId, status: 'connected' })); + dispatch(setSocketId({ userId, socketId: socketService.getSocket()?.id })); + + // Initialize MCP server + initMCPServer(socketService.getSocket()); + }); + + socketService.on('disconnect', () => { + dispatch(setSocketStatus({ userId, status: 'disconnected' })); + cleanupMCP(); + }); + + socketService.on('connect_error', (error) => { + console.error('Socket connection error:', error); + dispatch(setSocketStatus({ userId, status: 'disconnected' })); + }); + + return () => { + socketService.disconnect(); + cleanupMCP(); + }; + }, [token, userId]); + + const contextValue: SocketContextValue = { + socket: socketService.getSocket(), + isConnected: socketService.isConnected(), + emit: socketService.emit.bind(socketService), + on: socketService.on.bind(socketService), + off: socketService.off.bind(socketService) + }; + + return ( + + {children} + + ); +} +``` + +### Usage + +```typescript +import { useSocket } from '../providers/SocketProvider'; + +function MyComponent() { + const { socket, isConnected, emit, on, off } = useSocket(); + + useEffect(() => { + const handler = (data) => console.log('Received:', data); + on('event-name', handler); + return () => off('event-name', handler); + }, [on, off]); + + const sendMessage = () => { + emit('send-message', { text: 'Hello!' }); + }; + + return ( +
+ Status: {isConnected ? 'Connected' : 'Disconnected'} + +
+ ); +} +``` + +## TelegramProvider (`providers/TelegramProvider.tsx`) + +Manages Telegram MTProto connection lifecycle. + +### Responsibilities +- Initialize MTProto client when user is authenticated +- Connect to Telegram servers +- Store session string in Redux +- Provide Telegram context to children + +### Implementation + +```typescript +interface TelegramContextValue { + client: TelegramClient | null; + connectionStatus: ConnectionStatus; + authStatus: AuthStatus; + connect: () => Promise; + disconnect: () => Promise; +} + +export function TelegramProvider({ children }) { + const dispatch = useAppDispatch(); + const userId = useAppSelector((state) => state.user.profile?.id); + const telegramState = useAppSelector((state) => + state.telegram.byUser[userId] + ); + + useEffect(() => { + if (!userId) return; + + // Parallel initialization for faster startup + const init = async () => { + try { + // Initialize and connect in parallel + await Promise.all([ + dispatch(initializeTelegram(userId)).unwrap(), + dispatch(connectTelegram(userId)).unwrap() + ]); + } catch (error) { + console.error('Telegram initialization failed:', error); + } + }; + + init(); + + return () => { + dispatch(disconnectTelegram(userId)); + }; + }, [userId]); + + // Restore session from persisted state + useEffect(() => { + if (telegramState?.sessionString) { + const client = mtprotoService.getInstance().getClient(); + if (client) { + client.setSession(telegramState.sessionString); + } + } + }, [telegramState?.sessionString]); + + const contextValue: TelegramContextValue = { + client: mtprotoService.getInstance().getClient(), + connectionStatus: telegramState?.connectionStatus || 'disconnected', + authStatus: telegramState?.authStatus || 'not_authenticated', + connect: () => dispatch(connectTelegram(userId)).unwrap(), + disconnect: () => dispatch(disconnectTelegram(userId)).unwrap() + }; + + return ( + + {children} + + ); +} +``` + +### Usage + +```typescript +import { useTelegram } from '../providers/TelegramProvider'; + +function ChatList() { + const { client, connectionStatus, authStatus } = useTelegram(); + const [chats, setChats] = useState([]); + + useEffect(() => { + if (connectionStatus === 'connected' && authStatus === 'authenticated') { + const fetchChats = async () => { + const dialogs = await client.getDialogs({ limit: 20 }); + setChats(dialogs); + }; + fetchChats(); + } + }, [client, connectionStatus, authStatus]); + + if (connectionStatus !== 'connected') { + return
Connecting to Telegram...
; + } + + return ( +
    + {chats.map((chat) => ( +
  • {chat.title}
  • + ))} +
+ ); +} +``` + +## UserProvider (`providers/UserProvider.tsx`) + +Minimal user context provider (most user state is in Redux). + +### Responsibilities +- Legacy user context for compatibility +- May be deprecated in favor of Redux + +### Implementation + +```typescript +interface UserContextValue { + user: User | null; + loading: boolean; +} + +export function UserProvider({ children }) { + const user = useAppSelector((state) => state.user.profile); + const loading = useAppSelector((state) => state.user.loading); + + return ( + + {children} + + ); +} +``` + +### Usage + +```typescript +import { useUserContext } from '../providers/UserProvider'; + +function Header() { + const { user, loading } = useUserContext(); + + if (loading) return ; + if (!user) return null; + + return Welcome, {user.firstName}; +} +``` + +## Provider Patterns + +### Effect-Based Lifecycle + +Providers use `useEffect` to manage service lifecycle: + +```typescript +useEffect(() => { + // Setup on mount or dependency change + service.connect(); + + // Cleanup on unmount or dependency change + return () => { + service.disconnect(); + }; +}, [dependencies]); +``` + +### Redux Integration + +Providers read from and dispatch to Redux: + +```typescript +// Read state +const token = useAppSelector((state) => state.auth.token); + +// Dispatch actions +const dispatch = useAppDispatch(); +dispatch(setStatus({ userId, status: 'connected' })); +``` + +### Parallel Initialization + +TelegramProvider runs init and connect in parallel: + +```typescript +await Promise.all([ + dispatch(initializeTelegram(userId)).unwrap(), + dispatch(connectTelegram(userId)).unwrap() +]); +``` + +This reduces startup time compared to sequential operations. + +### Session Restoration + +Providers restore persisted state on mount: + +```typescript +useEffect(() => { + if (persistedSession) { + service.restoreSession(persistedSession); + } +}, [persistedSession]); +``` + +## Context vs Redux + +| Use Context For | Use Redux For | +|-----------------|---------------| +| Service instances (socket, client) | Serializable state (status, data) | +| Methods (emit, on, off) | Persisted state (sessions, tokens) | +| Derived values | Complex state logic | + +Example: +- `SocketContext` provides `socket` instance and `emit` method +- Redux stores `socketStatus` and `socketId` + +## Testing Providers + +### Mock Provider for Tests + +```typescript +// test-utils.tsx +const mockSocketContext: SocketContextValue = { + socket: null, + isConnected: true, + emit: jest.fn(), + on: jest.fn(), + off: jest.fn() +}; + +export function TestProviders({ children }) { + return ( + + + {children} + + + ); +} +``` + +### Testing Provider Effects + +```typescript +test('SocketProvider connects when token is available', () => { + const store = createTestStore({ auth: { token: 'test-token' } }); + + render( + + + + + + ); + + expect(socketService.connect).toHaveBeenCalledWith('test-token'); +}); +``` + +--- + +*Previous: [Components](./06-components.md) | Next: [Hooks & Utils](./08-hooks-utils.md)* diff --git a/docs/src/08-hooks-utils.md b/docs/src/08-hooks-utils.md new file mode 100644 index 000000000..7ff41ffbb --- /dev/null +++ b/docs/src/08-hooks-utils.md @@ -0,0 +1,505 @@ +# Hooks & Utilities + +Custom React hooks and utility functions. + +## Custom Hooks + +### useSocket (`hooks/useSocket.ts`) + +Access Socket.io functionality from any component. + +```typescript +interface UseSocketReturn { + socket: Socket | null; + isConnected: boolean; + emit: (event: string, data: unknown) => void; + on: (event: string, handler: Function) => void; + off: (event: string, handler: Function) => void; + once: (event: string, handler: Function) => void; +} + +function useSocket(): UseSocketReturn; +``` + +**Usage:** + +```typescript +import { useSocket } from '../hooks/useSocket'; + +function ChatInput() { + const { emit, isConnected } = useSocket(); + + const sendMessage = (text: string) => { + if (isConnected) { + emit('chat:message', { text }); + } + }; + + return ( + e.key === 'Enter' && sendMessage(e.target.value)} + /> + ); +} +``` + +**With event listeners:** + +```typescript +function Notifications() { + const { on, off } = useSocket(); + const [notifications, setNotifications] = useState([]); + + useEffect(() => { + const handler = (notification) => { + setNotifications((prev) => [...prev, notification]); + }; + + on('notification', handler); + return () => off('notification', handler); + }, [on, off]); + + return ; +} +``` + +### useUser (`hooks/useUser.ts`) + +Access user profile data and loading state. + +```typescript +interface UseUserReturn { + user: User | null; + loading: boolean; + error: string | null; + refetch: () => Promise; +} + +function useUser(): UseUserReturn; +``` + +**Usage:** + +```typescript +import { useUser } from '../hooks/useUser'; + +function ProfileHeader() { + const { user, loading, error, refetch } = useUser(); + + if (loading) return ; + if (error) return ; + if (!user) return null; + + return ( +
+ + {user.firstName} {user.lastName} +
+ ); +} +``` + +### Settings Modal Hooks + +#### useSettingsNavigation (`components/settings/hooks/useSettingsNavigation.ts`) + +URL-based navigation for settings modal. + +```typescript +interface UseSettingsNavigationReturn { + currentRoute: string; // Current settings path + navigateTo: (panel: string) => void; // Navigate to panel + navigateBack: () => void; // Go back one level + closeModal: () => void; // Close settings entirely +} + +function useSettingsNavigation(): UseSettingsNavigationReturn; +``` + +**Usage:** + +```typescript +import { useSettingsNavigation } from './hooks/useSettingsNavigation'; + +function SettingsMenu() { + const { navigateTo, closeModal } = useSettingsNavigation(); + + return ( + + ); +} +``` + +#### useSettingsAnimation (`components/settings/hooks/useSettingsAnimation.ts`) + +Animation state management for settings modal. + +```typescript +interface UseSettingsAnimationReturn { + isEntering: boolean; // Modal is animating in + isExiting: boolean; // Modal is animating out + animationClass: string; // CSS class for current state +} + +function useSettingsAnimation(): UseSettingsAnimationReturn; +``` + +**Usage:** + +```typescript +import { useSettingsAnimation } from './hooks/useSettingsAnimation'; + +function SettingsModal() { + const { animationClass, isExiting } = useSettingsAnimation(); + + return ( +
+ {/* Content */} +
+ ); +} +``` + +## Utilities + +### Configuration (`utils/config.ts`) + +Environment variable access with defaults. + +```typescript +// Backend URL +export const BACKEND_URL = + import.meta.env.VITE_BACKEND_URL || 'https://api.example.com'; + +// Telegram configuration +export const TELEGRAM_API_ID = import.meta.env.VITE_TELEGRAM_API_ID; +export const TELEGRAM_API_HASH = import.meta.env.VITE_TELEGRAM_API_HASH; +export const TELEGRAM_BOT_USERNAME = import.meta.env.VITE_TELEGRAM_BOT_USERNAME; +export const TELEGRAM_BOT_ID = import.meta.env.VITE_TELEGRAM_BOT_ID; + +// Debug mode +export const DEBUG = import.meta.env.VITE_DEBUG === 'true'; +``` + +**Usage:** + +```typescript +import { BACKEND_URL, DEBUG } from '../utils/config'; + +const response = await fetch(`${BACKEND_URL}/api/users`); + +if (DEBUG) { + console.log('Response:', response); +} +``` + +### Deep Link (`utils/deeplink.ts`) + +Build deep link URLs for authentication handoff. + +```typescript +// Build auth deep link +function buildAuthDeepLink(token: string): string; + +// Parse deep link URL +function parseDeepLink(url: string): { path: string; params: URLSearchParams }; +``` + +**Usage:** + +```typescript +import { buildAuthDeepLink } from '../utils/deeplink'; + +// Build URL for browser redirect +const deepLink = buildAuthDeepLink(loginToken); +// → "outsourced://auth?token=abc123" + +// In web frontend after auth: +window.location.href = deepLink; +``` + +### Desktop Deep Link Listener (`utils/desktopDeepLinkListener.ts`) + +Handle incoming deep links in desktop app. + +```typescript +// Setup listener for deep link events +async function setupDesktopDeepLinkListener(): Promise; +``` + +**Called in main.tsx:** + +```typescript +// Lazy import to ensure Tauri IPC is ready +import('./utils/desktopDeepLinkListener').then((m) => { + m.setupDesktopDeepLinkListener().catch(console.error); +}); +``` + +**What it does:** +1. Listens for `onOpenUrl` events from Tauri deep-link plugin +2. Parses `outsourced://auth?token=...` URLs +3. Calls Rust `exchange_token` command (bypasses CORS) +4. Stores session in Redux +5. Navigates to `/onboarding` or `/home` + +**Loop prevention:** +```typescript +// Set flag before navigation to prevent reprocessing +localStorage.setItem('deepLinkHandled', 'true'); +window.location.replace('/'); + +// On next load, clear flag +if (localStorage.getItem('deepLinkHandled') === 'true') { + localStorage.removeItem('deepLinkHandled'); + return; // Don't process again +} +``` + +### URL Opener (`utils/openUrl.ts`) + +Cross-platform URL opening. + +```typescript +// Open URL in system browser +async function openUrl(url: string): Promise; +``` + +**Usage:** + +```typescript +import { openUrl } from '../utils/openUrl'; + +// Opens in system browser (not in-app WebView) +await openUrl('https://telegram.org/auth'); +``` + +**Implementation:** +```typescript +export async function openUrl(url: string): Promise { + try { + // Try Tauri opener plugin first + const { open } = await import('@tauri-apps/plugin-opener'); + await open(url); + } catch { + // Fallback to browser API + window.open(url, '_blank'); + } +} +``` + +## Polyfills (`polyfills.ts`) + +Node.js polyfills for browser environment. + +The `telegram` npm package requires Node.js APIs. These are polyfilled: + +```typescript +// polyfills.ts +import { Buffer } from 'buffer'; +import process from 'process'; +import util from 'util'; + +window.Buffer = Buffer; +window.process = process; +window.util = util; +``` + +**Imported at app entry:** + +```typescript +// main.tsx +import './polyfills'; +// ... rest of app +``` + +**Vite configuration:** + +```typescript +// vite.config.ts +export default defineConfig({ + resolve: { + alias: { + buffer: 'buffer', + process: 'process/browser', + util: 'util' + } + }, + define: { + 'process.env': {}, + global: 'globalThis' + } +}); +``` + +## Types + +### API Types (`types/api.ts`) + +```typescript +// API response wrapper +interface ApiResponse { + success: boolean; + data?: T; + error?: string; +} + +// API error +interface ApiError { + code: string; + message: string; + details?: unknown; +} + +// User interface +interface User { + id: string; + firstName: string; + lastName?: string; + username?: string; + email?: string; + avatar?: string; + telegramId?: string; + subscription?: SubscriptionInfo; + usage?: UsageInfo; + createdAt: string; + updatedAt: string; +} +``` + +### Onboarding Types (`types/onboarding.ts`) + +```typescript +// Onboarding step definition +interface OnboardingStep { + id: string; + title: string; + component: React.ComponentType; +} + +// Step component props +interface StepProps { + onNext: () => void; + onBack: () => void; +} + +// Connection option +interface ConnectionOption { + id: string; + label: string; + icon: React.ComponentType; + description: string; + comingSoon?: boolean; +} +``` + +## Static Data + +### Countries (`data/countries.ts`) + +Country list for phone number input. + +```typescript +interface Country { + code: string; // "US" + name: string; // "United States" + dialCode: string; // "+1" + flag: string; // "🇺🇸" +} + +export const countries: Country[]; +``` + +**Usage:** + +```typescript +import { countries } from '../data/countries'; + +function PhoneInput() { + const [country, setCountry] = useState(countries[0]); + + return ( +
+ + +
+ ); +} +``` + +## Best Practices + +### Hook Dependencies + +Always include dependencies in useEffect: + +```typescript +// Good +useEffect(() => { + on('event', handler); + return () => off('event', handler); +}, [on, off, handler]); + +// Bad - missing dependencies +useEffect(() => { + on('event', handler); + return () => off('event', handler); +}, []); +``` + +### Cleanup Functions + +Always clean up subscriptions: + +```typescript +useEffect(() => { + const subscription = subscribe(); + return () => subscription.unsubscribe(); +}, []); +``` + +### Error Boundaries + +Wrap utility calls in try-catch: + +```typescript +try { + await openUrl(url); +} catch (error) { + console.error('Failed to open URL:', error); + // Fallback behavior +} +``` + +### Type Safety + +Use TypeScript generics for API calls: + +```typescript +const user = await apiClient.get('/users/me'); +// user is typed as User +``` + +--- + +*Previous: [Providers](./07-providers.md) | [Back to Index](./README.md)* diff --git a/docs/src/README.md b/docs/src/README.md new file mode 100644 index 000000000..e326cc8c6 --- /dev/null +++ b/docs/src/README.md @@ -0,0 +1,78 @@ +# Source Code Documentation + +This documentation covers the `/src` folder structure of the Outsourced Crypto Community Platform. + +## Quick Reference + +| Document | Description | +|----------|-------------| +| [Architecture Overview](./01-architecture.md) | High-level system architecture and provider chain | +| [State Management](./02-state-management.md) | Redux Toolkit slices, persistence, and selectors | +| [Services Layer](./03-services.md) | Singleton services (Socket.io, MTProto, API client) | +| [MCP System](./04-mcp-system.md) | Model Context Protocol with 81 Telegram tools | +| [Pages & Routing](./05-pages-routing.md) | Route definitions, guards, and page components | +| [Components](./06-components.md) | Reusable UI components and settings modal system | +| [Providers](./07-providers.md) | React context providers and lifecycle management | +| [Hooks & Utils](./08-hooks-utils.md) | Custom hooks and utility functions | + +## File Count Summary + +| Category | Files | Purpose | +|----------|-------|---------| +| Entry & Configuration | 7 | App init, routing, styles, types | +| State Management | 13 | Redux slices, selectors, hooks | +| Providers | 3 | Socket, Telegram, User contexts | +| Services | 5 | Singleton API clients | +| Pages | 9 | Full-page route components | +| Components | 28 | Reusable UI + settings modal (16 files) | +| MCP Core | 14 | MCP interfaces, transport, logging | +| MCP Telegram Tools | 81 | Individual Telegram API operations | +| Hooks | 2 | Custom React hooks | +| Types | 2 | TypeScript interfaces | +| Utils | 4 | Config, deep link, URL utilities | +| Data | 1 | Static data (countries) | +| Assets | 10+ | Icons and images | +| **TOTAL** | **171+** | Complete frontend application | + +## Directory Structure + +``` +src/ +├── App.tsx # Root component with provider chain +├── AppRoutes.tsx # Route definitions +├── main.tsx # Entry point +├── polyfills.ts # Node.js polyfills for browser +├── index.css # Global styles +│ +├── store/ # Redux state management (13 files) +├── providers/ # Context providers (3 files) +├── services/ # Singleton services (5 files) +├── lib/mcp/ # MCP system (95+ files) +├── pages/ # Page components (9 files) +├── components/ # UI components (28 files) +├── hooks/ # Custom hooks (2 files) +├── types/ # TypeScript types (2 files) +├── utils/ # Utilities (4 files) +├── data/ # Static data (1 file) +└── assets/ # Icons and images +``` + +## Key Architectural Decisions + +1. **HashRouter over BrowserRouter** - Required for Tauri deep link compatibility +2. **Redux Toolkit with Persistence** - Robust state management with rehydration +3. **Singleton Services** - Prevents connection leaks for Socket.io and MTProto +4. **Per-User State Scoping** - Telegram/socket state keyed by user ID +5. **Portal-Based Settings Modal** - URL routing without affecting main routes +6. **81-Tool MCP System** - Comprehensive Telegram API coverage + +## Getting Started + +1. Read [Architecture Overview](./01-architecture.md) for the big picture +2. Understand [State Management](./02-state-management.md) for data flow +3. Review [Services Layer](./03-services.md) for backend communication +4. Explore [MCP System](./04-mcp-system.md) for AI tool integration + +--- + +*Documentation maintained by stevenbaba* diff --git a/docs/telegram-login-desktop.md b/docs/telegram-login-desktop.md new file mode 100644 index 000000000..043956113 --- /dev/null +++ b/docs/telegram-login-desktop.md @@ -0,0 +1,207 @@ +# Telegram Login (Web → Desktop Handoff) + +This app implements **Telegram login for the desktop (Tauri) client** using a **system-browser auth flow** plus a **custom URL scheme deep link** (`outsourced://`) to return control back to the desktop app. + +--- + +## High-level flow + +1. **User clicks “Continue with Telegram”** inside the desktop app. +2. The app opens the system browser to the backend: + - `GET ${BACKEND_URL}/auth/telegram-widget?redirect=outsourced://auth` +3. The backend performs Telegram authentication (bot-based login / OAuth-like flow). +4. On success, the backend generates a **short-lived single-use `loginToken`** and redirects the browser to: + - `outsourced://auth?token=` +5. The OS routes that deep link to the installed desktop app. +6. The desktop app extracts the `token` from the deep link and exchanges it for a **long-lived `sessionToken`** by calling a Rust Tauri command (bypassing CORS): + - `POST ${BACKEND_URL}/auth/desktop-exchange` with `{ token }` +7. The desktop app stores `sessionToken` (and optional `user`) and navigates into onboarding. + +--- + +## Where this is implemented (current code) + +### 1) Desktop UI entry point + +The Telegram button in `src/components/TelegramLoginButton.tsx` opens the backend URL in the user’s system browser (via a Tauri command on desktop): + +```ts +await startTelegramLoginWithUrl(BACKEND_URL); +``` + +The backend base URL is configured here (`src/utils/config.ts`): + +```ts +export const BACKEND_URL = + import.meta.env.VITE_BACKEND_URL || 'https://2937933edf8a.ngrok-free.app'; +``` + +### 2) Deep link registration (Tauri config + Rust) + +The URL scheme is declared in `src-tauri/tauri.conf.json`: + +```json +{ + "plugins": { + "deep-link": { + "desktop": { + "schemes": ["outsourced"] + } + } + } +} +``` + +The deep-link plugin is initialized in `src-tauri/src/lib.rs`: + +```rust +pub fn run() { + tauri::Builder::default() + .plugin(tauri_plugin_opener::init()) + .plugin(tauri_plugin_deep_link::init()) + .setup(|app| { + #[cfg(any(windows, target_os = "linux"))] + { + app.deep_link().register_all()?; + } + Ok(()) + }) + .invoke_handler(tauri::generate_handler![greet, exchange_token]) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} +``` + +### 3) Desktop deep link listener (frontend) + +The listener is lazy-loaded in `src/main.tsx`: + +```ts +import('./utils/desktopDeepLinkListener').then(m => { + m.setupDesktopDeepLinkListener().catch(err => { + console.error('[DeepLink] setup error:', err); + }); +}); +``` + +The deep link handler: +- Accepts only the `outsourced:` scheme +- Requires `outsourced://auth?token=...` +- Calls the Rust command `exchange_token` +- Stores `sessionToken` in Redux auth state +- Redirects to `#/onboarding` (HashRouter) + +```ts +const handleDeepLinkUrls = async (urls: string[] | null | undefined) => { + if (!urls || urls.length === 0) return; + const url = urls[0]; + + try { + const parsed = new URL(url); + if (parsed.protocol !== 'outsourced:') return; + if (parsed.hostname !== 'auth') return; + + const token = parsed.searchParams.get('token'); + if (!token) return; + + const data = await invoke('exchange_token', { backendUrl: BACKEND_URL, token }); + // ... store sessionToken + user ... + window.location.hash = '/onboarding'; + } catch (error) { + console.error('[DeepLink] Failed to handle deep link URL:', url, error); + } +}; +``` + +### 4) Token exchange happens in Rust (CORS-safe) + +The command `exchange_token` posts to the backend and returns the JSON body: + +```rust +#[tauri::command] +async fn exchange_token(backend_url: String, token: String) -> Result { + let client = reqwest::Client::new(); + let url = format!("{}/auth/desktop-exchange", backend_url); + let response = client + .post(&url) + .header("Content-Type", "application/json") + .json(&serde_json::json!({ "token": token })) + .send() + .await + .map_err(|e| format!("Request failed: {}", e))?; + // ... status handling ... + Ok(body) +} +``` + +--- + +## Backend contract (required for Telegram login to work) + +Your backend must implement **both**: + +### A) `GET /auth/telegram-widget?redirect=outsourced://auth` + +- **Purpose**: start Telegram auth in the user’s browser. +- **On success**: + - create/find user + - mint a **short-lived** `loginToken` (single-use, recommended TTL \(\le 5\) minutes) + - redirect to: `outsourced://auth?token=` + +### B) `POST /auth/desktop-exchange` + +- **Purpose**: exchange `loginToken` for a long-lived desktop session token. +- **Request**: + +```json +{ "token": "loginToken-from-deeplink" } +``` + +- **Response (200)**: + +```json +{ + "sessionToken": "long-lived-session-token", + "user": { "id": "uuid", "username": "string", "firstName": "string" } +} +``` + +--- + +## Platform notes (important for “it works on my machine” issues) + +- **macOS**: deep links do **not** work reliably in `tauri dev` because there’s no `.app` bundle/`Info.plist`. You generally need a built `.app` bundle (debug build is fine). +- **Windows/Linux**: deep link scheme registration is done at runtime via `register_all()` and works in dev more easily. + +--- + +## “Required code changes” checklist (to make Telegram login work properly) + +### Backend (required) + +- **Implement `GET /auth/telegram?platform=desktop`** and ensure it redirects to `outsourced://auth?token=...` on success. +- **Implement `POST /auth/desktop-exchange`** to exchange the login token for a session token. +- **Enforce security**: + - `loginToken` is **single-use** + - short TTL (recommended \(\le 5\) minutes) + - reject expired/reused tokens + +### Desktop app (required for reliable behavior) + +- **Ensure the deep-link plugin is configured and permitted**: + - `src-tauri/tauri.conf.json` includes `"schemes": ["outsourced"]` + - `src-tauri/capabilities/default.json` includes `"deep-link:default"` +- **Use a real backend URL**: + - set `VITE_BACKEND_URL` for dev/prod so `Login.tsx` opens the correct domain + +### Desktop app (recommended hardening / cleanup) + +- **Validate the deep link target more strictly** in `src/utils/desktopDeepLinkListener.ts`: + - today it checks only `parsed.protocol === 'outsourced:'` + - recommended: also require `parsed.hostname === 'auth'` (and optionally a known path) +- **Don’t skip Telegram auth in onboarding**: + - `src/pages/onboarding/Step1Phone.tsx` currently has a “Continue with Telegram” button that *only navigates* and does not authenticate. +- **Remove sensitive Telegram secrets from frontend env**: + - any `VITE_*` variables are bundled into the frontend; don’t place bot tokens / api hashes there. + - the desktop app typically only needs `VITE_BACKEND_URL`; Telegram verification secrets should live on the backend. + diff --git a/index.html b/index.html index 77171a3b8..74c56fa29 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,7 @@ - + AlphaHuman diff --git a/package.json b/package.json index d917ccfba..0ec11b6c1 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,10 @@ "dev": "vite", "build": "tsc && vite build", "preview": "vite preview", - "tauri": "tauri" + "tauri": "tauri", + "macos:build": "tauri build --debug --bundles app", + "macos:run": "open 'src-tauri/target/debug/bundle/macos/AlphaHuman.app'", + "macos:dev": "tauri build --debug --bundles app && open 'src-tauri/target/debug/bundle/macos/AlphaHuman.app'" }, "dependencies": { "@reduxjs/toolkit": "^2.11.2", diff --git a/public/alpha.svg b/public/alpha.svg new file mode 100644 index 000000000..61ff098e6 --- /dev/null +++ b/public/alpha.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 945480d22..3e17434f2 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -41,6 +41,56 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "anyhow" version = "1.0.100" @@ -207,6 +257,17 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "auto-launch" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f012b8cc0c850f34117ec8252a44418f2e34a2cf501de89e29b241ae5f79471" +dependencies = [ + "dirs 4.0.0", + "thiserror 1.0.69", + "winreg 0.10.1", +] + [[package]] name = "autocfg" version = "1.5.0" @@ -441,6 +502,12 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + [[package]] name = "combine" version = "4.6.7" @@ -700,13 +767,33 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys 0.3.7", +] + [[package]] name = "dirs" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ - "dirs-sys", + "dirs-sys 0.5.0", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users 0.4.6", + "winapi", ] [[package]] @@ -717,7 +804,7 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", - "redox_users", + "redox_users 0.5.2", "windows-sys 0.61.2", ] @@ -827,7 +914,7 @@ dependencies = [ "rustc_version", "toml 0.9.11+spec-1.1.0", "vswhom", - "winreg", + "winreg 0.55.0", ] [[package]] @@ -872,6 +959,29 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "env_filter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -1814,6 +1924,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itoa" version = "1.0.17" @@ -1843,6 +1959,30 @@ dependencies = [ "system-deps", ] +[[package]] +name = "jiff" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67e8da4c49d6d9909fe03361f9b620f58898859f5c7aded68351e85e71ecf50" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c84ee7f197eca9a86c6fd6cb771e55eb991632f15f2bc3ca6ec838929e6e78" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "jni" version = "0.21.1" @@ -1908,6 +2048,16 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "keyring" +version = "3.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c" +dependencies = [ + "log", + "zeroize", +] + [[package]] name = "kuchikiki" version = "0.8.8-speedreader" @@ -2009,6 +2159,18 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" +[[package]] +name = "mac-notification-sys" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65fd3f75411f4725061682ed91f131946e912859d0044d39c4ec0aac818d7621" +dependencies = [ + "cc", + "objc2", + "objc2-foundation", + "time", +] + [[package]] name = "markup5ever" version = "0.14.1" @@ -2162,6 +2324,20 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" +[[package]] +name = "notify-rust" +version = "4.11.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6442248665a5aa2514e794af3b39661a8e73033b1cc5e59899e1276117ee4400" +dependencies = [ + "futures-lite", + "log", + "mac-notification-sys", + "serde", + "tauri-winrt-notification", + "zbus", +] + [[package]] name = "num-conv" version = "0.2.0" @@ -2417,6 +2593,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "open" version = "5.3.3" @@ -2736,7 +2918,7 @@ checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" dependencies = [ "base64 0.22.1", "indexmap 2.13.0", - "quick-xml", + "quick-xml 0.38.4", "serde", "time", ] @@ -2768,6 +2950,21 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "portable-atomic" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + [[package]] name = "potential_utf" version = "0.1.4" @@ -2866,6 +3063,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", +] + [[package]] name = "quick-xml" version = "0.38.4" @@ -2915,6 +3121,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + [[package]] name = "rand_chacha" version = "0.2.2" @@ -2935,6 +3151,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + [[package]] name = "rand_core" version = "0.5.1" @@ -2953,6 +3179,15 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "rand_hc" version = "0.2.0" @@ -2986,6 +3221,17 @@ dependencies = [ "bitflags 2.10.0", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + [[package]] name = "redox_users" version = "0.5.2" @@ -3787,7 +4033,7 @@ dependencies = [ "anyhow", "bytes", "cookie", - "dirs", + "dirs 6.0.0", "dunce", "embed_plist", "getrandom 0.3.4", @@ -3833,12 +4079,19 @@ dependencies = [ name = "tauri-app" version = "0.1.0" dependencies = [ + "env_logger", + "keyring", + "log", + "once_cell", + "parking_lot", "reqwest", "serde", "serde_json", "tauri", "tauri-build", + "tauri-plugin-autostart", "tauri-plugin-deep-link", + "tauri-plugin-notification", "tauri-plugin-opener", "tokio", ] @@ -3851,7 +4104,7 @@ checksum = "17fcb8819fd16463512a12f531d44826ce566f486d7ccd211c9c8cebdaec4e08" dependencies = [ "anyhow", "cargo_toml", - "dirs", + "dirs 6.0.0", "glob", "heck 0.5.0", "json-patch", @@ -3923,6 +4176,20 @@ dependencies = [ "walkdir", ] +[[package]] +name = "tauri-plugin-autostart" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "459383cebc193cdd03d1ba4acc40f2c408a7abce419d64bdcd2d745bc2886f70" +dependencies = [ + "auto-launch", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", +] + [[package]] name = "tauri-plugin-deep-link" version = "2.4.6" @@ -3944,6 +4211,25 @@ dependencies = [ "windows-result 0.3.4", ] +[[package]] +name = "tauri-plugin-notification" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01fc2c5ff41105bd1f7242d8201fdf3efd70749b82fa013a17f2126357d194cc" +dependencies = [ + "log", + "notify-rust", + "rand 0.9.2", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "time", + "url", +] + [[package]] name = "tauri-plugin-opener" version = "2.5.3" @@ -4067,6 +4353,18 @@ dependencies = [ "toml 0.9.11+spec-1.1.0", ] +[[package]] +name = "tauri-winrt-notification" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b1e66e07de489fe43a46678dd0b8df65e0c973909df1b60ba33874e297ba9b9" +dependencies = [ + "quick-xml 0.37.5", + "thiserror 2.0.18", + "windows", + "windows-version", +] + [[package]] name = "tempfile" version = "3.24.0" @@ -4421,7 +4719,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5e85aa143ceb072062fc4d6356c1b520a51d636e7bc8e77ec94be3608e5e80c" dependencies = [ "crossbeam-channel", - "dirs", + "dirs 6.0.0", "libappindicator", "muda", "objc2", @@ -4561,6 +4859,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.20.0" @@ -5276,6 +5580,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + [[package]] name = "winreg" version = "0.55.0" @@ -5308,7 +5621,7 @@ dependencies = [ "block2", "cookie", "crossbeam-channel", - "dirs", + "dirs 6.0.0", "dpi", "dunce", "gdkx11", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index bd13a777d..dee2eac64 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "tauri-app" version = "0.1.0" -description = "A Tauri App" -authors = ["you"] +description = "AlphaHuman - Crypto Community Platform" +authors = ["MegaMind"] edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -18,11 +18,37 @@ crate-type = ["staticlib", "cdylib", "rlib"] tauri-build = { version = "2", features = [] } [dependencies] -tauri = { version = "2", features = [] } +# Tauri core and plugins +tauri = { version = "2", features = ["tray-icon", "macos-private-api"] } tauri-plugin-opener = "2" tauri-plugin-deep-link = "2.0.0" +tauri-plugin-autostart = "2" +tauri-plugin-notification = "2" + +# Serialization serde = { version = "1", features = ["derive"] } serde_json = "1" -reqwest = { version = "0.12", features = ["json"] } -tokio = { version = "1", features = ["full"] } +# HTTP client +reqwest = { version = "0.12", features = ["json"] } + +# Async runtime +tokio = { version = "1", features = ["full", "sync"] } + +# Secure storage (keychain) +keyring = "3" + +# Concurrency utilities +once_cell = "1.19" +parking_lot = "0.12" + +# Logging +log = "0.4" +env_logger = "0.11" + +[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] +# Desktop-only dependencies can go here + +[features] +# This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!! +custom-protocol = ["tauri/custom-protocol"] diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index ec2e275b3..154794486 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -6,6 +6,15 @@ "permissions": [ "core:default", "opener:default", - "deep-link:default" + "deep-link:default", + "core:tray:default", + "autostart:default", + "autostart:allow-enable", + "autostart:allow-disable", + "autostart:allow-is-enabled", + "notification:default", + "notification:allow-notify", + "notification:allow-request-permission", + "notification:allow-is-permission-granted" ] } diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png index 6be5e50e9..0306b9522 100644 Binary files a/src-tauri/icons/128x128.png and b/src-tauri/icons/128x128.png differ diff --git a/src-tauri/icons/128x128@2x.png b/src-tauri/icons/128x128@2x.png index e81becee5..c893e5df8 100644 Binary files a/src-tauri/icons/128x128@2x.png and b/src-tauri/icons/128x128@2x.png differ diff --git a/src-tauri/icons/32x32.png b/src-tauri/icons/32x32.png index a437dd517..29e98b34e 100644 Binary files a/src-tauri/icons/32x32.png and b/src-tauri/icons/32x32.png differ diff --git a/src-tauri/icons/Square107x107Logo.png b/src-tauri/icons/Square107x107Logo.png index 0ca4f2719..e67ba4dde 100644 Binary files a/src-tauri/icons/Square107x107Logo.png and b/src-tauri/icons/Square107x107Logo.png differ diff --git a/src-tauri/icons/Square142x142Logo.png b/src-tauri/icons/Square142x142Logo.png index b81f82039..08ceba37a 100644 Binary files a/src-tauri/icons/Square142x142Logo.png and b/src-tauri/icons/Square142x142Logo.png differ diff --git a/src-tauri/icons/Square150x150Logo.png b/src-tauri/icons/Square150x150Logo.png index 624c7bfba..5f0ec6faa 100644 Binary files a/src-tauri/icons/Square150x150Logo.png and b/src-tauri/icons/Square150x150Logo.png differ diff --git a/src-tauri/icons/Square284x284Logo.png b/src-tauri/icons/Square284x284Logo.png index c021d2ba7..d5d1e222e 100644 Binary files a/src-tauri/icons/Square284x284Logo.png and b/src-tauri/icons/Square284x284Logo.png differ diff --git a/src-tauri/icons/Square30x30Logo.png b/src-tauri/icons/Square30x30Logo.png index 621970023..3a4f5f6c9 100644 Binary files a/src-tauri/icons/Square30x30Logo.png and b/src-tauri/icons/Square30x30Logo.png differ diff --git a/src-tauri/icons/Square310x310Logo.png b/src-tauri/icons/Square310x310Logo.png index f9bc04839..402248c76 100644 Binary files a/src-tauri/icons/Square310x310Logo.png and b/src-tauri/icons/Square310x310Logo.png differ diff --git a/src-tauri/icons/Square44x44Logo.png b/src-tauri/icons/Square44x44Logo.png index d5fbfb2ab..fb3b673ec 100644 Binary files a/src-tauri/icons/Square44x44Logo.png and b/src-tauri/icons/Square44x44Logo.png differ diff --git a/src-tauri/icons/Square71x71Logo.png b/src-tauri/icons/Square71x71Logo.png index 63440d798..69cd01964 100644 Binary files a/src-tauri/icons/Square71x71Logo.png and b/src-tauri/icons/Square71x71Logo.png differ diff --git a/src-tauri/icons/Square89x89Logo.png b/src-tauri/icons/Square89x89Logo.png index f3f705af2..ae262c7d8 100644 Binary files a/src-tauri/icons/Square89x89Logo.png and b/src-tauri/icons/Square89x89Logo.png differ diff --git a/src-tauri/icons/StoreLogo.png b/src-tauri/icons/StoreLogo.png index 455638826..4836b8ac1 100644 Binary files a/src-tauri/icons/StoreLogo.png and b/src-tauri/icons/StoreLogo.png differ diff --git a/src-tauri/icons/icon.icns b/src-tauri/icons/icon.icns index 12a5bcee2..2e51d0b91 100644 Binary files a/src-tauri/icons/icon.icns and b/src-tauri/icons/icon.icns differ diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico index b3636e4b2..8b3a82a48 100644 Binary files a/src-tauri/icons/icon.ico and b/src-tauri/icons/icon.ico differ diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png index e1cd2619e..f6fe4e403 100644 Binary files a/src-tauri/icons/icon.png and b/src-tauri/icons/icon.png differ diff --git a/src-tauri/src/commands/auth.rs b/src-tauri/src/commands/auth.rs new file mode 100644 index 000000000..7d9f569e3 --- /dev/null +++ b/src-tauri/src/commands/auth.rs @@ -0,0 +1,99 @@ +use crate::models::auth::{AuthState, TokenExchangeResponse, User}; +use crate::services::session_service::SessionService; +use crate::services::socket_service::SOCKET_SERVICE; +use once_cell::sync::Lazy; +use std::sync::Arc; + +// Global session service instance +pub static SESSION_SERVICE: Lazy> = + Lazy::new(|| Arc::new(SessionService::new())); + +/// Exchange a login token for a session token +/// This is called after the user authenticates via deep link +#[tauri::command] +pub async fn exchange_token( + backend_url: String, + token: String, +) -> Result { + let client = reqwest::Client::new(); + let url = format!("{}/auth/desktop-exchange", backend_url); + + let response = client + .post(&url) + .header("Content-Type", "application/json") + .header("ngrok-skip-browser-warning", "true") + .json(&serde_json::json!({ "token": token })) + .send() + .await + .map_err(|e| format!("Request failed: {}", e))?; + + let status = response.status().as_u16(); + let body: serde_json::Value = response + .json() + .await + .map_err(|e| format!("Failed to parse response: {}", e))?; + + if status != 200 { + let error = body + .get("error") + .and_then(|e| e.as_str()) + .unwrap_or("Unknown error"); + return Err(format!("Exchange failed ({}): {}", status, error)); + } + + // Try to parse and store session + if let Ok(exchange_response) = serde_json::from_value::(body.clone()) { + // Store session securely + let _ = SESSION_SERVICE.store_session(&exchange_response.session_token, &exchange_response.user); + } + + Ok(body) +} + +/// Get the current authentication state +#[tauri::command] +pub fn get_auth_state() -> AuthState { + AuthState { + is_authenticated: SESSION_SERVICE.is_authenticated(), + user: SESSION_SERVICE.get_user(), + } +} + +/// Get the current session token +#[tauri::command] +pub fn get_session_token() -> Option { + SESSION_SERVICE.get_token() +} + +/// Get the current user +#[tauri::command] +pub fn get_current_user() -> Option { + SESSION_SERVICE.get_user() +} + +/// Check if the user is authenticated +#[tauri::command] +pub fn is_authenticated() -> bool { + SESSION_SERVICE.is_authenticated() +} + +/// Logout and clear session +#[tauri::command] +pub fn logout() -> Result<(), String> { + // Request socket to disconnect + let _ = SOCKET_SERVICE.request_disconnect(); + + // Clear session + SESSION_SERVICE.clear_session()?; + + // Clear socket credentials + SOCKET_SERVICE.clear_credentials(); + + Ok(()) +} + +/// Store session manually (used by frontend if needed) +#[tauri::command] +pub fn store_session(token: String, user: User) -> Result<(), String> { + SESSION_SERVICE.store_session(&token, &user) +} diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs new file mode 100644 index 000000000..f47252d34 --- /dev/null +++ b/src-tauri/src/commands/mod.rs @@ -0,0 +1,10 @@ +pub mod auth; +pub mod socket; +pub mod telegram; +pub mod window; + +// Re-export all commands for registration +pub use auth::*; +pub use socket::*; +pub use telegram::*; +pub use window::*; diff --git a/src-tauri/src/commands/socket.rs b/src-tauri/src/commands/socket.rs new file mode 100644 index 000000000..ad9246507 --- /dev/null +++ b/src-tauri/src/commands/socket.rs @@ -0,0 +1,66 @@ +use crate::models::socket::{ConnectionStatus, SocketState}; +use crate::services::socket_service::SOCKET_SERVICE; +use tauri::AppHandle; + +/// Request the frontend to connect to the socket server +#[tauri::command] +pub fn socket_connect( + app: AppHandle, + backend_url: String, + token: String, +) -> Result<(), String> { + // Set app handle for event emission + SOCKET_SERVICE.set_app_handle(app); + + // Request frontend to connect + SOCKET_SERVICE.request_connect(&backend_url, &token) +} + +/// Request the frontend to disconnect from the socket server +#[tauri::command] +pub fn socket_disconnect() -> Result<(), String> { + SOCKET_SERVICE.request_disconnect() +} + +/// Get current socket state +#[tauri::command] +pub fn get_socket_state() -> SocketState { + SOCKET_SERVICE.get_state() +} + +/// Check if socket is connected +#[tauri::command] +pub fn is_socket_connected() -> bool { + SOCKET_SERVICE.is_connected() +} + +/// Report socket connected (called by frontend) +#[tauri::command] +pub fn report_socket_connected(socket_id: Option) { + SOCKET_SERVICE.report_connected(socket_id); +} + +/// Report socket disconnected (called by frontend) +#[tauri::command] +pub fn report_socket_disconnected() { + SOCKET_SERVICE.report_disconnected(); +} + +/// Report socket error (called by frontend) +#[tauri::command] +pub fn report_socket_error(error: String) { + SOCKET_SERVICE.report_error(&error); +} + +/// Update socket status (called by frontend) +#[tauri::command] +pub fn update_socket_status(status: String, socket_id: Option) { + let status = match status.as_str() { + "connected" => ConnectionStatus::Connected, + "connecting" => ConnectionStatus::Connecting, + "reconnecting" => ConnectionStatus::Reconnecting, + "error" => ConnectionStatus::Error, + _ => ConnectionStatus::Disconnected, + }; + SOCKET_SERVICE.update_status(status, socket_id); +} diff --git a/src-tauri/src/commands/telegram.rs b/src-tauri/src/commands/telegram.rs new file mode 100644 index 000000000..a545db310 --- /dev/null +++ b/src-tauri/src/commands/telegram.rs @@ -0,0 +1,35 @@ +use crate::utils::config::get_telegram_widget_url; +use tauri::AppHandle; +use tauri_plugin_opener::OpenerExt; + +/// Start Telegram login flow by opening the widget in system browser +/// The widget will redirect back to the app via deep link after auth +#[tauri::command] +pub async fn start_telegram_login(app: AppHandle) -> Result<(), String> { + let url = get_telegram_widget_url(); + + // Open in system browser using the opener plugin + app.opener() + .open_url(&url, None::<&str>) + .map_err(|e| format!("Failed to open browser: {}", e))?; + + Ok(()) +} + +/// Start Telegram login with a custom backend URL +#[tauri::command] +pub async fn start_telegram_login_with_url( + app: AppHandle, + backend_url: String, +) -> Result<(), String> { + let url = format!( + "{}/auth/telegram-widget?redirect=outsourced://auth", + backend_url + ); + + app.opener() + .open_url(&url, None::<&str>) + .map_err(|e| format!("Failed to open browser: {}", e))?; + + Ok(()) +} diff --git a/src-tauri/src/commands/window.rs b/src-tauri/src/commands/window.rs new file mode 100644 index 000000000..cd3eb227e --- /dev/null +++ b/src-tauri/src/commands/window.rs @@ -0,0 +1,80 @@ +use tauri::{AppHandle, Manager}; + +/// Show the main window +#[tauri::command] +pub fn show_window(app: AppHandle) { + if let Some(window) = app.get_webview_window("main") { + let _ = window.show(); + let _ = window.unminimize(); + let _ = window.set_focus(); + } +} + +/// Hide the main window +#[tauri::command] +pub fn hide_window(app: AppHandle) { + if let Some(window) = app.get_webview_window("main") { + let _ = window.hide(); + } +} + +/// Toggle window visibility +#[tauri::command] +pub fn toggle_window(app: AppHandle) { + if let Some(window) = app.get_webview_window("main") { + match window.is_visible() { + Ok(true) => { + let _ = window.hide(); + } + Ok(false) | Err(_) => { + let _ = window.show(); + let _ = window.unminimize(); + let _ = window.set_focus(); + } + } + } +} + +/// Check if window is visible +#[tauri::command] +pub fn is_window_visible(app: AppHandle) -> bool { + app.get_webview_window("main") + .and_then(|w| w.is_visible().ok()) + .unwrap_or(false) +} + +/// Minimize the main window +#[tauri::command] +pub fn minimize_window(app: AppHandle) { + if let Some(window) = app.get_webview_window("main") { + let _ = window.minimize(); + } +} + +/// Maximize or unmaximize the main window +#[tauri::command] +pub fn maximize_window(app: AppHandle) { + if let Some(window) = app.get_webview_window("main") { + if window.is_maximized().unwrap_or(false) { + let _ = window.unmaximize(); + } else { + let _ = window.maximize(); + } + } +} + +/// Close the main window (triggers minimize on macOS if configured) +#[tauri::command] +pub fn close_window(app: AppHandle) { + if let Some(window) = app.get_webview_window("main") { + let _ = window.close(); + } +} + +/// Set window title +#[tauri::command] +pub fn set_window_title(app: AppHandle, title: String) { + if let Some(window) = app.get_webview_window("main") { + let _ = window.set_title(&title); + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index de1fa2bb1..cfff8d8ab 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,62 +1,209 @@ +//! AlphaHuman Desktop Application +//! +//! This is the Rust backend for the cross-platform crypto community platform. +//! It provides: +//! - System tray with background execution +//! - Deep link authentication +//! - Persistent Socket.io connection +//! - Secure session storage +//! - Native notifications + +mod commands; +mod models; +mod services; +mod utils; + +use commands::*; +use services::socket_service::SOCKET_SERVICE; +use tauri::{ + menu::{Menu, MenuItem}, + tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}, + AppHandle, Manager, RunEvent, +}; + +#[cfg(any(windows, target_os = "linux"))] use tauri_plugin_deep_link::DeepLinkExt; +/// Demo command - can be removed in production #[tauri::command] fn greet(name: &str) -> String { format!("Hello, {}! You've been greeted from Rust!", name) } -#[derive(serde::Deserialize)] -struct ExchangeResponse { - #[serde(rename = "sessionToken")] - session_token: Option, - user: Option, - error: Option, +// Helper function to show the window +fn show_main_window(app: &AppHandle) { + if let Some(window) = app.get_webview_window("main") { + let _ = window.show(); + let _ = window.unminimize(); + let _ = window.set_focus(); + } } -#[tauri::command] -async fn exchange_token(backend_url: String, token: String) -> Result { - let client = reqwest::Client::new(); - let url = format!("{}/auth/desktop-exchange", backend_url); - - let response = client - .post(&url) - .header("Content-Type", "application/json") - .header("ngrok-skip-browser-warning", "true") - .json(&serde_json::json!({ "token": token })) - .send() - .await - .map_err(|e| format!("Request failed: {}", e))?; - - let status = response.status().as_u16(); - let body: serde_json::Value = response - .json() - .await - .map_err(|e| format!("Failed to parse response: {}", e))?; - - if status != 200 { - let error = body - .get("error") - .and_then(|e| e.as_str()) - .unwrap_or("Unknown error"); - return Err(format!("Exchange failed ({}): {}", status, error)); +// Helper function to toggle window visibility +fn toggle_main_window_visibility(app: &AppHandle) { + if let Some(window) = app.get_webview_window("main") { + match window.is_visible() { + Ok(true) => { + let _ = window.hide(); + } + Ok(false) => { + show_main_window(app); + } + Err(_) => { + // If we can't determine visibility, try to show it + show_main_window(app); + } + } + } else { + eprintln!("Could not find window 'main'"); } +} - Ok(body) +// Setup system tray with menu +#[cfg(desktop)] +fn setup_tray(app: &AppHandle) -> Result<(), Box> { + let show_hide_item = MenuItem::with_id(app, "show_hide", "Show/Hide Window", true, None::<&str>)?; + let quit_item = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?; + + let menu = Menu::with_items(app, &[&show_hide_item, &quit_item])?; + + let _tray = TrayIconBuilder::with_id("main-tray") + .icon(app.default_window_icon().unwrap().clone()) + .menu(&menu) + .tooltip("AlphaHuman") + .on_menu_event(move |app, event| match event.id().as_ref() { + "show_hide" => { + toggle_main_window_visibility(app); + } + "quit" => { + // Cleanup before exit - request frontend to disconnect + let _ = SOCKET_SERVICE.request_disconnect(); + app.exit(0); + } + _ => {} + }) + .on_tray_icon_event(|tray, event| match event { + TrayIconEvent::Click { + button: MouseButton::Left, + button_state: MouseButtonState::Up, + .. + } => { + let app = tray.app_handle(); + toggle_main_window_visibility(app); + } + TrayIconEvent::DoubleClick { + button: MouseButton::Left, + .. + } => { + let app = tray.app_handle(); + show_main_window(app); + } + _ => {} + }) + .build(app)?; + + Ok(()) } #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { - tauri::Builder::default() + let mut builder = tauri::Builder::default() + // Plugins .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_deep_link::init()) + .plugin(tauri_plugin_autostart::init( + tauri_plugin_autostart::MacosLauncher::LaunchAgent, + Some(vec!["--minimized"]), + )); + + // Add notification plugin on desktop only + #[cfg(desktop)] + { + builder = builder.plugin(tauri_plugin_notification::init()); + } + + builder + // Setup .setup(|app| { + // Initialize socket service with app handle + SOCKET_SERVICE.set_app_handle(app.handle().clone()); + + // Register deep link handlers (Windows/Linux) #[cfg(any(windows, target_os = "linux"))] { app.deep_link().register_all()?; } + + // Setup system tray (desktop only) + #[cfg(desktop)] + { + setup_tray(app.handle())?; + } + + // macOS-specific: Handle window close event to minimize to tray + #[cfg(target_os = "macos")] + { + if let Some(window) = app.get_webview_window("main") { + let app_handle = app.handle().clone(); + window.on_window_event(move |event| { + if let tauri::WindowEvent::CloseRequested { api, .. } = event { + // Prevent the window from closing, hide it instead + api.prevent_close(); + if let Some(win) = app_handle.get_webview_window("main") { + let _ = win.hide(); + } + } + }); + } + } + Ok(()) }) - .invoke_handler(tauri::generate_handler![greet, exchange_token]) - .run(tauri::generate_context!()) - .expect("error while running tauri application"); + // Register all commands + .invoke_handler(tauri::generate_handler![ + // Demo + greet, + // Auth commands + exchange_token, + get_auth_state, + get_session_token, + get_current_user, + is_authenticated, + logout, + store_session, + // Socket commands + socket_connect, + socket_disconnect, + get_socket_state, + is_socket_connected, + report_socket_connected, + report_socket_disconnected, + report_socket_error, + update_socket_status, + // Telegram commands + start_telegram_login, + start_telegram_login_with_url, + // Window commands + show_window, + hide_window, + toggle_window, + is_window_visible, + minimize_window, + maximize_window, + close_window, + set_window_title, + ]) + .build(tauri::generate_context!()) + .expect("error while building tauri application") + .run(|app_handle, event| { + // Handle macOS Dock icon click (reopen event) + #[cfg(target_os = "macos")] + if let RunEvent::Reopen { .. } = event { + show_main_window(app_handle); + } + + // Suppress unused variable warning on non-macOS + #[cfg(not(target_os = "macos"))] + let _ = (app_handle, event); + }); } diff --git a/src-tauri/src/models/auth.rs b/src-tauri/src/models/auth.rs new file mode 100644 index 000000000..490a3dfaa --- /dev/null +++ b/src-tauri/src/models/auth.rs @@ -0,0 +1,56 @@ +use serde::{Deserialize, Serialize}; + +/// User session information +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Session { + /// JWT session token + pub token: String, + /// User ID + pub user_id: String, + /// When the session was created (Unix timestamp) + pub created_at: u64, + /// When the session expires (Unix timestamp) + pub expires_at: Option, +} + +/// User profile information +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct User { + pub id: String, + #[serde(rename = "firstName")] + pub first_name: Option, + #[serde(rename = "lastName")] + pub last_name: Option, + pub username: Option, + pub email: Option, + #[serde(rename = "telegramId")] + pub telegram_id: Option, +} + +/// Token exchange request payload +#[derive(Debug, Serialize)] +pub struct TokenExchangeRequest { + pub token: String, +} + +/// Token exchange response from backend +#[derive(Debug, Deserialize)] +pub struct TokenExchangeResponse { + #[serde(rename = "sessionToken")] + pub session_token: String, + pub user: User, +} + +/// Auth error response from backend +#[derive(Debug, Deserialize)] +pub struct AuthErrorResponse { + pub success: bool, + pub error: Option, +} + +/// Auth state that can be emitted to frontend +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AuthState { + pub is_authenticated: bool, + pub user: Option, +} diff --git a/src-tauri/src/models/mod.rs b/src-tauri/src/models/mod.rs new file mode 100644 index 000000000..321ecbfb7 --- /dev/null +++ b/src-tauri/src/models/mod.rs @@ -0,0 +1,2 @@ +pub mod auth; +pub mod socket; diff --git a/src-tauri/src/models/socket.rs b/src-tauri/src/models/socket.rs new file mode 100644 index 000000000..c3b5416f2 --- /dev/null +++ b/src-tauri/src/models/socket.rs @@ -0,0 +1,74 @@ +use serde::{Deserialize, Serialize}; + +/// Socket connection status +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum ConnectionStatus { + Disconnected, + Connecting, + Connected, + Reconnecting, + Error, +} + +impl Default for ConnectionStatus { + fn default() -> Self { + Self::Disconnected + } +} + +/// Socket connection state emitted to frontend +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SocketState { + pub status: ConnectionStatus, + pub socket_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +impl Default for SocketState { + fn default() -> Self { + Self { + status: ConnectionStatus::Disconnected, + socket_id: None, + error: None, + } + } +} + +/// Generic socket message wrapper +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SocketMessage { + pub event: String, + pub data: serde_json::Value, +} + +/// MCP request structure (JSON-RPC 2.0) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct McpRequest { + pub jsonrpc: String, + pub id: serde_json::Value, + pub method: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub params: Option, +} + +/// MCP response structure (JSON-RPC 2.0) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct McpResponse { + pub jsonrpc: String, + pub id: serde_json::Value, + #[serde(skip_serializing_if = "Option::is_none")] + pub result: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +/// MCP error structure +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct McpError { + pub code: i32, + pub message: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub data: Option, +} diff --git a/src-tauri/src/services/mod.rs b/src-tauri/src/services/mod.rs new file mode 100644 index 000000000..ef4d1aed2 --- /dev/null +++ b/src-tauri/src/services/mod.rs @@ -0,0 +1,11 @@ +pub mod session_service; +pub mod socket_service; + +#[cfg(desktop)] +pub mod notification_service; + +pub use session_service::SessionService; +pub use socket_service::SocketService; + +#[cfg(desktop)] +pub use notification_service::NotificationService; diff --git a/src-tauri/src/services/notification_service.rs b/src-tauri/src/services/notification_service.rs new file mode 100644 index 000000000..5740291b3 --- /dev/null +++ b/src-tauri/src/services/notification_service.rs @@ -0,0 +1,58 @@ +use tauri::{AppHandle, Manager}; +use tauri_plugin_notification::NotificationExt; + +/// Service for managing native notifications +pub struct NotificationService; + +impl NotificationService { + /// Show a simple notification + pub fn show(app: &AppHandle, title: &str, body: &str) -> Result<(), String> { + app.notification() + .builder() + .title(title) + .body(body) + .show() + .map_err(|e| format!("Failed to show notification: {}", e)) + } + + /// Show a notification with an icon + pub fn show_with_icon( + app: &AppHandle, + title: &str, + body: &str, + icon: &str, + ) -> Result<(), String> { + app.notification() + .builder() + .title(title) + .body(body) + .icon(icon) + .show() + .map_err(|e| format!("Failed to show notification: {}", e)) + } + + /// Show a notification for a new message + pub fn show_message_notification( + app: &AppHandle, + sender: &str, + message: &str, + ) -> Result<(), String> { + Self::show(app, &format!("New message from {}", sender), message) + } + + /// Check if notifications are permitted + pub fn is_permission_granted(app: &AppHandle) -> Result { + app.notification() + .permission_state() + .map(|state| state == tauri_plugin_notification::PermissionState::Granted) + .map_err(|e| format!("Failed to check permission: {}", e)) + } + + /// Request notification permission + pub fn request_permission(app: &AppHandle) -> Result { + app.notification() + .request_permission() + .map(|state| state == tauri_plugin_notification::PermissionState::Granted) + .map_err(|e| format!("Failed to request permission: {}", e)) + } +} diff --git a/src-tauri/src/services/session_service.rs b/src-tauri/src/services/session_service.rs new file mode 100644 index 000000000..61841c988 --- /dev/null +++ b/src-tauri/src/services/session_service.rs @@ -0,0 +1,180 @@ +use crate::models::auth::{Session, User}; +use crate::utils::config::{APP_IDENTIFIER, KEYCHAIN_SERVICE}; +use serde::{Deserialize, Serialize}; +use std::sync::RwLock; +use std::time::{SystemTime, UNIX_EPOCH}; + +/// Session data stored in keychain +#[derive(Debug, Clone, Serialize, Deserialize)] +struct StoredSession { + token: String, + user_id: String, + user: Option, + created_at: u64, + expires_at: Option, +} + +/// Service for managing user sessions with secure storage +pub struct SessionService { + /// In-memory cache of the current session + cached_session: RwLock>, +} + +impl SessionService { + /// Create a new SessionService instance + pub fn new() -> Self { + let service = Self { + cached_session: RwLock::new(None), + }; + // Try to load existing session from keychain + if let Ok(session) = service.load_from_keychain() { + if let Ok(mut cache) = service.cached_session.write() { + *cache = Some(session); + } + } + service + } + + /// Store a new session + pub fn store_session(&self, token: &str, user: &User) -> Result<(), String> { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|e| e.to_string())? + .as_secs(); + + let session = StoredSession { + token: token.to_string(), + user_id: user.id.clone(), + user: Some(user.clone()), + created_at: now, + expires_at: None, // Can be set from token if needed + }; + + // Store in keychain + self.save_to_keychain(&session)?; + + // Update cache + if let Ok(mut cache) = self.cached_session.write() { + *cache = Some(session); + } + + Ok(()) + } + + /// Get the current session token + pub fn get_token(&self) -> Option { + self.cached_session + .read() + .ok() + .and_then(|cache| cache.as_ref().map(|s| s.token.clone())) + } + + /// Get the current session + pub fn get_session(&self) -> Option { + self.cached_session.read().ok().and_then(|cache| { + cache.as_ref().map(|s| Session { + token: s.token.clone(), + user_id: s.user_id.clone(), + created_at: s.created_at, + expires_at: s.expires_at, + }) + }) + } + + /// Get the current user + pub fn get_user(&self) -> Option { + self.cached_session + .read() + .ok() + .and_then(|cache| cache.as_ref().and_then(|s| s.user.clone())) + } + + /// Check if there's an active session + pub fn is_authenticated(&self) -> bool { + self.cached_session + .read() + .ok() + .map(|cache| cache.is_some()) + .unwrap_or(false) + } + + /// Clear the current session (logout) + pub fn clear_session(&self) -> Result<(), String> { + // Clear from keychain + self.delete_from_keychain()?; + + // Clear cache + if let Ok(mut cache) = self.cached_session.write() { + *cache = None; + } + + Ok(()) + } + + /// Save session to OS keychain + #[cfg(not(target_os = "ios"))] + fn save_to_keychain(&self, session: &StoredSession) -> Result<(), String> { + let entry = keyring::Entry::new(KEYCHAIN_SERVICE, APP_IDENTIFIER) + .map_err(|e| format!("Failed to create keyring entry: {}", e))?; + + let json = serde_json::to_string(session) + .map_err(|e| format!("Failed to serialize session: {}", e))?; + + entry + .set_password(&json) + .map_err(|e| format!("Failed to store in keychain: {}", e))?; + + Ok(()) + } + + /// Load session from OS keychain + #[cfg(not(target_os = "ios"))] + fn load_from_keychain(&self) -> Result { + let entry = keyring::Entry::new(KEYCHAIN_SERVICE, APP_IDENTIFIER) + .map_err(|e| format!("Failed to create keyring entry: {}", e))?; + + let json = entry + .get_password() + .map_err(|e| format!("Failed to load from keychain: {}", e))?; + + let session: StoredSession = serde_json::from_str(&json) + .map_err(|e| format!("Failed to deserialize session: {}", e))?; + + Ok(session) + } + + /// Delete session from OS keychain + #[cfg(not(target_os = "ios"))] + fn delete_from_keychain(&self) -> Result<(), String> { + let entry = keyring::Entry::new(KEYCHAIN_SERVICE, APP_IDENTIFIER) + .map_err(|e| format!("Failed to create keyring entry: {}", e))?; + + // Ignore error if entry doesn't exist + let _ = entry.delete_credential(); + + Ok(()) + } + + // iOS fallback implementations (keyring not supported) + #[cfg(target_os = "ios")] + fn save_to_keychain(&self, _session: &StoredSession) -> Result<(), String> { + // iOS uses different secure storage, handled by frontend + Ok(()) + } + + #[cfg(target_os = "ios")] + fn load_from_keychain(&self) -> Result { + Err("Not implemented for iOS".to_string()) + } + + #[cfg(target_os = "ios")] + fn delete_from_keychain(&self) -> Result<(), String> { + Ok(()) + } +} + +impl Default for SessionService { + fn default() -> Self { + Self::new() + } +} diff --git a/src-tauri/src/services/socket_service.rs b/src-tauri/src/services/socket_service.rs new file mode 100644 index 000000000..1954edf36 --- /dev/null +++ b/src-tauri/src/services/socket_service.rs @@ -0,0 +1,182 @@ +//! Socket.io state management service +//! +//! This service manages socket connection state and communicates with the frontend. +//! The actual Socket.io client runs in the frontend (JavaScript), while this Rust +//! service: +//! - Tracks connection state +//! - Emits events to the frontend +//! - Manages background execution (socket stays connected when window is hidden) +//! +//! For background execution, the Tauri app keeps running in the system tray, +//! and the frontend's Socket.io connection persists because the WebView is not +//! destroyed when the window is hidden. + +use crate::models::socket::{ConnectionStatus, SocketState}; +use parking_lot::RwLock; +use std::sync::Arc; +use tauri::{AppHandle, Emitter}; + +/// Events emitted to the frontend +pub mod events { + pub const SOCKET_CONNECTED: &str = "socket:connected"; + pub const SOCKET_DISCONNECTED: &str = "socket:disconnected"; + pub const SOCKET_ERROR: &str = "socket:error"; + pub const SOCKET_MESSAGE: &str = "socket:message"; + pub const SOCKET_STATE_CHANGED: &str = "socket:state_changed"; + pub const SOCKET_SHOULD_CONNECT: &str = "socket:should_connect"; + pub const SOCKET_SHOULD_DISCONNECT: &str = "socket:should_disconnect"; +} + +/// Socket state management service +/// +/// This service tracks socket connection state and provides an interface +/// for the frontend to report connection status and for the backend to +/// request connection/disconnection. +pub struct SocketService { + /// Current connection status (reported by frontend) + status: RwLock, + /// Socket ID once connected (reported by frontend) + socket_id: RwLock>, + /// Auth token for connection + auth_token: RwLock>, + /// Backend URL + backend_url: RwLock, + /// App handle for emitting events + app_handle: RwLock>, +} + +impl SocketService { + /// Create a new SocketService + pub fn new() -> Self { + Self { + status: RwLock::new(ConnectionStatus::Disconnected), + socket_id: RwLock::new(None), + auth_token: RwLock::new(None), + backend_url: RwLock::new(String::new()), + app_handle: RwLock::new(None), + } + } + + /// Set the app handle for emitting events + pub fn set_app_handle(&self, handle: AppHandle) { + *self.app_handle.write() = Some(handle); + } + + /// Get current connection status + pub fn get_status(&self) -> ConnectionStatus { + *self.status.read() + } + + /// Get current socket state + pub fn get_state(&self) -> SocketState { + SocketState { + status: *self.status.read(), + socket_id: self.socket_id.read().clone(), + error: None, + } + } + + /// Check if connected + pub fn is_connected(&self) -> bool { + *self.status.read() == ConnectionStatus::Connected + } + + /// Store connection parameters and tell frontend to connect + pub fn request_connect(&self, backend_url: &str, token: &str) -> Result<(), String> { + *self.backend_url.write() = backend_url.to_string(); + *self.auth_token.write() = Some(token.to_string()); + + // Tell frontend to connect + if let Some(ref app) = *self.app_handle.read() { + app.emit( + events::SOCKET_SHOULD_CONNECT, + serde_json::json!({ + "backendUrl": backend_url, + "token": token + }), + ) + .map_err(|e| format!("Failed to emit connect event: {}", e))?; + } + + Ok(()) + } + + /// Tell frontend to disconnect + pub fn request_disconnect(&self) -> Result<(), String> { + if let Some(ref app) = *self.app_handle.read() { + app.emit(events::SOCKET_SHOULD_DISCONNECT, ()) + .map_err(|e| format!("Failed to emit disconnect event: {}", e))?; + } + + Ok(()) + } + + /// Update connection status (called by frontend via command) + pub fn update_status(&self, status: ConnectionStatus, socket_id: Option) { + *self.status.write() = status; + *self.socket_id.write() = socket_id.clone(); + + // Emit state change to any listeners + if let Some(ref app) = *self.app_handle.read() { + let state = SocketState { + status, + socket_id, + error: None, + }; + let _ = app.emit(events::SOCKET_STATE_CHANGED, &state); + } + } + + /// Report connection (called by frontend) + pub fn report_connected(&self, socket_id: Option) { + self.update_status(ConnectionStatus::Connected, socket_id); + } + + /// Report disconnection (called by frontend) + pub fn report_disconnected(&self) { + self.update_status(ConnectionStatus::Disconnected, None); + } + + /// Report error (called by frontend) + pub fn report_error(&self, error: &str) { + *self.status.write() = ConnectionStatus::Error; + + if let Some(ref app) = *self.app_handle.read() { + let state = SocketState { + status: ConnectionStatus::Error, + socket_id: None, + error: Some(error.to_string()), + }; + let _ = app.emit(events::SOCKET_STATE_CHANGED, &state); + let _ = app.emit(events::SOCKET_ERROR, error); + } + } + + /// Get stored connection parameters for reconnection + pub fn get_connection_params(&self) -> Option<(String, String)> { + let backend_url = self.backend_url.read().clone(); + let token = self.auth_token.read().clone(); + + if !backend_url.is_empty() { + token.map(|t| (backend_url, t)) + } else { + None + } + } + + /// Clear stored credentials + pub fn clear_credentials(&self) { + *self.auth_token.write() = None; + *self.backend_url.write() = String::new(); + } +} + +impl Default for SocketService { + fn default() -> Self { + Self::new() + } +} + +// Global singleton instance +use once_cell::sync::Lazy; +pub static SOCKET_SERVICE: Lazy> = Lazy::new(|| Arc::new(SocketService::new())); diff --git a/src-tauri/src/utils/config.rs b/src-tauri/src/utils/config.rs new file mode 100644 index 000000000..03b5638a9 --- /dev/null +++ b/src-tauri/src/utils/config.rs @@ -0,0 +1,38 @@ +/// Configuration constants and environment utilities +/// +/// This module provides configuration values that can be +/// overridden via environment variables at runtime. + +use std::env; + +/// Default backend URL (can be overridden via BACKEND_URL env var) +pub const DEFAULT_BACKEND_URL: &str = "https://api.alphahuman.io"; + +/// Application identifier for keychain storage +pub const APP_IDENTIFIER: &str = "com.megamind.tauri-app"; + +/// Service name for keychain +pub const KEYCHAIN_SERVICE: &str = "AlphaHuman"; + +/// Deep link scheme +pub const DEEP_LINK_SCHEME: &str = "outsourced"; + +/// Socket.io reconnection settings +pub const SOCKET_RECONNECT_ATTEMPTS: u32 = 5; +pub const SOCKET_RECONNECT_DELAY_MS: u64 = 1000; +pub const SOCKET_PING_INTERVAL_MS: u64 = 25000; + +/// Get the backend URL from environment or use default +pub fn get_backend_url() -> String { + env::var("BACKEND_URL").unwrap_or_else(|_| DEFAULT_BACKEND_URL.to_string()) +} + +/// Get the Telegram widget auth URL +pub fn get_telegram_widget_url() -> String { + format!("{}/auth/telegram-widget?redirect={}://auth", get_backend_url(), DEEP_LINK_SCHEME) +} + +/// Get the token exchange endpoint URL +pub fn get_token_exchange_url() -> String { + format!("{}/auth/desktop-exchange", get_backend_url()) +} diff --git a/src-tauri/src/utils/mod.rs b/src-tauri/src/utils/mod.rs new file mode 100644 index 000000000..ef68c3694 --- /dev/null +++ b/src-tauri/src/utils/mod.rs @@ -0,0 +1 @@ +pub mod config; diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index d42e8e455..ade51bfa5 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,6 +1,6 @@ { "$schema": "https://schema.tauri.app/config/2", - "productName": "tauri-app", + "productName": "AlphaHuman", "version": "0.1.0", "identifier": "com.megamind.tauri-app", "build": { @@ -12,14 +12,20 @@ "app": { "windows": [ { - "title": "tauri-app", + "label": "main", + "title": "AlphaHuman", "width": 800, - "height": 600 + "height": 600, + "visible": false, + "decorations": true, + "resizable": true, + "center": true } ], "security": { "csp": null - } + }, + "macOSPrivateApi": true }, "bundle": { "active": true, diff --git a/src/components/TelegramLoginButton.tsx b/src/components/TelegramLoginButton.tsx index 81eac730d..d55d89d4e 100644 --- a/src/components/TelegramLoginButton.tsx +++ b/src/components/TelegramLoginButton.tsx @@ -1,5 +1,6 @@ -import { TELEGRAM_BOT_USERNAME } from "../utils/config"; +import { BACKEND_URL, TELEGRAM_BOT_USERNAME } from "../utils/config"; import { openUrl } from "../utils/openUrl"; +import { isTauri, startTelegramLoginWithUrl } from "../utils/tauriCommands"; interface TelegramLoginButtonProps { @@ -13,9 +14,24 @@ const TelegramLoginButton = ({ text = "Yes, Login with Telegram", disabled: externalDisabled = false, }: TelegramLoginButtonProps) => { - const handleTelegramLogin = () => { + const handleTelegramLogin = async () => { if (externalDisabled) return; - openUrl(`https://t.me/${TELEGRAM_BOT_USERNAME}?start=login`); + + console.log("Starting Telegram login", isTauri()); + + // Desktop (Tauri): use system browser → backend Telegram widget → deep link back to app. + if (isTauri()) { + try { + await startTelegramLoginWithUrl(BACKEND_URL); + return; + } catch (err) { + console.error("[TelegramLoginButton] Failed to start desktop Telegram login:", err); + // Fall through to browser fallback below. + } + } + + // Web fallback: open bot (existing flow). + await openUrl(`https://t.me/${TELEGRAM_BOT_USERNAME}?start=login`); }; const isDisabled = externalDisabled; diff --git a/src/providers/SocketProvider.tsx b/src/providers/SocketProvider.tsx index e3aa2bc31..5884e0c9e 100644 --- a/src/providers/SocketProvider.tsx +++ b/src/providers/SocketProvider.tsx @@ -9,17 +9,44 @@ import { updateTelegramMCPServerSocket, cleanupTelegramMCPServer, } from "../lib/mcp/telegram"; +import { + isTauri, + setupTauriSocketListeners, + cleanupTauriSocketListeners, + reportSocketConnected, + reportSocketDisconnected, + reportSocketError, + updateSocketStatus, +} from "../utils/tauriSocket"; /** * SocketProvider manages the socket connection based on JWT token * - Connects when token is set * - Disconnects when token is unset + * - Integrates with Tauri for background persistence */ const SocketProvider = ({ children }: { children: React.ReactNode }) => { const token = useAppSelector((state) => state.auth.token); const socketStatus = useAppSelector(selectSocketStatus); const previousTokenRef = useRef(null); + const tauriListenersSetup = useRef(false); + // Setup Tauri event listeners once + useEffect(() => { + if (isTauri() && !tauriListenersSetup.current) { + setupTauriSocketListeners(); + tauriListenersSetup.current = true; + } + + return () => { + if (isTauri() && tauriListenersSetup.current) { + cleanupTauriSocketListeners(); + tauriListenersSetup.current = false; + } + }; + }, []); + + // Handle socket connection based on token useEffect(() => { const previousToken = previousTokenRef.current; @@ -27,6 +54,11 @@ const SocketProvider = ({ children }: { children: React.ReactNode }) => { if (token && token !== previousToken) { socketService.connect(token); previousTokenRef.current = token; + + // Report to Rust that we're connecting + if (isTauri()) { + updateSocketStatus("connecting"); + } } // Token was unset - disconnect @@ -34,10 +66,15 @@ const SocketProvider = ({ children }: { children: React.ReactNode }) => { socketService.disconnect(); cleanupTelegramMCPServer(); previousTokenRef.current = null; + + // Report to Rust + if (isTauri()) { + reportSocketDisconnected(); + } } }, [token]); - // Handle MCP initialization when socket connects + // Handle MCP initialization and Tauri status reporting useEffect(() => { if (socketStatus === "connected") { const socket = socketService.getSocket(); @@ -48,18 +85,58 @@ const SocketProvider = ({ children }: { children: React.ReactNode }) => { } else { initTelegramMCPServer(socket); } + + // Report to Rust + if (isTauri()) { + reportSocketConnected(socket?.id); + } } else if (socketStatus === "disconnected") { cleanupTelegramMCPServer(); + + // Report to Rust + if (isTauri()) { + reportSocketDisconnected(); + } + } else if (socketStatus === "connecting") { + // Report connecting status to Rust + if (isTauri()) { + updateSocketStatus("connecting"); + } } }, [socketStatus]); + // Listen for socket errors and report to Rust + useEffect(() => { + const socket = socketService.getSocket(); + if (!socket) return; + + const handleError = (error: Error) => { + if (isTauri()) { + reportSocketError(error.message || "Socket error"); + } + }; + + const handleConnectError = (error: Error) => { + if (isTauri()) { + reportSocketError(error.message || "Connection error"); + updateSocketStatus("error"); + } + }; + + socket.on("error", handleError); + socket.on("connect_error", handleConnectError); + + return () => { + socket.off("error", handleError); + socket.off("connect_error", handleConnectError); + }; + }, [socketStatus]); + // Cleanup on unmount only - // Note: This should only run when the entire app unmounts, not on re-renders useEffect(() => { return () => { // Only disconnect on actual unmount (e.g., app closing) // Don't disconnect on re-renders or route changes - // Check if token still exists - if it does, don't disconnect (might be a re-render) const currentToken = store.getState().auth.token; if (!currentToken) { socketService.disconnect(); diff --git a/src/providers/TelegramProvider.tsx b/src/providers/TelegramProvider.tsx index 26cd7f3dd..c856f3b47 100644 --- a/src/providers/TelegramProvider.tsx +++ b/src/providers/TelegramProvider.tsx @@ -176,20 +176,8 @@ const TelegramProvider = ({ children }: TelegramProviderProps) => { } // Only check if the service is actually ready - if (!mtprotoService.isReady()) { - return; - } - - const uid = userId; - const doCheck = async () => { - try { - await mtprotoService.checkConnection(uid); - } catch (error) { - console.warn("Telegram connection check failed:", error); - } - }; - - doCheck(); + if (!mtprotoService.isReady()) return; + checkConnection(); }, [shouldSetupTelegram, isInitialized, connectionStatus, userId]); const checkConnection = async (): Promise => { diff --git a/src/utils/desktopDeepLinkListener.ts b/src/utils/desktopDeepLinkListener.ts index 8e118190b..0dcda0398 100644 --- a/src/utils/desktopDeepLinkListener.ts +++ b/src/utils/desktopDeepLinkListener.ts @@ -1,16 +1,9 @@ import { getCurrent, onOpenUrl } from "@tauri-apps/plugin-deep-link"; -import { invoke } from "@tauri-apps/api/core"; +import { invoke, isTauri as coreIsTauri } from "@tauri-apps/api/core"; import { BACKEND_URL } from "./config"; import { store } from "../store"; import { setToken } from "../store/authSlice"; -/** - * Check if running in Tauri desktop app - */ -const isTauri = (): boolean => { - return typeof window !== "undefined" && !!(window as any).__TAURI__; -}; - /** * Handle a list of deep link URLs delivered by the Tauri deep-link plugin. * Parses `outsourced://auth?token=...` URLs and exchanges the token for a @@ -28,6 +21,10 @@ const handleDeepLinkUrls = async (urls: string[] | null | undefined) => { if (parsed.protocol !== "outsourced:") { return; } + // Harden: ensure this deep link is intended for auth handoff + if (parsed.hostname !== "auth") { + return; + } const token = parsed.searchParams.get("token"); if (!token) { @@ -37,48 +34,31 @@ const handleDeepLinkUrls = async (urls: string[] | null | undefined) => { console.log("[DeepLink] Received token"); - let sessionToken: string | undefined; - try { - if (isTauri()) { - // Use Tauri invoke to call Rust backend (bypasses CORS) - const data = await invoke<{ - sessionToken?: string; - }>("exchange_token", { backendUrl: BACKEND_URL, token }); - - sessionToken = data.sessionToken; - } else { - // Browser fallback: use fetch API - const response = await fetch(`${BACKEND_URL}/auth/desktop-exchange`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ token }), - }); - - if (response.ok) { - const data = await response.json(); - sessionToken = data.data?.sessionToken; - } - } + // Bring app window to foreground so macOS users actually see completion. + // (In this app, the window can start hidden and live in the tray.) + await invoke("show_window"); } catch (err) { - console.warn("[DeepLink] Token exchange failed:", err); + // Not fatal; we still continue the auth flow. + console.warn("[DeepLink] Failed to show window:", err); } - // If the backend didn't return a session, store the raw token so the - // login flow can proceed. This path is used during development when - // the backend server is not yet running. + // Use Tauri invoke to call Rust backend (bypasses CORS) + const data = await invoke<{ + sessionToken?: string; + }>("exchange_token", { backendUrl: BACKEND_URL, token }); + + const sessionToken = data.sessionToken; if (!sessionToken) { - sessionToken = token; + console.warn("[DeepLink] Token exchange did not return a sessionToken"); + return; } // Store session token in store store.dispatch(setToken(sessionToken)); - // Navigate to post-login flow. This listener runs outside the React - // router context, so we assign the path directly and reload. - window.location.replace("/onboarding/step1"); + // Navigate to post-login flow. We use HashRouter, so update the hash route. + window.location.hash = "/onboarding"; } catch (error) { console.error("[DeepLink] Failed to handle deep link URL:", url, error); } @@ -91,7 +71,7 @@ const handleDeepLinkUrls = async (urls: string[] | null | undefined) => { */ export const setupDesktopDeepLinkListener = async () => { // Only set up deep link listener in Tauri environment - if (!isTauri()) { + if (!coreIsTauri()) { return; } diff --git a/src/utils/openUrl.ts b/src/utils/openUrl.ts index 9adda2be6..d0e3bacac 100644 --- a/src/utils/openUrl.ts +++ b/src/utils/openUrl.ts @@ -1,4 +1,5 @@ import { openUrl as tauriOpenUrl } from "@tauri-apps/plugin-opener"; +import { isTauri } from "@tauri-apps/api/core"; /** * Opens a URL in the default browser or app. @@ -6,7 +7,7 @@ import { openUrl as tauriOpenUrl } from "@tauri-apps/plugin-opener"; */ export const openUrl = async (url: string): Promise => { // Check if we're running in Tauri desktop app - if ((window as any).__TAURI__) { + if (isTauri()) { try { await tauriOpenUrl(url); return; diff --git a/src/utils/tauriCommands.ts b/src/utils/tauriCommands.ts new file mode 100644 index 000000000..f2ed90691 --- /dev/null +++ b/src/utils/tauriCommands.ts @@ -0,0 +1,189 @@ +/** + * Tauri Commands + * + * Helper functions for invoking Tauri commands from the frontend. + */ + +import { invoke, isTauri as coreIsTauri } from '@tauri-apps/api/core'; + +// Check if we're running in Tauri +export const isTauri = (): boolean => { + // Tauri v2: prefer the official runtime check over window globals. + return coreIsTauri(); +}; + +/** + * Start Telegram login via the widget in the system browser + * The backend will redirect back to the app via deep link + */ +export async function startTelegramLogin(): Promise { + if (!isTauri()) { + throw new Error('Not running in Tauri'); + } + + await invoke('start_telegram_login'); +} + +/** + * Start Telegram login with a custom backend URL + */ +export async function startTelegramLoginWithUrl(backendUrl: string): Promise { + if (!isTauri()) { + throw new Error('Not running in Tauri'); + } + + await invoke('start_telegram_login_with_url', { backendUrl }); +} + +/** + * Exchange a login token for a session token + */ +export async function exchangeToken( + backendUrl: string, + token: string +): Promise<{ sessionToken: string; user: object }> { + if (!isTauri()) { + throw new Error('Not running in Tauri'); + } + + return await invoke('exchange_token', { backendUrl, token }); +} + +/** + * Get the current authentication state from Rust + */ +export async function getAuthState(): Promise<{ + is_authenticated: boolean; + user: object | null; +}> { + if (!isTauri()) { + return { is_authenticated: false, user: null }; + } + + return await invoke('get_auth_state'); +} + +/** + * Get the session token from secure storage + */ +export async function getSessionToken(): Promise { + if (!isTauri()) { + return null; + } + + return await invoke('get_session_token'); +} + +/** + * Logout and clear session + */ +export async function logout(): Promise { + if (!isTauri()) { + return; + } + + await invoke('logout'); +} + +/** + * Store session in secure storage + */ +export async function storeSession( + token: string, + user: object +): Promise { + if (!isTauri()) { + return; + } + + await invoke('store_session', { token, user }); +} + +/** + * Show the main window + */ +export async function showWindow(): Promise { + if (!isTauri()) { + return; + } + + await invoke('show_window'); +} + +/** + * Hide the main window + */ +export async function hideWindow(): Promise { + if (!isTauri()) { + return; + } + + await invoke('hide_window'); +} + +/** + * Toggle window visibility + */ +export async function toggleWindow(): Promise { + if (!isTauri()) { + return; + } + + await invoke('toggle_window'); +} + +/** + * Check if window is visible + */ +export async function isWindowVisible(): Promise { + if (!isTauri()) { + return true; // In browser, window is always visible + } + + return await invoke('is_window_visible'); +} + +/** + * Minimize the window + */ +export async function minimizeWindow(): Promise { + if (!isTauri()) { + return; + } + + await invoke('minimize_window'); +} + +/** + * Maximize or unmaximize the window + */ +export async function maximizeWindow(): Promise { + if (!isTauri()) { + return; + } + + await invoke('maximize_window'); +} + +/** + * Close the window (minimizes to tray on macOS) + */ +export async function closeWindow(): Promise { + if (!isTauri()) { + return; + } + + await invoke('close_window'); +} + +/** + * Set the window title + */ +export async function setWindowTitle(title: string): Promise { + if (!isTauri()) { + document.title = title; + return; + } + + await invoke('set_window_title', { title }); +} diff --git a/src/utils/tauriSocket.ts b/src/utils/tauriSocket.ts new file mode 100644 index 000000000..a24a5a7fc --- /dev/null +++ b/src/utils/tauriSocket.ts @@ -0,0 +1,185 @@ +/** + * Tauri Socket Integration + * + * This module provides integration between the Rust backend and the frontend + * Socket.io service. In Tauri, the socket stays connected even when the + * window is hidden because the WebView is not destroyed. + * + * The Rust backend emits events to coordinate: + * - socket:should_connect - When Rust wants frontend to connect + * - socket:should_disconnect - When Rust wants frontend to disconnect + * + * The frontend reports status back: + * - report_socket_connected - When connection established + * - report_socket_disconnected - When disconnected + * - report_socket_error - When error occurs + */ + +import { listen, UnlistenFn } from '@tauri-apps/api/event'; +import { invoke, isTauri as coreIsTauri } from '@tauri-apps/api/core'; +import { socketService } from '../services/socketService'; + +// Check if we're running in Tauri +export const isTauri = (): boolean => { + return coreIsTauri(); +}; + +let unlistenConnect: UnlistenFn | null = null; +let unlistenDisconnect: UnlistenFn | null = null; + +/** + * Setup Tauri socket event listeners + * This should be called once when the app starts + */ +export async function setupTauriSocketListeners(): Promise { + if (!isTauri()) { + return; + } + + try { + // Listen for connect requests from Rust + unlistenConnect = await listen<{ backendUrl: string; token: string }>( + 'socket:should_connect', + async (event) => { + console.log('[TauriSocket] Received connect request'); + const { token } = event.payload; + + try { + socketService.connect(token); + // Note: We'll report connected status when socket actually connects + } catch (error) { + console.error('[TauriSocket] Failed to connect:', error); + await invoke('report_socket_error', { + error: error instanceof Error ? error.message : 'Connection failed' + }); + } + } + ); + + // Listen for disconnect requests from Rust + unlistenDisconnect = await listen( + 'socket:should_disconnect', + async () => { + console.log('[TauriSocket] Received disconnect request'); + socketService.disconnect(); + await invoke('report_socket_disconnected'); + } + ); + + console.log('[TauriSocket] Event listeners setup complete'); + } catch (error) { + console.error('[TauriSocket] Failed to setup listeners:', error); + } +} + +/** + * Cleanup Tauri socket event listeners + */ +export function cleanupTauriSocketListeners(): void { + if (unlistenConnect) { + unlistenConnect(); + unlistenConnect = null; + } + if (unlistenDisconnect) { + unlistenDisconnect(); + unlistenDisconnect = null; + } +} + +/** + * Report socket connected status to Rust + */ +export async function reportSocketConnected(socketId?: string): Promise { + if (!isTauri()) { + return; + } + + try { + await invoke('report_socket_connected', { socketId: socketId ?? null }); + console.log('[TauriSocket] Reported connected'); + } catch (error) { + console.error('[TauriSocket] Failed to report connected:', error); + } +} + +/** + * Report socket disconnected status to Rust + */ +export async function reportSocketDisconnected(): Promise { + if (!isTauri()) { + return; + } + + try { + await invoke('report_socket_disconnected'); + console.log('[TauriSocket] Reported disconnected'); + } catch (error) { + console.error('[TauriSocket] Failed to report disconnected:', error); + } +} + +/** + * Report socket error to Rust + */ +export async function reportSocketError(error: string): Promise { + if (!isTauri()) { + return; + } + + try { + await invoke('report_socket_error', { error }); + console.log('[TauriSocket] Reported error:', error); + } catch (error) { + console.error('[TauriSocket] Failed to report error:', error); + } +} + +/** + * Update socket status in Rust + */ +export async function updateSocketStatus( + status: 'connected' | 'connecting' | 'disconnected' | 'reconnecting' | 'error', + socketId?: string +): Promise { + if (!isTauri()) { + return; + } + + try { + await invoke('update_socket_status', { status, socketId: socketId ?? null }); + } catch (error) { + console.error('[TauriSocket] Failed to update status:', error); + } +} + +/** + * Get session token from Rust secure storage + */ +export async function getSecureToken(): Promise { + if (!isTauri()) { + return null; + } + + try { + return await invoke('get_session_token'); + } catch (error) { + console.error('[TauriSocket] Failed to get secure token:', error); + return null; + } +} + +/** + * Check if user is authenticated (from Rust) + */ +export async function isAuthenticatedFromRust(): Promise { + if (!isTauri()) { + return false; + } + + try { + return await invoke('is_authenticated'); + } catch (error) { + console.error('[TauriSocket] Failed to check auth:', error); + return false; + } +} diff --git a/yarn.lock b/yarn.lock index fe144c93d..85b1130f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -21,7 +21,7 @@ resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz" integrity sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg== -"@babel/core@^7.0.0", "@babel/core@^7.0.0-0", "@babel/core@^7.28.0": +"@babel/core@^7.28.0": version "7.28.6" resolved "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz" integrity sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw== @@ -170,11 +170,136 @@ resolved "https://registry.npmjs.org/@cryptography/aes/-/aes-0.1.1.tgz" integrity sha512-PcYz4FDGblO6tM2kSC+VzhhK62vml6k6/YAkiWtyPvrgJVfnDRoHGDtKn5UiaRRUrvUTTocBpvc2rRgTCqxjsg== +"@esbuild/aix-ppc64@0.27.2": + version "0.27.2" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz#521cbd968dcf362094034947f76fa1b18d2d403c" + integrity sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw== + +"@esbuild/android-arm64@0.27.2": + version "0.27.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz#61ea550962d8aa12a9b33194394e007657a6df57" + integrity sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA== + +"@esbuild/android-arm@0.27.2": + version "0.27.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.27.2.tgz#554887821e009dd6d853f972fde6c5143f1de142" + integrity sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA== + +"@esbuild/android-x64@0.27.2": + version "0.27.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.27.2.tgz#a7ce9d0721825fc578f9292a76d9e53334480ba2" + integrity sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A== + "@esbuild/darwin-arm64@0.27.2": version "0.27.2" resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz" integrity sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg== +"@esbuild/darwin-x64@0.27.2": + version "0.27.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz#e741fa6b1abb0cd0364126ba34ca17fd5e7bf509" + integrity sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA== + +"@esbuild/freebsd-arm64@0.27.2": + version "0.27.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz#2b64e7116865ca172d4ce034114c21f3c93e397c" + integrity sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g== + +"@esbuild/freebsd-x64@0.27.2": + version "0.27.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz#e5252551e66f499e4934efb611812f3820e990bb" + integrity sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA== + +"@esbuild/linux-arm64@0.27.2": + version "0.27.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz#dc4acf235531cd6984f5d6c3b13dbfb7ddb303cb" + integrity sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw== + +"@esbuild/linux-arm@0.27.2": + version "0.27.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz#56a900e39240d7d5d1d273bc053daa295c92e322" + integrity sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw== + +"@esbuild/linux-ia32@0.27.2": + version "0.27.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz#d4a36d473360f6870efcd19d52bbfff59a2ed1cc" + integrity sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w== + +"@esbuild/linux-loong64@0.27.2": + version "0.27.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz#fcf0ab8c3eaaf45891d0195d4961cb18b579716a" + integrity sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg== + +"@esbuild/linux-mips64el@0.27.2": + version "0.27.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz#598b67d34048bb7ee1901cb12e2a0a434c381c10" + integrity sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw== + +"@esbuild/linux-ppc64@0.27.2": + version "0.27.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz#3846c5df6b2016dab9bc95dde26c40f11e43b4c0" + integrity sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ== + +"@esbuild/linux-riscv64@0.27.2": + version "0.27.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz#173d4475b37c8d2c3e1707e068c174bb3f53d07d" + integrity sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA== + +"@esbuild/linux-s390x@0.27.2": + version "0.27.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz#f7a4790105edcab8a5a31df26fbfac1aa3dacfab" + integrity sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w== + +"@esbuild/linux-x64@0.27.2": + version "0.27.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz#2ecc1284b1904aeb41e54c9ddc7fcd349b18f650" + integrity sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA== + +"@esbuild/netbsd-arm64@0.27.2": + version "0.27.2" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz#e2863c2cd1501845995cb11adf26f7fe4be527b0" + integrity sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw== + +"@esbuild/netbsd-x64@0.27.2": + version "0.27.2" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz#93f7609e2885d1c0b5a1417885fba8d1fcc41272" + integrity sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA== + +"@esbuild/openbsd-arm64@0.27.2": + version "0.27.2" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz#a1985604a203cdc325fd47542e106fafd698f02e" + integrity sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA== + +"@esbuild/openbsd-x64@0.27.2": + version "0.27.2" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz#8209e46c42f1ffbe6e4ef77a32e1f47d404ad42a" + integrity sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg== + +"@esbuild/openharmony-arm64@0.27.2": + version "0.27.2" + resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz#8fade4441893d9cc44cbd7dcf3776f508ab6fb2f" + integrity sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag== + +"@esbuild/sunos-x64@0.27.2": + version "0.27.2" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz#980d4b9703a16f0f07016632424fc6d9a789dfc2" + integrity sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg== + +"@esbuild/win32-arm64@0.27.2": + version "0.27.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz#1c09a3633c949ead3d808ba37276883e71f6111a" + integrity sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg== + +"@esbuild/win32-ia32@0.27.2": + version "0.27.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz#1b1e3a63ad4bef82200fef4e369e0fff7009eee5" + integrity sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ== + +"@esbuild/win32-x64@0.27.2": + version "0.27.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz#9e585ab6086bef994c6e8a5b3a0481219ada862b" + integrity sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ== + "@jridgewell/gen-mapping@^0.3.12", "@jridgewell/gen-mapping@^0.3.2", "@jridgewell/gen-mapping@^0.3.5": version "0.3.13" resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz" @@ -217,7 +342,7 @@ "@nodelib/fs.stat" "2.0.5" run-parallel "^1.1.9" -"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5": +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": version "2.0.5" resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== @@ -265,11 +390,131 @@ estree-walker "^2.0.2" picomatch "^4.0.2" +"@rollup/rollup-android-arm-eabi@4.56.0": + version "4.56.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.56.0.tgz#067cfcd81f1c1bfd92aefe3ad5ef1523549d5052" + integrity sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw== + +"@rollup/rollup-android-arm64@4.56.0": + version "4.56.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.56.0.tgz#85e39a44034d7d4e4fee2a1616f0bddb85a80517" + integrity sha512-lfbVUbelYqXlYiU/HApNMJzT1E87UPGvzveGg2h0ktUNlOCxKlWuJ9jtfvs1sKHdwU4fzY7Pl8sAl49/XaEk6Q== + "@rollup/rollup-darwin-arm64@4.56.0": version "4.56.0" resolved "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.56.0.tgz" integrity sha512-EgxD1ocWfhoD6xSOeEEwyE7tDvwTgZc8Bss7wCWe+uc7wO8G34HHCUH+Q6cHqJubxIAnQzAsyUsClt0yFLu06w== +"@rollup/rollup-darwin-x64@4.56.0": + version "4.56.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.56.0.tgz#89ae6c66b1451609bd1f297da9384463f628437d" + integrity sha512-1vXe1vcMOssb/hOF8iv52A7feWW2xnu+c8BV4t1F//m9QVLTfNVpEdja5ia762j/UEJe2Z1jAmEqZAK42tVW3g== + +"@rollup/rollup-freebsd-arm64@4.56.0": + version "4.56.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.56.0.tgz#cdbdb9947b26e76c188a31238c10639347413628" + integrity sha512-bof7fbIlvqsyv/DtaXSck4VYQ9lPtoWNFCB/JY4snlFuJREXfZnm+Ej6yaCHfQvofJDXLDMTVxWscVSuQvVWUQ== + +"@rollup/rollup-freebsd-x64@4.56.0": + version "4.56.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.56.0.tgz#9b1458d07b6e040be16ee36d308a2c9520f7f7cc" + integrity sha512-KNa6lYHloW+7lTEkYGa37fpvPq+NKG/EHKM8+G/g9WDU7ls4sMqbVRV78J6LdNuVaeeK5WB9/9VAFbKxcbXKYg== + +"@rollup/rollup-linux-arm-gnueabihf@4.56.0": + version "4.56.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.56.0.tgz#1d50ded7c965d5f125f5832c971ad5b287befef7" + integrity sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A== + +"@rollup/rollup-linux-arm-musleabihf@4.56.0": + version "4.56.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.56.0.tgz#53597e319b7e65990d3bc2a5048097384814c179" + integrity sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw== + +"@rollup/rollup-linux-arm64-gnu@4.56.0": + version "4.56.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.56.0.tgz#597002909dec198ca4bdccb25f043d32db3d6283" + integrity sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ== + +"@rollup/rollup-linux-arm64-musl@4.56.0": + version "4.56.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.56.0.tgz#286f0e0f799545ce288bdc5a7c777261fcba3d54" + integrity sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA== + +"@rollup/rollup-linux-loong64-gnu@4.56.0": + version "4.56.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.56.0.tgz#1fab07fa1a4f8d3697735b996517f1bae0ba101b" + integrity sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg== + +"@rollup/rollup-linux-loong64-musl@4.56.0": + version "4.56.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.56.0.tgz#efc2cb143d6c067f95205482afb177f78ed9ea3d" + integrity sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA== + +"@rollup/rollup-linux-ppc64-gnu@4.56.0": + version "4.56.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.56.0.tgz#e8de8bd3463f96b92b7dfb7f151fd80ffe8a937c" + integrity sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw== + +"@rollup/rollup-linux-ppc64-musl@4.56.0": + version "4.56.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.56.0.tgz#8c508fe28a239da83b3a9da75bcf093186e064b4" + integrity sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg== + +"@rollup/rollup-linux-riscv64-gnu@4.56.0": + version "4.56.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.56.0.tgz#ff6d51976e0830732880770a9e18553136b8d92b" + integrity sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew== + +"@rollup/rollup-linux-riscv64-musl@4.56.0": + version "4.56.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.56.0.tgz#325fb35eefc7e81d75478318f0deee1e4a111493" + integrity sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ== + +"@rollup/rollup-linux-s390x-gnu@4.56.0": + version "4.56.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.56.0.tgz#37410fabb5d3ba4ad34abcfbe9ba9b6288413f30" + integrity sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ== + +"@rollup/rollup-linux-x64-gnu@4.56.0": + version "4.56.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.56.0.tgz#8ef907a53b2042068fc03fcc6a641e2b02276eca" + integrity sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw== + +"@rollup/rollup-linux-x64-musl@4.56.0": + version "4.56.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.56.0.tgz#61b9ba09ea219e0174b3f35a6ad2afc94bdd5662" + integrity sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA== + +"@rollup/rollup-openbsd-x64@4.56.0": + version "4.56.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.56.0.tgz#fc4e54133134c1787d0b016ffdd5aeb22a5effd3" + integrity sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA== + +"@rollup/rollup-openharmony-arm64@4.56.0": + version "4.56.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.56.0.tgz#959ae225b1eeea0cc5b7c9f88e4834330fb6cd09" + integrity sha512-LhN/Reh+7F3RCgQIRbgw8ZMwUwyqJM+8pXNT6IIJAqm2IdKkzpCh/V9EdgOMBKuebIrzswqy4ATlrDgiOwbRcQ== + +"@rollup/rollup-win32-arm64-msvc@4.56.0": + version "4.56.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.56.0.tgz#842acd38869fa1cbdbc240c76c67a86f93444c27" + integrity sha512-kbFsOObXp3LBULg1d3JIUQMa9Kv4UitDmpS+k0tinPBz3watcUiV2/LUDMMucA6pZO3WGE27P7DsfaN54l9ing== + +"@rollup/rollup-win32-ia32-msvc@4.56.0": + version "4.56.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.56.0.tgz#7ab654def4042df44cb29f8ed9d5044e850c66d5" + integrity sha512-vSSgny54D6P4vf2izbtFm/TcWYedw7f8eBrOiGGecyHyQB9q4Kqentjaj8hToe+995nob/Wv48pDqL5a62EWtg== + +"@rollup/rollup-win32-x64-gnu@4.56.0": + version "4.56.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.56.0.tgz#7426cdec1b01d2382ffd5cda83cbdd1c8efb3ca6" + integrity sha512-FeCnkPCTHQJFbiGG49KjV5YGW/8b9rrXAM2Mz2kiIoktq2qsJxRD5giEMEOD2lPdgs72upzefaUvS+nc8E3UzQ== + +"@rollup/rollup-win32-x64-msvc@4.56.0": + version "4.56.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.56.0.tgz#9eec0212732a432c71bde0350bc40b673d15b2db" + integrity sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g== + "@socket.io/component-emitter@~3.1.0": version "3.1.2" resolved "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz" @@ -309,6 +554,56 @@ resolved "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.9.6.tgz" integrity sha512-gf5no6N9FCk1qMrti4lfwP77JHP5haASZgVbBgpZG7BUepB3fhiLCXGUK8LvuOjP36HivXewjg72LTnPDScnQQ== +"@tauri-apps/cli-darwin-x64@2.9.6": + version "2.9.6" + resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.9.6.tgz#7beb6ba8218002d7e160764326ce03407e76305d" + integrity sha512-oWh74WmqbERwwrwcueJyY6HYhgCksUc6NT7WKeXyrlY/FPmNgdyQAgcLuTSkhRFuQ6zh4Np1HZpOqCTpeZBDcw== + +"@tauri-apps/cli-linux-arm-gnueabihf@2.9.6": + version "2.9.6" + resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.9.6.tgz#523ddcd86a99bdda5156bd9de96dfd3e4fa75b7f" + integrity sha512-/zde3bFroFsNXOHN204DC2qUxAcAanUjVXXSdEGmhwMUZeAQalNj5cz2Qli2elsRjKN/hVbZOJj0gQ5zaYUjSg== + +"@tauri-apps/cli-linux-arm64-gnu@2.9.6": + version "2.9.6" + resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.9.6.tgz#60b4cbd4b8b97c5f5be6cc70fa75455b5a6d6292" + integrity sha512-pvbljdhp9VOo4RnID5ywSxgBs7qiylTPlK56cTk7InR3kYSTJKYMqv/4Q/4rGo/mG8cVppesKIeBMH42fw6wjg== + +"@tauri-apps/cli-linux-arm64-musl@2.9.6": + version "2.9.6" + resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.9.6.tgz#ce5e5396db6c7f22b80154757479b1486163364a" + integrity sha512-02TKUndpodXBCR0oP//6dZWGYcc22Upf2eP27NvC6z0DIqvkBBFziQUcvi2n6SrwTRL0yGgQjkm9K5NIn8s6jw== + +"@tauri-apps/cli-linux-riscv64-gnu@2.9.6": + version "2.9.6" + resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.9.6.tgz#aa8ec23d62cbb85a75c3e172637e78f485dbfcf8" + integrity sha512-fmp1hnulbqzl1GkXl4aTX9fV+ubHw2LqlLH1PE3BxZ11EQk+l/TmiEongjnxF0ie4kV8DQfDNJ1KGiIdWe1GvQ== + +"@tauri-apps/cli-linux-x64-gnu@2.9.6": + version "2.9.6" + resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.9.6.tgz#036c0463c5eee2298ed6ca8cb2838738816c7290" + integrity sha512-vY0le8ad2KaV1PJr+jCd8fUF9VOjwwQP/uBuTJvhvKTloEwxYA/kAjKK9OpIslGA9m/zcnSo74czI6bBrm2sYA== + +"@tauri-apps/cli-linux-x64-musl@2.9.6": + version "2.9.6" + resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.9.6.tgz#62b05db3d28b0f12c150836a387bd572de44f5be" + integrity sha512-TOEuB8YCFZTWVDzsO2yW0+zGcoMiPPwcUgdnW1ODnmgfwccpnihDRoks+ABT1e3fHb1ol8QQWsHSCovb3o2ENQ== + +"@tauri-apps/cli-win32-arm64-msvc@2.9.6": + version "2.9.6" + resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.9.6.tgz#01f69ba09a6581e70bdfa206c5801b64b329d28d" + integrity sha512-ujmDGMRc4qRLAnj8nNG26Rlz9klJ0I0jmZs2BPpmNNf0gM/rcVHhqbEkAaHPTBVIrtUdf7bGvQAD2pyIiUrBHQ== + +"@tauri-apps/cli-win32-ia32-msvc@2.9.6": + version "2.9.6" + resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.9.6.tgz#b36c60db5119d74f126d4c4d8288d1c6ae4b45f0" + integrity sha512-S4pT0yAJgFX8QRCyKA1iKjZ9Q/oPjCZf66A/VlG5Yw54Nnr88J1uBpmenINbXxzyhduWrIXBaUbEY1K80ZbpMg== + +"@tauri-apps/cli-win32-x64-msvc@2.9.6": + version "2.9.6" + resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.9.6.tgz#d58c9f8af835b7e4fc30e201e979342c70bea426" + integrity sha512-ldWuWSSkWbKOPjQMJoYVj9wLHcOniv7diyI5UAJ4XsBdtaFB0pKHQsqw/ItUma0VXGC7vB4E9fZjivmxur60aw== + "@tauri-apps/cli@^2": version "2.9.6" resolved "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.9.6.tgz" @@ -373,7 +668,7 @@ dependencies: "@babel/types" "^7.28.2" -"@types/estree@^1.0.0", "@types/estree@1.0.8": +"@types/estree@1.0.8", "@types/estree@^1.0.0": version "1.0.8" resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz" integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== @@ -383,7 +678,7 @@ resolved "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz" integrity sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA== -"@types/node@^20.19.0 || >=22.12.0", "@types/node@^25.0.10": +"@types/node@^25.0.10": version "25.0.10" resolved "https://registry.npmjs.org/@types/node/-/node-25.0.10.tgz" integrity sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg== @@ -412,7 +707,7 @@ "@types/history" "^4.7.11" "@types/react" "*" -"@types/react@*", "@types/react@^18.2.25 || ^19", "@types/react@^19.1.8", "@types/react@^19.2.0", "@types/react@>=18.0.0": +"@types/react@*", "@types/react@^19.1.8": version "19.2.9" resolved "https://registry.npmjs.org/@types/react/-/react-19.2.9.tgz" integrity sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA== @@ -526,17 +821,7 @@ binary-extensions@^2.0.0: resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz" integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== -bn.js@^4.0.0: - version "4.12.2" - resolved "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz" - integrity sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw== - -bn.js@^4.1.0: - version "4.12.2" - resolved "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz" - integrity sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw== - -bn.js@^4.11.9: +bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.9: version "4.12.2" resolved "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz" integrity sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw== @@ -627,7 +912,7 @@ browserify-zlib@^0.2.0: dependencies: pako "~1.0.5" -browserslist@^4.24.0, browserslist@^4.28.1, "browserslist@>= 4.21.0": +browserslist@^4.24.0, browserslist@^4.28.1: version "4.28.1" resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz" integrity sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA== @@ -825,7 +1110,7 @@ csstype@^3.2.2: resolved "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz" integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ== -d@^1.0.1, d@^1.0.2, d@1: +d@1, d@^1.0.1, d@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/d/-/d-1.0.2.tgz" integrity sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw== @@ -1198,7 +1483,7 @@ get-proto@^1.0.1: dunder-proto "^1.0.1" es-object-atoms "^1.0.0" -glob-parent@^5.1.2: +glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== @@ -1212,13 +1497,6 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" -glob-parent@~5.1.2: - version "5.1.2" - resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" - integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== - dependencies: - is-glob "^4.0.1" - gopd@^1.0.1, gopd@^1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz" @@ -1310,7 +1588,7 @@ ieee754@^1.1.13, ieee754@^1.2.1: resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== -immer@^11.0.0, immer@>=9.0.6: +immer@^11.0.0: version "11.1.3" resolved "https://registry.npmjs.org/immer/-/immer-11.1.3.tgz" integrity sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q== @@ -1548,16 +1826,16 @@ minimalistic-crypto-utils@^1.0.1: resolved "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz" integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== -ms@^2.1.3: - version "2.1.3" - resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - ms@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== +ms@^2.1.3: + version "2.1.3" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + mz@^2.7.0: version "2.7.0" resolved "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz" @@ -1744,22 +2022,12 @@ picocolors@^1.1.1: resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz" integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== -picomatch@^2.0.4: +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== -picomatch@^2.2.1: - version "2.3.1" - resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" - integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== - -picomatch@^2.3.1: - version "2.3.1" - resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" - integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== - -"picomatch@^3 || ^4", picomatch@^4.0.2, picomatch@^4.0.3: +picomatch@^4.0.2, picomatch@^4.0.3: version "4.0.3" resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz" integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q== @@ -1816,22 +2084,6 @@ postcss-nested@^6.2.0: dependencies: postcss-selector-parser "^6.1.1" -postcss-selector-parser@^6.1.1: - version "6.1.2" - resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz" - integrity sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg== - dependencies: - cssesc "^3.0.0" - util-deprecate "^1.0.2" - -postcss-selector-parser@^6.1.2: - version "6.1.2" - resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz" - integrity sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg== - dependencies: - cssesc "^3.0.0" - util-deprecate "^1.0.2" - postcss-selector-parser@6.0.10: version "6.0.10" resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz" @@ -1840,12 +2092,20 @@ postcss-selector-parser@6.0.10: cssesc "^3.0.0" util-deprecate "^1.0.2" +postcss-selector-parser@^6.1.1, postcss-selector-parser@^6.1.2: + version "6.1.2" + resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz" + integrity sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + postcss-value-parser@^4.0.0, postcss-value-parser@^4.2.0: version "4.2.0" resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -postcss@^8.0.0, postcss@^8.1.0, postcss@^8.2.14, postcss@^8.4.21, postcss@^8.4.47, postcss@^8.5.6, postcss@>=8.0.9: +postcss@^8.4.47, postcss@^8.5.6: version "8.5.6" resolved "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz" integrity sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg== @@ -1918,14 +2178,14 @@ randomfill@^1.0.4: randombytes "^2.0.5" safe-buffer "^5.1.0" -"react-dom@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", react-dom@^19.1.0, react-dom@>=18: +react-dom@^19.1.0: version "19.2.4" resolved "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz" integrity sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ== dependencies: scheduler "^0.27.0" -"react-redux@^7.2.1 || ^8.1.3 || ^9.0.0", react-redux@^9.2.0: +react-redux@^9.2.0: version "9.2.0" resolved "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz" integrity sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g== @@ -1953,7 +2213,7 @@ react-router@7.13.0: cookie "^1.0.1" set-cookie-parser "^2.6.0" -"react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.9.0 || ^17.0.0 || ^18 || ^19", "react@^18.0 || ^19", react@^19.1.0, react@^19.2.4, react@>=18, react@>=18.0.0: +react@^19.1.0: version "19.2.4" resolved "https://registry.npmjs.org/react/-/react-19.2.4.tgz" integrity sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ== @@ -2016,7 +2276,7 @@ redux-thunk@^3.1.0: resolved "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz" integrity sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw== -redux@^5.0.0, redux@^5.0.1, redux@>4.0.0: +redux@^5.0.0, redux@^5.0.1: version "5.0.1" resolved "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz" integrity sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w== @@ -2048,7 +2308,7 @@ ripemd160@^2.0.0, ripemd160@^2.0.1, ripemd160@^2.0.3: hash-base "^3.1.2" inherits "^2.0.4" -rollup@^1.20.0||^2.0.0||^3.0.0||^4.0.0, rollup@^4.43.0: +rollup@^4.43.0: version "4.56.0" resolved "https://registry.npmjs.org/rollup/-/rollup-4.56.0.tgz" integrity sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg== @@ -2094,12 +2354,7 @@ safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== -safe-buffer@~5.1.0: - version "5.1.2" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -safe-buffer@~5.1.1: +safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== @@ -2290,7 +2545,7 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== -tailwindcss@^3.4.19, "tailwindcss@>=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1", "tailwindcss@>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1": +tailwindcss@^3.4.19: version "3.4.19" resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz" integrity sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ== @@ -2453,12 +2708,12 @@ url@^0.11.4: punycode "^1.4.1" qs "^6.12.3" -use-sync-external-store@^1.4.0, use-sync-external-store@>=1.2.0: +use-sync-external-store@^1.4.0: version "1.6.0" resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz" integrity sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w== -utf-8-validate@^5.0.2, utf-8-validate@^5.0.5, utf-8-validate@>=5.0.2: +utf-8-validate@^5.0.2, utf-8-validate@^5.0.5: version "5.0.10" resolved "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz" integrity sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ== @@ -2489,7 +2744,7 @@ vite-plugin-node-polyfills@^0.25.0: "@rollup/plugin-inject" "^5.0.5" node-stdlib-browser "^1.3.1" -"vite@^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", "vite@^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", vite@^7.0.4: +vite@^7.0.4: version "7.3.1" resolved "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz" integrity sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==