Repository: OpenSecretCloud/Maple
Pull request #45: External web oauth deep link
Author: login: AnthonyRonning
Created: 2025-04-04T21:12:51Z | Merged: 2025-04-07T18:16:44Z

# OAuth Implementation for Desktop App: Analysis and New Approach

## Part 1: Current Approach and Issues

### Current Implementation

The current implementation tries to handle OAuth directly within the Tauri desktop app using the following approach:

1. Use `tauri-plugin-oauth` to start a local server that listens for OAuth callbacks
2. Modify the OAuth redirect URL to point to this local server (using `127.0.0.1:PORT`)
3. Handle the OAuth callback within the desktop app and process the tokens

### Why It Doesn't Work

This approach encounters several issues:

1. **OAuth Provider Restrictions**:
   - OAuth providers (GitHub/Google) restrict callback URLs to registered domains
   - Using `127.0.0.1` or `localhost` requires specific registration and configuration
   - Some providers may reject these URLs entirely for security reasons

2. **URL Encoding Issues**:
   - The redirect URL needs precise encoding when passed to OAuth providers
   - Small errors in encoding can cause providers to reject the URL

3. **Port Management**:
   - The local server uses a dynamic port, which needs to be properly passed
   - This introduces complexity and potential race conditions

4. **Token Persistence**:
   - The way tokens are being stored differs between desktop and web
   - This leads to tokens disappearing after app restart

5. **Consistent Authentication Experience**:
   - The flow differs between desktop and web, creating inconsistency
   - Makes future platform support (iOS/Android) more complex

## Part 2: Code to Remove

Based on the recent commits, the following code should be removed:

1. **OAuth Plugin Integration**:
   - Remove `tauri-plugin-oauth = "2"` from `src-tauri/Cargo.toml`
   - Remove `@fabianlars/tauri-plugin-oauth` from `package.json`

2. **Rust-side OAuth Server**:
   - Remove the `start_oauth_server` function from `src-tauri/src/lib.rs`
   - Remove the plugin registration: `.plugin(tauri_plugin_oauth::init())`
   - Remove the associated invoke handler for the OAuth server

3. **Frontend OAuth Handling**:
   - Remove all the custom URL manipulation code in `login.tsx` that:
     - Starts the OAuth server
     - Modifies redirect URLs
     - Listens for OAuth events
     - Handles the callback manually

## Part 3: New Implementation Details

### Step 1: Replace OAuth Plugin with Deep Link Plugin

1. **Cargo.toml Changes**:
   ```toml
   # Remove
   tauri-plugin-oauth = "2"
   
   # Add
   tauri-plugin-deep-link = "2"
   ```

2. **Package.json Changes**:
   ```json
   // Remove
   "@fabianlars/tauri-plugin-oauth": "^2.0.0",
   
   // Add
   "@tauri-apps/plugin-deep-link": "^2.0.0",
   ```

3. **Update Tauri Configuration**:
   In `tauri.conf.json`:
   ```json
   {
     "plugins": {
       "deep-link": {
         "domains": ["opensecret.cloud", "trymaple.ai"]
       }
     }
   }
   ```

### Step 2: Modify Rust Backend (src-tauri/src/lib.rs)

1. **Remove OAuth Server Code**:
   - Delete the `start_oauth_server` function
   - Remove any OAuth server specific code

2. **Add Deep Link Handler**:
   ```rust
   use tauri::{Emitter, Runtime};
   use tauri_plugin_deep_link::DeepLinkExt;
   
   // This handles incoming deep links
   fn handle_deep_link(app: &tauri::AppHandle, url: String) {
       log::info!("[Deep Link] Received: {}", url);
       // Forward the URL to the frontend
       match app.emit_all("deep-link-received", url) {
           Ok(_) => log::info!("[Deep Link] Event emitted successfully"),
           Err(e) => log::error!("[Deep Link] Failed to emit event: {}", e),
       }
   }
   
   #[cfg_attr(mobile, tauri::mobile_entry_point)]
   pub fn run() {
       #[cfg(desktop)]
       let app = tauri::Builder::default()
           // Other plugins...
           .plugin(tauri_plugin_deep_link::init())
           .setup(|app| {
               // Register deep link handler
               app.handle().deep_link_handler(|app, url| handle_deep_link(app, url));
               // Rest of setup...
               Ok(())
           })
           // Rest of builder...
   ```

### Step 3: Update Frontend Authentication (login.tsx)

1. **Modify Login Flow**:
   ```typescript
   const handleGitHubLogin = async () => {
     try {
       const isTauriEnv = await isTauri();
       console.log("[OAuth] Using", isTauriEnv ? "desktop" : "web", "flow");
       
       if (isTauriEnv) {
         // For desktop, redirect to the web app's desktop-auth route
         const desktopAuthUrl = "https://trymaple.ai/desktop-auth?provider=github";
         
         // If there's a selected plan, add it to the URL
         if (selected_plan) {
           desktopAuthUrl += `&selected_plan=${encodeURIComponent(selected_plan)}`;
         }
         
         // Open the URL in the default browser
         // This will start the web-based auth flow
         window.location.href = desktopAuthUrl;
       } else {
         // Web flow remains unchanged
         const { auth_url } = await os.initiateGitHubAuth("");
         if (selected_plan) {
           sessionStorage.setItem("selected_plan", selected_plan);
         }
         window.location.href = auth_url;
       }
     } catch (error) {
       console.error("Failed to initiate GitHub login:", error);
       setError("Failed to initiate GitHub login. Please try again.");
     }
   };
   ```

2. **Setup Deep Link Listener**:
   In a component that runs at app startup:
   ```typescript
   import { listen } from "@tauri-apps/api/event";
   import { isTauri } from "@tauri-apps/api/core";
   import { register } from "@tauri-apps/plugin-deep-link";
   
   async function setupDeepLinkHandling() {
     if (await isTauri()) {
       // Register the app to handle deep links
       await register();
       
       // Listen for deep link events from the Rust backend
       await listen<string>("deep-link-received", (event) => {
         const url = event.payload;
         console.log("[Deep Link] Received URL:", url);
         
         try {
           // Parse the URL to extract the tokens
           const urlObj = new URL(url);
           // The URL will look like: cloud.opensecret.maple://auth?token=...&refresh=...
           const token = urlObj.searchParams.get("token");
           const refreshToken = urlObj.searchParams.get("refresh");
           
           if (token && refreshToken) {
             console.log("[Deep Link] Auth tokens received");
             
             // Store the tokens in localStorage
             localStorage.setItem("auth_token", token);
             localStorage.setItem("refresh_token", refreshToken);
             
             // Refresh the app state to reflect the logged-in status
             window.location.href = "/"; // Reload the app
           } else {
             console.error("[Deep Link] Missing tokens in deep link");
           }
         } catch (error) {
           console.error("[Deep Link] Failed to process deep link:", error);
         }
       });
       
       console.log("[Deep Link] Handler setup complete");
     }
   }
   
   // Call this in your app initialization
   setupDeepLinkHandling();
   ```

### Step 4: Create Desktop Auth Page on Web (desktop-auth.tsx)

This is a new route that needs to be added to your web application:

```typescript
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useEffect } from "react";
import { useOpenSecret } from "@opensecret/react";

export const Route = createFileRoute("/desktop-auth")({
  component: DesktopAuth,
  validateSearch: (search: Record<string, unknown>) => ({
    provider: typeof search.provider === "string" ? search.provider : "github",
    selected_plan: typeof search.selected_plan === "string" ? search.selected_plan : undefined
  })
});

function DesktopAuth() {
  const { provider, selected_plan } = Route.useSearch();
  const navigate = useNavigate();
  const os = useOpenSecret();
  
  useEffect(() => {
    const initiateAuth = async () => {
      try {
        // Store the flag to indicate this is a desktop auth flow
        localStorage.setItem("redirect-to-native", "true");
        
        // Store selected plan if present
        if (selected_plan) {
          sessionStorage.setItem("selected_plan", selected_plan);
        }
        
        // Initiate appropriate OAuth flow
        let auth_url;
        if (provider === "github") {
          const result = await os.initiateGitHubAuth("");
          auth_url = result.auth_url;
        } else if (provider === "google") {
          const result = await os.initiateGoogleAuth("");
          auth_url = result.auth_url;
        } else {
          throw new Error("Unsupported provider");
        }
        
        // Redirect to the OAuth provider
        window.location.href = auth_url;
      } catch (error) {
        console.error(`Failed to initiate ${provider} login:`, error);
        // Redirect to login page on error
        navigate({ to: "/login" });
      }
    };
    
    initiateAuth();
  }, [os, provider, selected_plan, navigate]);
  
  return (
    <div className="flex items-center justify-center min-h-screen">
      <div className="text-center">
        <h1 className="text-2xl font-bold mb-4">Redirecting to {provider}</h1>
        <p>Please wait while we redirect you to complete authentication...</p>
      </div>
    </div>
  );
}
```

### Step 5: Modify OAuth Callback Handler (auth.$provider.callback.tsx)

Update your existing callback handler to support the desktop flow:

```typescript
function OAuthCallback() {
  // ... existing code ...
  
  useEffect(() => {
    const processCallback = async () => {
      if (processedRef.current) return;
      processedRef.current = true;

      const urlParams = new URLSearchParams(window.location.search);
      const code = urlParams.get("code");
      const state = urlParams.get("state");

      if (code && state) {
        try {
          let response;
          if (provider === "github") {
            response = await handleGitHubCallback(code, state, "");
          } else if (provider === "google") {
            response = await handleGoogleCallback(code, state, "");
          } else {
            throw new Error("Unsupported provider");
          }

          // Check if this is a desktop auth flow
          const isDesktopAuth = localStorage.getItem("redirect-to-native") === "true";
          
          // Clear the flag
          localStorage.removeItem("redirect-to-native");
          
          if (isDesktopAuth) {
            // This is a desktop auth flow - redirect to the desktop app with tokens
            const token = response.access_token; // Or however the token is named in the response
            const refreshToken = response.refresh_token; // If available
            
            // Construct the deep link URL
            const deepLinkUrl = `cloud.opensecret.maple://auth?token=${encodeURIComponent(token)}`;
            
            if (refreshToken) {
              deepLinkUrl += `&refresh=${encodeURIComponent(refreshToken)}`;
            }
            
            // Redirect to the deep link
            setTimeout(() => {
              window.location.href = deepLinkUrl;
            }, 1000);
            
            return; // Stop further processing
          }
          
          // Regular web flow - unchanged
          const selectedPlan = sessionStorage.getItem("selected_plan");
          sessionStorage.removeItem("selected_plan");

          setTimeout(() => {
            if (selectedPlan) {
              navigate({
                to: "/pricing",
                search: { selected_plan: selectedPlan }
              });
            } else {
              navigate({ to: "/" });
            }
          }, 2000);
        } catch (error) {
          // ... error handling ...
        } finally {
          setIsProcessing(false);
        }
      } else {
        // ... error handling ...
      }
    };

    processCallback();
  }, [handleGitHubCallback, handleGoogleCallback, navigate, provider]);
  
  // If this is a desktop auth flow, show a different UI
  if (localStorage.getItem("redirect-to-native") === "true") {
    return (
      <Card className="max-w-md mx-auto mt-20">
        <CardHeader>
          <CardTitle>{formattedProvider} Authentication Successful</CardTitle>
        </CardHeader>
        <CardContent>
          <p className="mb-4">Authentication successful! Redirecting you back to the desktop app...</p>
          <div className="flex justify-center">
            <Loader2 className="h-8 w-8 animate-spin" />
          </div>
        </CardContent>
      </Card>
    );
  }
  
  // Regular UI for web flow - unchanged
  // ...
}
```

## Summary of the Implementation

This new approach:

1. Leverages the existing web OAuth flow that works reliably
2. Uses deep links to transfer tokens back to the desktop app
3. Provides a consistent auth experience across platforms
4. Simplifies the desktop app's responsibilities
5. Ensures proper token persistence
6. Sets the foundation for future mobile app integration

The desktop app's role is simplified to:
- Redirecting to the web app for authentication
- Listening for deep links with tokens
- Storing tokens securely
- Refreshing the UI to reflect logged-in state

The web app's role is enhanced to:
- Detect desktop auth flows via a flag in localStorage
- Complete the OAuth flow as usual
- Redirect back to the desktop app with tokens via deep links

This approach effectively separates concerns and uses each platform for what it does best, creating a more reliable and maintainable authentication system.


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
	- Introduced deep linking support for desktop authentication, enabling seamless redirection and token handling.
	- Added a dedicated authentication route that directly triggers external login flows for desktop users.
	- Implemented a new `DeepLinkHandler` component to manage deep link events and token storage.
- **Refactor**
	- Streamlined the authentication process by replacing the legacy OAuth-based flow with a direct, deep link-driven approach for improved usability.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/frontend/bun.lock b/frontend/bun.lock
index 6d9401c3..c636f19a 100644
--- a/frontend/bun.lock
+++ b/frontend/bun.lock
@@ -4,7 +4,6 @@
     "": {
       "name": "maple",
       "dependencies": {
-        "@fabianlars/tauri-plugin-oauth": "^2.0.0",
         "@opensecret/react": "0.4.0",
         "@radix-ui/react-alert-dialog": "^1.1.1",
         "@radix-ui/react-avatar": "^1.1.0",
@@ -19,6 +18,8 @@
         "@tanstack/react-query": "^5.56.2",
         "@tanstack/react-router": "^1.50.1",
         "@tauri-apps/api": "^2.0.0",
+        "@tauri-apps/plugin-deep-link": "^2.0.0",
+        "@tauri-apps/plugin-opener": "^2.2.6",
         "class-variance-authority": "^0.7.0",
         "clsx": "^2.1.1",
         "lucide-react": "^0.436.0",
@@ -169,8 +170,6 @@
 
     "@eslint/plugin-kit": ["@eslint/plugin-kit@0.2.5", "", { "dependencies": { "@eslint/core": "^0.10.0", "levn": "^0.4.1" } }, "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A=="],
 
-    "@fabianlars/tauri-plugin-oauth": ["@fabianlars/tauri-plugin-oauth@2.0.0", "", { "dependencies": { "@tauri-apps/api": "^2.0.3" } }, "sha512-I1s08ZXrsFuYfNWusAcpLyiCfr5TCvaBrRuKfTG+XQrcaqnAcwjdWH0U5J9QWuMDLwCUMnVxdobtMJzPR8raxQ=="],
-
     "@floating-ui/core": ["@floating-ui/core@1.6.9", "", { "dependencies": { "@floating-ui/utils": "^0.2.9" } }, "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw=="],
 
     "@floating-ui/dom": ["@floating-ui/dom@1.6.13", "", { "dependencies": { "@floating-ui/core": "^1.6.0", "@floating-ui/utils": "^0.2.9" } }, "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w=="],
@@ -413,6 +412,10 @@
 
     "@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.2.7", "", { "os": "win32", "cpu": "x64" }, "sha512-ZB8Kw90j8Ld+9tCWyD2fWCYfIrzbQohJ4DJSidNwbnehlZzP7wAz6Z3xjsvUdKtQ3ibtfoeTqVInzCCEpI+pWg=="],
 
+    "@tauri-apps/plugin-deep-link": ["@tauri-apps/plugin-deep-link@2.2.1", "", { "dependencies": { "@tauri-apps/api": "^2.0.0" } }, "sha512-8skZ6qIH/kWaV8d6jj3aPvvkIOuqkVk0APRDey9n9N3Ueu3n4MIbuxpAKR2EdoAyQxnXxPTNVyjw2D35/vfGyg=="],
+
+    "@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.2.6", "", { "dependencies": { "@tauri-apps/api": "^2.0.0" } }, "sha512-bSdkuP71ZQRepPOn8BOEdBKYJQvl6+jb160QtJX/i2H9BF6ZySY/kYljh76N2Ne5fJMQRge7rlKoStYQY5Jq1w=="],
+
     "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
 
     "@types/babel__generator": ["@types/babel__generator@7.6.8", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw=="],
diff --git a/frontend/package.json b/frontend/package.json
index 7d03a09f..a967e6a3 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -13,8 +13,6 @@
     "format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,json,css,md}\""
   },
   "dependencies": {
-    "@fabianlars/tauri-plugin-oauth": "^2.0.0",
-    "@tauri-apps/api": "^2.0.0",
     "@opensecret/react": "0.4.0",
     "@radix-ui/react-alert-dialog": "^1.1.1",
     "@radix-ui/react-avatar": "^1.1.0",
@@ -28,6 +26,9 @@
     "@radix-ui/react-tooltip": "^1.1.2",
     "@tanstack/react-query": "^5.56.2",
     "@tanstack/react-router": "^1.50.1",
+    "@tauri-apps/api": "^2.0.0",
+    "@tauri-apps/plugin-deep-link": "^2.0.0",
+    "@tauri-apps/plugin-opener": "^2.2.6",
     "class-variance-authority": "^0.7.0",
     "clsx": "^2.1.1",
     "lucide-react": "^0.436.0",
diff --git a/frontend/src-tauri/Cargo.lock b/frontend/src-tauri/Cargo.lock
index 0e68cff7..0b3ae7d9 100644
--- a/frontend/src-tauri/Cargo.lock
+++ b/frontend/src-tauri/Cargo.lock
@@ -135,6 +135,91 @@ dependencies = [
  "pin-project-lite",
 ]
 
+[[package]]
+name = "async-channel"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a"
+dependencies = [
+ "concurrent-queue",
+ "event-listener-strategy",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-executor"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec"
+dependencies = [
+ "async-task",
+ "concurrent-queue",
+ "fastrand",
+ "futures-lite",
+ "slab",
+]
+
+[[package]]
+name = "async-fs"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a"
+dependencies = [
+ "async-lock",
+ "blocking",
+ "futures-lite",
+]
+
+[[package]]
+name = "async-io"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059"
+dependencies = [
+ "async-lock",
+ "cfg-if",
+ "concurrent-queue",
+ "futures-io",
+ "futures-lite",
+ "parking",
+ "polling",
+ "rustix 0.38.44",
+ "slab",
+ "tracing",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "async-lock"
+version = "3.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18"
+dependencies = [
+ "event-listener",
+ "event-listener-strategy",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-process"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb"
+dependencies = [
+ "async-channel",
+ "async-io",
+ "async-lock",
+ "async-signal",
+ "async-task",
+ "blocking",
+ "cfg-if",
+ "event-listener",
+ "futures-lite",
+ "rustix 0.38.44",
+ "tracing",
+]
+
 [[package]]
 name = "async-recursion"
 version = "1.1.1"
@@ -146,6 +231,30 @@ dependencies = [
  "syn 2.0.98",
 ]
 
+[[package]]
+name = "async-signal"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3"
+dependencies = [
+ "async-io",
+ "async-lock",
+ "atomic-waker",
+ "cfg-if",
+ "futures-core",
+ "futures-io",
+ "rustix 0.38.44",
+ "signal-hook-registry",
+ "slab",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "async-task"
+version = "4.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
+
 [[package]]
 name = "async-trait"
 version = "0.1.88"
@@ -180,6 +289,12 @@ dependencies = [
  "system-deps",
 ]
 
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
 [[package]]
 name = "autocfg"
 version = "1.4.0"
@@ -267,6 +382,19 @@ dependencies = [
  "objc2 0.6.0",
 ]
 
+[[package]]
+name = "blocking"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea"
+dependencies = [
+ "async-channel",
+ "async-task",
+ "futures-io",
+ "futures-lite",
+ "piper",
+]
+
 [[package]]
 name = "borsh"
 version = "1.5.5"
@@ -518,6 +646,26 @@ dependencies = [
  "crossbeam-utils",
 ]
 
+[[package]]
+name = "const-random"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
+dependencies = [
+ "const-random-macro",
+]
+
+[[package]]
+name = "const-random-macro"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
+dependencies = [
+ "getrandom 0.2.15",
+ "once_cell",
+ "tiny-keccak",
+]
+
 [[package]]
 name = "convert_case"
 version = "0.4.0"
@@ -607,6 +755,12 @@ version = "0.8.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
 
+[[package]]
+name = "crunchy"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
+
 [[package]]
 name = "crypto-common"
 version = "0.1.6"
@@ -806,6 +960,15 @@ dependencies = [
  "syn 2.0.98",
 ]
 
+[[package]]
+name = "dlv-list"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f"
+dependencies = [
+ "const-random",
+]
+
 [[package]]
 name = "dpi"
 version = "0.1.1"
@@ -1462,6 +1625,12 @@ dependencies = [
  "ahash",
 ]
 
+[[package]]
+name = "hashbrown"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+
 [[package]]
 name = "hashbrown"
 version = "0.15.2"
@@ -1480,6 +1649,12 @@ version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
 
+[[package]]
+name = "hermit-abi"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc"
+
 [[package]]
 name = "hex"
 version = "0.4.3"
@@ -1811,6 +1986,25 @@ version = "2.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
 
+[[package]]
+name = "is-docker"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "is-wsl"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
+dependencies = [
+ "is-docker",
+ "once_cell",
+]
+
 [[package]]
 name = "itoa"
 version = "0.4.8"
@@ -1981,6 +2175,12 @@ dependencies = [
  "redox_syscall",
 ]
 
+[[package]]
+name = "linux-raw-sys"
+version = "0.4.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
+
 [[package]]
 name = "linux-raw-sys"
 version = "0.9.3"
@@ -2028,9 +2228,10 @@ dependencies = [
  "tauri",
  "tauri-build",
  "tauri-plugin",
+ "tauri-plugin-deep-link",
  "tauri-plugin-dialog",
  "tauri-plugin-log",
- "tauri-plugin-oauth",
+ "tauri-plugin-opener",
  "tauri-plugin-updater",
  "tokio",
 ]
@@ -2463,12 +2664,34 @@ version = "1.20.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e"
 
+[[package]]
+name = "open"
+version = "5.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2483562e62ea94312f3576a7aca397306df7990b8d89033e18766744377ef95"
+dependencies = [
+ "dunce",
+ "is-wsl",
+ "libc",
+ "pathdiff",
+]
+
 [[package]]
 name = "option-ext"
 version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
 
+[[package]]
+name = "ordered-multimap"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79"
+dependencies = [
+ "dlv-list",
+ "hashbrown 0.14.5",
+]
+
 [[package]]
 name = "ordered-stream"
 version = "0.2.0"
@@ -2547,6 +2770,12 @@ dependencies = [
  "windows-targets 0.52.6",
 ]
 
+[[package]]
+name = "pathdiff"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
+
 [[package]]
 name = "percent-encoding"
 version = "2.3.1"
@@ -2699,6 +2928,17 @@ version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
 
+[[package]]
+name = "piper"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066"
+dependencies = [
+ "atomic-waker",
+ "fastrand",
+ "futures-io",
+]
+
 [[package]]
 name = "pkg-config"
 version = "0.3.31"
@@ -2731,6 +2971,21 @@ dependencies = [
  "miniz_oxide",
 ]
 
+[[package]]
+name = "polling"
+version = "3.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f"
+dependencies = [
+ "cfg-if",
+ "concurrent-queue",
+ "hermit-abi",
+ "pin-project-lite",
+ "rustix 0.38.44",
+ "tracing",
+ "windows-sys 0.59.0",
+]
+
 [[package]]
 name = "powerfmt"
 version = "0.2.0"
@@ -3132,7 +3387,7 @@ dependencies = [
  "wasm-streams",
  "web-sys",
  "webpki-roots",
- "windows-registry",
+ "windows-registry 0.2.0",
 ]
 
 [[package]]
@@ -3203,6 +3458,17 @@ dependencies = [
  "syn 1.0.109",
 ]
 
+[[package]]
+name = "rust-ini"
+version = "0.21.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e310ef0e1b6eeb79169a1171daf9abcb87a2e17c03bee2c4bb100b55c75409f"
+dependencies = [
+ "cfg-if",
+ "ordered-multimap",
+ "trim-in-place",
+]
+
 [[package]]
 name = "rust_decimal"
 version = "1.36.0"
@@ -3240,6 +3506,19 @@ dependencies = [
  "semver",
 ]
 
+[[package]]
+name = "rustix"
+version = "0.38.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
+dependencies = [
+ "bitflags 2.8.0",
+ "errno",
+ "libc",
+ "linux-raw-sys 0.4.15",
+ "windows-sys 0.59.0",
+]
+
 [[package]]
 name = "rustix"
 version = "1.0.3"
@@ -3249,7 +3528,7 @@ dependencies = [
  "bitflags 2.8.0",
  "errno",
  "libc",
- "linux-raw-sys",
+ "linux-raw-sys 0.9.3",
  "windows-sys 0.59.0",
 ]
 
@@ -3974,6 +4253,26 @@ dependencies = [
  "walkdir",
 ]
 
+[[package]]
+name = "tauri-plugin-deep-link"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dba4412f30eaff6f5d210e20383c2d6835593977402092e95b72497a4f8632fa"
+dependencies = [
+ "dunce",
+ "rust-ini",
+ "serde",
+ "serde_json",
+ "tauri",
+ "tauri-plugin",
+ "tauri-utils",
+ "thiserror 2.0.11",
+ "tracing",
+ "url",
+ "windows-registry 0.5.1",
+ "windows-result 0.3.2",
+]
+
 [[package]]
 name = "tauri-plugin-dialog"
 version = "2.2.1"
@@ -4038,18 +4337,25 @@ dependencies = [
 ]
 
 [[package]]
-name = "tauri-plugin-oauth"
-version = "2.0.0"
+name = "tauri-plugin-opener"
+version = "2.2.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eda564acdb23185caf700f89dd6e5d4540225d6a991516b2cad0cbcf27e4dcd3"
+checksum = "2fdc6cb608e04b7d2b6d1f21e9444ad49245f6d03465ba53323d692d1ceb1a30"
 dependencies = [
- "httparse",
- "log",
+ "dunce",
+ "glob",
+ "objc2-app-kit",
+ "objc2-foundation 0.3.0",
+ "open",
+ "schemars",
  "serde",
+ "serde_json",
  "tauri",
  "tauri-plugin",
- "thiserror 1.0.69",
+ "thiserror 2.0.11",
  "url",
+ "windows",
+ "zbus",
 ]
 
 [[package]]
@@ -4188,7 +4494,7 @@ dependencies = [
  "fastrand",
  "getrandom 0.3.1",
  "once_cell",
- "rustix",
+ "rustix 1.0.3",
  "windows-sys 0.59.0",
 ]
 
@@ -4282,6 +4588,15 @@ dependencies = [
  "time-core",
 ]
 
+[[package]]
+name = "tiny-keccak"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
+dependencies = [
+ "crunchy",
+]
+
 [[package]]
 name = "tinystr"
 version = "0.7.6"
@@ -4483,6 +4798,12 @@ dependencies = [
  "windows-sys 0.59.0",
 ]
 
+[[package]]
+name = "trim-in-place"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc"
+
 [[package]]
 name = "try-lock"
 version = "0.2.5"
@@ -5049,6 +5370,17 @@ dependencies = [
  "windows-targets 0.52.6",
 ]
 
+[[package]]
+name = "windows-registry"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad1da3e436dc7653dfdf3da67332e22bff09bb0e28b0239e1624499c7830842e"
+dependencies = [
+ "windows-link",
+ "windows-result 0.3.2",
+ "windows-strings 0.4.0",
+]
+
 [[package]]
 name = "windows-result"
 version = "0.2.0"
@@ -5086,6 +5418,15 @@ dependencies = [
  "windows-link",
 ]
 
+[[package]]
+name = "windows-strings"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97"
+dependencies = [
+ "windows-link",
+]
+
 [[package]]
 name = "windows-sys"
 version = "0.45.0"
@@ -5503,7 +5844,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0d65cbf2f12c15564212d48f4e3dfb87923d25d611f2aed18f4cb23f0413d89e"
 dependencies = [
  "libc",
- "rustix",
+ "rustix 1.0.3",
 ]
 
 [[package]]
@@ -5547,8 +5888,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "59c333f648ea1b647bc95dc1d34807c8e25ed7a6feff3394034dc4776054b236"
 dependencies = [
  "async-broadcast",
+ "async-executor",
+ "async-fs",
+ "async-io",
+ "async-lock",
+ "async-process",
  "async-recursion",
+ "async-task",
  "async-trait",
+ "blocking",
  "enumflags2",
  "event-listener",
  "futures-core",
diff --git a/frontend/src-tauri/Cargo.toml b/frontend/src-tauri/Cargo.toml
index e7d49650..024af752 100644
--- a/frontend/src-tauri/Cargo.toml
+++ b/frontend/src-tauri/Cargo.toml
@@ -26,5 +26,6 @@ tauri-plugin-log = "2.3.1"
 tauri-plugin-updater = "2.7.0"
 tauri-plugin-dialog = "2.2.1"
 tauri-plugin = "2.1.1"
-tauri-plugin-oauth = "2"
+tauri-plugin-deep-link = "2"
+tauri-plugin-opener = "2"
 tokio = { version = "1.0", features = ["time"] }
diff --git a/frontend/src-tauri/capabilities/default.json b/frontend/src-tauri/capabilities/default.json
index 4c533d0b..12db43e6 100644
--- a/frontend/src-tauri/capabilities/default.json
+++ b/frontend/src-tauri/capabilities/default.json
@@ -7,6 +7,17 @@
   ],
   "permissions": [
     "core:default",
-    "updater:default"
+    "updater:default",
+    {
+      "identifier": "opener:allow-open-url",
+      "allow": [
+        {
+          "url": "http://localhost:5173/*"
+        },
+        {
+          "url": "https://trymaple.ai/*"
+        }
+      ]
+    }
   ]
 }
diff --git a/frontend/src-tauri/src/lib.rs b/frontend/src-tauri/src/lib.rs
index 125bfb6a..d2f4ba2e 100644
--- a/frontend/src-tauri/src/lib.rs
+++ b/frontend/src-tauri/src/lib.rs
@@ -1,23 +1,15 @@
-use tauri::{Emitter, Runtime};
-
-#[tauri::command]
-async fn start_oauth_server<R: Runtime>(window: tauri::Window<R>) -> Result<u16, String> {
-    log::info!("[OAuth] Starting OAuth server...");
-    let port = tauri_plugin_oauth::start(move |url| {
-        log::info!("[OAuth] Received redirect URL: {}", url);
-        // Emit the redirect URL to the frontend
-        match window.emit("oauth_redirect", url) {
-            Ok(_) => log::info!("[OAuth] Successfully emitted oauth_redirect event"),
-            Err(e) => log::error!("[OAuth] Failed to emit oauth_redirect event: {}", e),
-        }
-    })
-    .map_err(|err| {
-        log::error!("[OAuth] Failed to start OAuth server: {}", err);
-        err.to_string()
-    })?;
-
-    log::info!("[OAuth] OAuth server started on port: {}", port);
-    Ok(port)
+use tauri::Emitter;
+use tauri_plugin_deep_link::DeepLinkExt;
+use tauri_plugin_opener;
+
+// This handles incoming deep links
+fn handle_deep_link_event(url: &str, app: &tauri::AppHandle) {
+    log::info!("[Deep Link] Received: {}", url);
+    // Forward the URL to the frontend
+    match app.emit_to("main", "deep-link-received", url.to_string()) {
+        Ok(_) => log::info!("[Deep Link] Event emitted successfully"),
+        Err(e) => log::error!("[Deep Link] Failed to emit event: {}", e),
+    }
 }
 
 #[cfg_attr(mobile, tauri::mobile_entry_point)]
@@ -26,9 +18,24 @@ pub fn run() {
     let app = tauri::Builder::default()
         .plugin(tauri_plugin_log::Builder::default().level(log::LevelFilter::Info).build())
         .plugin(tauri_plugin_dialog::init())
-        .plugin(tauri_plugin_oauth::init())
-        .invoke_handler(tauri::generate_handler![start_oauth_server])
+        .plugin(tauri_plugin_deep_link::init())
+        .plugin(tauri_plugin_opener::init())
         .setup(|app| {
+            // Set up the deep link handler
+            // Use a cloned handle with 'static lifetime
+            let app_handle = app.handle().clone();
+            // Register the deep link handler
+            app.deep_link().on_open_url(move |event| {
+                if let Some(url) = event.urls().first() {
+                    // Use the cloned app_handle
+                    handle_deep_link_event(&url.to_string(), &app_handle);
+                }
+            });
+            // Optionally register the scheme at runtime
+            #[cfg(desktop)]
+            if let Err(e) = app.deep_link().register("cloud.opensecret.maple") {
+                log::error!("[Deep Link] Failed to register scheme: {}", e);
+            }
             // Create the application menu with update options
             #[cfg(desktop)]
             {
diff --git a/frontend/src-tauri/tauri.conf.json b/frontend/src-tauri/tauri.conf.json
index be77091e..dc5cda87 100644
--- a/frontend/src-tauri/tauri.conf.json
+++ b/frontend/src-tauri/tauri.conf.json
@@ -17,6 +17,11 @@
       ],
       "dialog": true,
       "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDRCRjU0OEJCM0M2OTNEQjAKUldTd1BXazh1MGoxUyt3TXdHREU1WUdzL1VNYlludXhORHpQSUc1cGxSSW1kdEYrOXNPNzRjdUUK"
+    },
+    "deep-link": {
+      "desktop": {
+        "schemes": ["cloud.opensecret.maple"]
+      }
     }
   },
   "app": {
diff --git a/frontend/src/app.tsx b/frontend/src/app.tsx
index 022ca97d..7bdd1266 100644
--- a/frontend/src/app.tsx
+++ b/frontend/src/app.tsx
@@ -10,6 +10,7 @@ import { LocalStateProvider } from "./state/LocalStateContext";
 import { ErrorFallback } from "./components/ErrorFallback";
 import { NotFoundFallback } from "./components/NotFoundFallback";
 import { BillingServiceProvider } from "./components/BillingServiceProvider";
+import { DeepLinkHandler } from "./components/DeepLinkHandler";
 
 // Create a new router instance
 const router = createRouter({
@@ -88,6 +89,7 @@ export default function App() {
           <QueryClientProvider client={queryClient}>
             <TooltipProvider>
               <BillingServiceProvider>
+                <DeepLinkHandler />
                 <InnerApp />
               </BillingServiceProvider>
             </TooltipProvider>
diff --git a/frontend/src/components/DeepLinkHandler.tsx b/frontend/src/components/DeepLinkHandler.tsx
new file mode 100644
index 00000000..05ba3e8f
--- /dev/null
+++ b/frontend/src/components/DeepLinkHandler.tsx
@@ -0,0 +1,61 @@
+import { useEffect } from "react";
+import { isTauri } from "@tauri-apps/api/core";
+import { listen } from "@tauri-apps/api/event";
+
+// For direct deep link handling, we'll listen to our custom event
+// If we had the types installed, we would use:
+// import { onOpenUrl } from '@tauri-apps/plugin-deep-link';
+
+export function DeepLinkHandler() {
+  useEffect(() => {
+    let unlisten: (() => void) | undefined;
+
+    const setupDeepLinkHandling = async () => {
+      try {
+        if (await isTauri()) {
+          // Listen for the custom event we emit from Rust
+          unlisten = await listen<string>("deep-link-received", (event) => {
+            const url = event.payload;
+            console.log("[Deep Link] Received URL:", url);
+
+            try {
+              // Parse the URL to extract the tokens
+              const urlObj = new URL(url);
+              // The URL will look like: cloud.opensecret.maple://auth?access_token=...&refresh_token=...
+              const accessToken = urlObj.searchParams.get("access_token");
+              const refreshToken = urlObj.searchParams.get("refresh_token");
+
+              if (accessToken && refreshToken) {
+                console.log("[Deep Link] Auth tokens received");
+
+                // Store the tokens in localStorage with consistent naming
+                localStorage.setItem("access_token", accessToken);
+                localStorage.setItem("refresh_token", refreshToken);
+
+                // Refresh the app state to reflect the logged-in status
+                window.location.href = "/"; // Reload the app
+              } else {
+                console.error("[Deep Link] Missing tokens in deep link");
+              }
+            } catch (error) {
+              console.error("[Deep Link] Failed to process deep link:", error);
+            }
+          });
+
+          console.log("[Deep Link] Handler setup complete");
+        }
+      } catch (error) {
+        console.error("[Deep Link] Setup failed:", error);
+      }
+    };
+
+    setupDeepLinkHandling();
+
+    // Return cleanup function
+    return () => {
+      if (unlisten) unlisten();
+    };
+  }, []);
+
+  return null; // This component doesn't render anything
+}
diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts
index 864a76d8..d834db2b 100644
--- a/frontend/src/routeTree.gen.ts
+++ b/frontend/src/routeTree.gen.ts
@@ -16,6 +16,7 @@ import { Route as ProofImport } from './routes/proof'
 import { Route as PricingImport } from './routes/pricing'
 import { Route as PasswordResetImport } from './routes/password-reset'
 import { Route as LoginImport } from './routes/login'
+import { Route as DesktopAuthImport } from './routes/desktop-auth'
 import { Route as AuthImport } from './routes/_auth'
 import { Route as IndexImport } from './routes/index'
 import { Route as VerifyCodeImport } from './routes/verify.$code'
@@ -55,6 +56,12 @@ const LoginRoute = LoginImport.update({
   getParentRoute: () => rootRoute,
 } as any)
 
+const DesktopAuthRoute = DesktopAuthImport.update({
+  id: '/desktop-auth',
+  path: '/desktop-auth',
+  getParentRoute: () => rootRoute,
+} as any)
+
 const AuthRoute = AuthImport.update({
   id: '/_auth',
   getParentRoute: () => rootRoute,
@@ -108,6 +115,13 @@ declare module '@tanstack/react-router' {
       preLoaderRoute: typeof AuthImport
       parentRoute: typeof rootRoute
     }
+    '/desktop-auth': {
+      id: '/desktop-auth'
+      path: '/desktop-auth'
+      fullPath: '/desktop-auth'
+      preLoaderRoute: typeof DesktopAuthImport
+      parentRoute: typeof rootRoute
+    }
     '/login': {
       id: '/login'
       path: '/login'
@@ -201,6 +215,7 @@ const PasswordResetRouteWithChildren = PasswordResetRoute._addFileChildren(
 export interface FileRoutesByFullPath {
   '/': typeof IndexRoute
   '': typeof AuthRouteWithChildren
+  '/desktop-auth': typeof DesktopAuthRoute
   '/login': typeof LoginRoute
   '/password-reset': typeof PasswordResetRouteWithChildren
   '/pricing': typeof PricingRoute
@@ -215,6 +230,7 @@ export interface FileRoutesByFullPath {
 export interface FileRoutesByTo {
   '/': typeof IndexRoute
   '': typeof AuthRouteWithChildren
+  '/desktop-auth': typeof DesktopAuthRoute
   '/login': typeof LoginRoute
   '/password-reset': typeof PasswordResetRouteWithChildren
   '/pricing': typeof PricingRoute
@@ -230,6 +246,7 @@ export interface FileRoutesById {
   __root__: typeof rootRoute
   '/': typeof IndexRoute
   '/_auth': typeof AuthRouteWithChildren
+  '/desktop-auth': typeof DesktopAuthRoute
   '/login': typeof LoginRoute
   '/password-reset': typeof PasswordResetRouteWithChildren
   '/pricing': typeof PricingRoute
@@ -246,6 +263,7 @@ export interface FileRouteTypes {
   fullPaths:
     | '/'
     | ''
+    | '/desktop-auth'
     | '/login'
     | '/password-reset'
     | '/pricing'
@@ -259,6 +277,7 @@ export interface FileRouteTypes {
   to:
     | '/'
     | ''
+    | '/desktop-auth'
     | '/login'
     | '/password-reset'
     | '/pricing'
@@ -272,6 +291,7 @@ export interface FileRouteTypes {
     | '__root__'
     | '/'
     | '/_auth'
+    | '/desktop-auth'
     | '/login'
     | '/password-reset'
     | '/pricing'
@@ -287,6 +307,7 @@ export interface FileRouteTypes {
 export interface RootRouteChildren {
   IndexRoute: typeof IndexRoute
   AuthRoute: typeof AuthRouteWithChildren
+  DesktopAuthRoute: typeof DesktopAuthRoute
   LoginRoute: typeof LoginRoute
   PasswordResetRoute: typeof PasswordResetRouteWithChildren
   PricingRoute: typeof PricingRoute
@@ -299,6 +320,7 @@ export interface RootRouteChildren {
 const rootRouteChildren: RootRouteChildren = {
   IndexRoute: IndexRoute,
   AuthRoute: AuthRouteWithChildren,
+  DesktopAuthRoute: DesktopAuthRoute,
   LoginRoute: LoginRoute,
   PasswordResetRoute: PasswordResetRouteWithChildren,
   PricingRoute: PricingRoute,
@@ -320,6 +342,7 @@ export const routeTree = rootRoute
       "children": [
         "/",
         "/_auth",
+        "/desktop-auth",
         "/login",
         "/password-reset",
         "/pricing",
@@ -338,6 +361,9 @@ export const routeTree = rootRoute
         "/_auth/chat/$chatId"
       ]
     },
+    "/desktop-auth": {
+      "filePath": "desktop-auth.tsx"
+    },
     "/login": {
       "filePath": "login.tsx"
     },
diff --git a/frontend/src/routes/__root.tsx b/frontend/src/routes/__root.tsx
index c033c210..60f5b7bc 100644
--- a/frontend/src/routes/__root.tsx
+++ b/frontend/src/routes/__root.tsx
@@ -12,6 +12,7 @@ export type RootSearchParams = {
   selected_plan?: string;
   success?: boolean;
   canceled?: boolean;
+  provider?: string;
 };
 
 export const Route = createRootRouteWithContext<RootRouterContext>()({
@@ -21,7 +22,8 @@ export const Route = createRootRouteWithContext<RootRouterContext>()({
     next: typeof search.next === "string" ? search.next : undefined,
     selected_plan: typeof search.selected_plan === "string" ? search.selected_plan : undefined,
     success: typeof search.success === "boolean" ? search.success : undefined,
-    canceled: typeof search.canceled === "boolean" ? search.canceled : undefined
+    canceled: typeof search.canceled === "boolean" ? search.canceled : undefined,
+    provider: typeof search.provider === "string" ? search.provider : undefined
   })
 });
 
diff --git a/frontend/src/routes/auth.$provider.callback.tsx b/frontend/src/routes/auth.$provider.callback.tsx
index 848e258a..2a875000 100644
--- a/frontend/src/routes/auth.$provider.callback.tsx
+++ b/frontend/src/routes/auth.$provider.callback.tsx
@@ -43,17 +43,40 @@ function OAuthCallback() {
 
       if (code && state) {
         try {
-          if (provider === "github") {
-            await handleGitHubCallback(code, state, "");
-          } else if (provider === "google") {
-            await handleGoogleCallback(code, state, "");
-          } else {
-            throw new Error("Unsupported provider");
+          // Get the auth token from localStorage
+          await (provider === "github"
+            ? handleGitHubCallback(code, state, "")
+            : handleGoogleCallback(code, state, ""));
+
+          // Check if this is a desktop auth flow
+          const isDesktopAuth = localStorage.getItem("redirect-to-native") === "true";
+
+          // Clear the flag
+          localStorage.removeItem("redirect-to-native");
+
+          if (isDesktopAuth) {
+            // This is a desktop auth flow - redirect to the desktop app with tokens
+            // Get tokens from localStorage where they're stored after auth
+            const accessToken = localStorage.getItem("access_token") || "";
+            const refreshToken = localStorage.getItem("refresh_token");
+
+            // Construct the deep link URL using the consistent token names
+            let deepLinkUrl = `cloud.opensecret.maple://auth?access_token=${encodeURIComponent(accessToken)}`;
+
+            if (refreshToken) {
+              deepLinkUrl += `&refresh_token=${encodeURIComponent(refreshToken)}`;
+            }
+
+            // Redirect to the deep link
+            setTimeout(() => {
+              window.location.href = deepLinkUrl;
+            }, 1000);
+
+            return; // Stop further processing
           }
 
-          // Check for stored selected_plan
+          // Regular web flow - unchanged
           const selectedPlan = sessionStorage.getItem("selected_plan");
-          // Clear it from storage
           sessionStorage.removeItem("selected_plan");
 
           // If successful, redirect after a short delay
@@ -88,6 +111,26 @@ function OAuthCallback() {
     processCallback();
   }, [handleGitHubCallback, handleGoogleCallback, navigate, provider]);
 
+  // If this is a desktop auth flow, show a different UI
+  if (localStorage.getItem("redirect-to-native") === "true") {
+    return (
+      <Card className="max-w-md mx-auto mt-20">
+        <CardHeader>
+          <CardTitle>{formattedProvider} Authentication Successful</CardTitle>
+        </CardHeader>
+        <CardContent>
+          <p className="mb-4">
+            Authentication successful! Redirecting you back to the desktop app...
+          </p>
+          <div className="flex justify-center">
+            <Loader2 className="h-8 w-8 animate-spin" />
+          </div>
+        </CardContent>
+      </Card>
+    );
+  }
+
+  // Regular processing UI for web flow
   if (isProcessing) {
     return (
       <Card className="max-w-md mx-auto mt-20">
diff --git a/frontend/src/routes/desktop-auth.tsx b/frontend/src/routes/desktop-auth.tsx
new file mode 100644
index 00000000..c08c5839
--- /dev/null
+++ b/frontend/src/routes/desktop-auth.tsx
@@ -0,0 +1,83 @@
+import { createFileRoute, useNavigate } from "@tanstack/react-router";
+import { useEffect } from "react";
+import { useOpenSecret } from "@opensecret/react";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Loader2 } from "lucide-react";
+
+// Define the search parameters interface
+interface DesktopAuthSearchParams {
+  provider: string;
+  selected_plan?: string;
+}
+
+export const Route = createFileRoute("/desktop-auth")({
+  component: DesktopAuth,
+  validateSearch: (search: Record<string, unknown>): DesktopAuthSearchParams => {
+    const provider = typeof search.provider === "string" ? search.provider : "github";
+    // Validate provider is supported
+    if (provider !== "github" && provider !== "google") {
+      throw new Error(`Unsupported provider: ${provider}`);
+    }
+    return {
+      provider,
+      selected_plan: typeof search.selected_plan === "string" ? search.selected_plan : undefined
+    };
+  }
+});
+
+function DesktopAuth() {
+  // Use the typed search params
+  const search = Route.useSearch();
+  const { provider, selected_plan } = search;
+  const navigate = useNavigate();
+  const os = useOpenSecret();
+
+  useEffect(() => {
+    const initiateAuth = async () => {
+      try {
+        // Store the flag to indicate this is a desktop auth flow
+        localStorage.setItem("redirect-to-native", "true");
+
+        // Store selected plan if present
+        if (selected_plan) {
+          sessionStorage.setItem("selected_plan", selected_plan);
+        }
+
+        // Initiate appropriate OAuth flow
+        let auth_url;
+        if (provider === "github") {
+          const result = await os.initiateGitHubAuth("");
+          auth_url = result.auth_url;
+        } else if (provider === "google") {
+          const result = await os.initiateGoogleAuth("");
+          auth_url = result.auth_url;
+        } else {
+          throw new Error("Unsupported provider");
+        }
+
+        // Redirect to the OAuth provider
+        window.location.href = auth_url;
+      } catch (error) {
+        console.error(`Failed to initiate ${provider} login:`, error);
+        // Redirect to login page on error
+        navigate({ to: "/login" });
+      }
+    };
+
+    initiateAuth();
+  }, [os, provider, selected_plan, navigate]);
+
+  return (
+    <Card className="max-w-md mx-auto mt-20">
+      <CardHeader>
+        <CardTitle>Redirecting to {provider}</CardTitle>
+      </CardHeader>
+      <CardContent>
+        <p className="mb-4">Please wait while we redirect you to complete authentication...</p>
+        <div className="flex justify-center">
+          <Loader2 className="h-8 w-8 animate-spin" />
+        </div>
+      </CardContent>
+    </Card>
+  );
+}
diff --git a/frontend/src/routes/login.tsx b/frontend/src/routes/login.tsx
index 9f5e96bb..8fe7ed5f 100644
--- a/frontend/src/routes/login.tsx
+++ b/frontend/src/routes/login.tsx
@@ -9,7 +9,6 @@ import { Loader2, Github, Mail } from "lucide-react";
 import { Google } from "@/components/icons/Google";
 import { AuthMain } from "@/components/AuthMain";
 import { isTauri, invoke } from "@tauri-apps/api/core";
-import { listen } from "@tauri-apps/api/event";
 
 type LoginSearchParams = {
   next?: string;
@@ -83,67 +82,29 @@ function LoginPage() {
 
   const handleGitHubLogin = async () => {
     try {
-      const { auth_url } = await os.initiateGitHubAuth("");
-      if (selected_plan) {
-        sessionStorage.setItem("selected_plan", selected_plan);
-      }
-
       const isTauriEnv = await isTauri();
-      // Log which OAuth flow we're using (desktop or web)
       console.log("[OAuth] Using", isTauriEnv ? "desktop" : "web", "flow");
 
       if (isTauriEnv) {
-        // Desktop app: Use the OAuth plugin
-        try {
-          // Start the OAuth server
-          await invoke<number>("start_oauth_server");
-
-          // Listen for the redirect URL
-          const unlisten = await listen<string>("oauth_redirect", async (event) => {
-            const url = event.payload;
-            console.log("[OAuth] Received redirect callback");
+        // For desktop, redirect to the web app's desktop-auth route
+        let desktopAuthUrl = "https://trymaple.ai/desktop-auth?provider=github";
 
-            // Extract the code and state from the URL
-            const urlObj = new URL(url);
-            const code = urlObj.searchParams.get("code");
-            const state = urlObj.searchParams.get("state");
-
-            if (code && state) {
-              try {
-                await os.handleGitHubCallback(code, state, "");
-                console.log("[OAuth] GitHub authentication successful");
-                // Cancel the OAuth server
-                await invoke("plugin:oauth|cancel");
-
-                // Redirect after successful login
-                setTimeout(() => {
-                  if (selected_plan) {
-                    navigate({
-                      to: "/pricing",
-                      search: { selected_plan }
-                    });
-                  } else {
-                    navigate({ to: "/" });
-                  }
-                }, 2000);
-              } catch (error) {
-                console.error("GitHub callback error:", error);
-                setError("Failed to complete GitHub authentication");
-              } finally {
-                // Remove the event listener
-                await unlisten();
-              }
-            }
-          });
-
-          // We'll let the OS handle opening the auth URL
-          window.location.href = auth_url;
-        } catch (error) {
-          console.error("Tauri OAuth error:", error);
-          setError("Failed to authenticate with GitHub");
+        // If there's a selected plan, add it to the URL
+        if (selected_plan) {
+          desktopAuthUrl += `&selected_plan=${encodeURIComponent(selected_plan)}`;
         }
+
+        // Use the opener plugin by directly invoking the command
+        invoke("plugin:opener|open_url", { url: desktopAuthUrl }).catch((error: Error) => {
+          console.error("[OAuth] Failed to open external browser:", error);
+          setError("Failed to open authentication page in browser");
+        });
       } else {
-        // Web app: Redirect to the auth URL
+        // Web flow remains unchanged
+        const { auth_url } = await os.initiateGitHubAuth("");
+        if (selected_plan) {
+          sessionStorage.setItem("selected_plan", selected_plan);
+        }
         window.location.href = auth_url;
       }
     } catch (error) {
@@ -154,67 +115,29 @@ function LoginPage() {
 
   const handleGoogleLogin = async () => {
     try {
-      const { auth_url } = await os.initiateGoogleAuth("");
-      if (selected_plan) {
-        sessionStorage.setItem("selected_plan", selected_plan);
-      }
-
       const isTauriEnv = await isTauri();
-      // Log which OAuth flow we're using (desktop or web)
       console.log("[OAuth] Using", isTauriEnv ? "desktop" : "web", "flow");
 
       if (isTauriEnv) {
-        // Desktop app: Use the OAuth plugin
-        try {
-          // Start the OAuth server
-          await invoke<number>("start_oauth_server");
-
-          // Listen for the redirect URL
-          const unlisten = await listen<string>("oauth_redirect", async (event) => {
-            const url = event.payload;
-            console.log("[OAuth] Received redirect callback");
+        // For desktop, redirect to the web app's desktop-auth route
+        let desktopAuthUrl = "https://trymaple.ai/desktop-auth?provider=google";
 
-            // Extract the code and state from the URL
-            const urlObj = new URL(url);
-            const code = urlObj.searchParams.get("code");
-            const state = urlObj.searchParams.get("state");
-
-            if (code && state) {
-              try {
-                await os.handleGoogleCallback(code, state, "");
-                console.log("[OAuth] Google authentication successful");
-                // Cancel the OAuth server
-                await invoke("plugin:oauth|cancel");
-
-                // Redirect after successful login
-                setTimeout(() => {
-                  if (selected_plan) {
-                    navigate({
-                      to: "/pricing",
-                      search: { selected_plan }
-                    });
-                  } else {
-                    navigate({ to: "/" });
-                  }
-                }, 2000);
-              } catch (error) {
-                console.error("Google callback error:", error);
-                setError("Failed to complete Google authentication");
-              } finally {
-                // Remove the event listener
-                await unlisten();
-              }
-            }
-          });
-
-          // We'll let the OS handle opening the auth URL
-          window.location.href = auth_url;
-        } catch (error) {
-          console.error("Tauri OAuth error:", error);
-          setError("Failed to authenticate with Google");
+        // If there's a selected plan, add it to the URL
+        if (selected_plan) {
+          desktopAuthUrl += `&selected_plan=${encodeURIComponent(selected_plan)}`;
         }
+
+        // Use the opener plugin by directly invoking the command
+        invoke("plugin:opener|open_url", { url: desktopAuthUrl }).catch((error: Error) => {
+          console.error("[OAuth] Failed to open external browser:", error);
+          setError("Failed to open authentication page in browser");
+        });
       } else {
-        // Web app: Redirect to the auth URL
+        // Web flow remains unchanged
+        const { auth_url } = await os.initiateGoogleAuth("");
+        if (selected_plan) {
+          sessionStorage.setItem("selected_plan", selected_plan);
+        }
         window.location.href = auth_url;
       }
     } catch (error) {


Reviews:
user: login: greptile-apps[bot]
body: 
## PR Summary

This PR refactors OAuth flows to use external web deep linking via updated Tauri plugins while deprecating the old OAuth server approach. Key changes span configuration, core Rust logic, and frontend routing.

- Updated **/frontend/src-tauri/Cargo.toml** and **/frontend/package.json** to remove deprecated dependencies and add deep-link/opener support.  
- Modified **/frontend/src-tauri/src/lib.rs** to emit "deep-link-received" events and register the deep link scheme.  
- Added **/frontend/src/components/DeepLinkHandler.tsx** to listen for deep link events and store tokens (note: lacks event unsubscription).  
- Revised **/frontend/src-tauri/tauri.conf.json** to enable desktop deep linking.  
- Introduced **/frontend/src/routes/desktop-auth.tsx** and updated **/frontend/src/routes/auth.$provider.callback.tsx** for desktop OAuth flows with deep link redirection.

<sub>13 file(s) reviewed, 2 comment(s)</sub>
<sub>[Edit PR Review Bot Settings](https://app.greptile.com/review/github) | [Greptile](https://greptile.com?utm_source=greptile_expert&utm_medium=github&utm_campaign=code_reviews&utm_content=maple_45)</sub>
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T21:15:38Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>🧹 Nitpick comments (6)</summary><blockquote>

<details>
<summary>frontend/src/routes/login.tsx (2)</summary><blockquote>

`89-101`: **Desktop GitHub login flow using deep linking looks good.**

The implementation correctly:
- Constructs the desktop authentication URL
- Adds the selected plan as a query parameter if it exists
- Uses the opener plugin to open the URL in an external browser
- Handles errors appropriately



Consider extracting the deep linking logic into a reusable function to reduce duplication with the Google login flow. Example:

```diff
+const openDesktopAuthUrl = async (provider: string) => {
+  let desktopAuthUrl = `https://trymaple.ai/desktop-auth?provider=${provider}`;
+  
+  if (selected_plan) {
+    desktopAuthUrl += `&selected_plan=${encodeURIComponent(selected_plan)}`;
+  }
+  
+  return invoke("plugin:opener|open_url", { url: desktopAuthUrl }).catch((error: Error) => {
+    console.error("[OAuth] Failed to open external browser:", error);
+    setError("Failed to open authentication page in browser");
+  });
+};

 const handleGitHubLogin = async () => {
   try {
     const isTauriEnv = await isTauri();
     console.log("[OAuth] Using", isTauriEnv ? "desktop" : "web", "flow");

     if (isTauriEnv) {
-      // For desktop, redirect to the web app's desktop-auth route
-      let desktopAuthUrl = "https://trymaple.ai/desktop-auth?provider=github";
-
-      // If there's a selected plan, add it to the URL
-      if (selected_plan) {
-        desktopAuthUrl += `&selected_plan=${encodeURIComponent(selected_plan)}`;
-      }
-
-      // Use the opener plugin by directly invoking the command
-      invoke("plugin:opener|open_url", { url: desktopAuthUrl }).catch((error: Error) => {
-        console.error("[OAuth] Failed to open external browser:", error);
-        setError("Failed to open authentication page in browser");
-      });
+      await openDesktopAuthUrl("github");
     } else {
```

---

`121-134`: **Desktop Google login flow using deep linking is correct.**

The implementation mirrors the GitHub flow, correctly handling the authentication URL construction and error cases.



Update the log message to better reflect the new implementation:

```diff
-console.log("[OAuth] Using", isTauriEnv ? "desktop" : "web", "flow");
+console.log("[Auth] Using", isTauriEnv ? "desktop deep link" : "web OAuth", "flow");
```

</blockquote></details>
<details>
<summary>frontend/src/components/DeepLinkHandler.tsx (2)</summary><blockquote>

`15-41`: **Event handler implementation looks good, but consider adding more context to console logs.**

The deep link event listener is properly implemented with good error handling. However, be cautious about logging the full URL since it contains sensitive auth tokens.


Consider masking sensitive information in logs:

```diff
-            console.log("[Deep Link] Received URL:", url);
+            console.log("[Deep Link] Received URL:", url.replace(/([?&](access|refresh)_token=)[^&]+/g, "$1[REDACTED]"));
```

---

`21-35`: **Token storage implementation is secure but consider adding timeout.**

The authentication token extraction and storage logic is correctly implemented. Using localStorage is appropriate for this use case.


Consider adding an expiration mechanism for the access token:

```diff
+                // Store token expiration time (e.g., 1 hour from now)
+                const expiresAt = Date.now() + 3600000; // 1 hour in milliseconds
+                localStorage.setItem("token_expires_at", expiresAt.toString());
```

This would help with token lifecycle management.

</blockquote></details>
<details>
<summary>frontend/src/routes/desktop-auth.tsx (2)</summary><blockquote>

`13-19`: **Route validation has a default value that might be unintended.**

The validation logic defaults to "github" if the provider parameter is not a string, which might not be the intended behavior.


Consider throwing an error or redirecting to an error page if no valid provider is specified:

```diff
  validateSearch: (search: Record<string, unknown>): DesktopAuthSearchParams => {
+    if (typeof search.provider !== "string") {
+      throw new Error("Provider parameter is required");
+    }
    return {
-      provider: typeof search.provider === "string" ? search.provider : "github",
+      provider: search.provider,
      selected_plan: typeof search.selected_plan === "string" ? search.selected_plan : undefined
    };
  }
```

---

`28-61`: **Authentication flow is well implemented but lacks validation of OAuth providers.**

The desktop authentication initiation logic is well structured, but it could benefit from stricter validation of supported providers.


Consider using an enum or predefined list of supported providers:

```diff
+  // Define allowed providers
+  const SUPPORTED_PROVIDERS = ["github", "google"] as const;
+  type SupportedProvider = typeof SUPPORTED_PROVIDERS[number];

   useEffect(() => {
     const initiateAuth = async () => {
       try {
+        // Validate provider
+        if (!SUPPORTED_PROVIDERS.includes(provider as SupportedProvider)) {
+          throw new Error(`Unsupported provider: ${provider}`);
+        }
+
         // Store the flag to indicate this is a desktop auth flow
         localStorage.setItem("redirect-to-native", "true");
```

This would provide better type safety and clearer validation.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between a209ddb9e9a574ae91bccbe2e4f345a89f994b44 and f4ecd974fc1381e50564d59be86f55e0d64ab3ec.

</details>

<details>
<summary>⛔ Files ignored due to path filters (2)</summary>

* `frontend/bun.lock` is excluded by `!**/*.lock`
* `frontend/src-tauri/Cargo.lock` is excluded by `!**/*.lock`

</details>

<details>
<summary>📒 Files selected for processing (13)</summary>

* `frontend/package.json` (1 hunks)
* `frontend/src-tauri/Cargo.toml` (1 hunks)
* `frontend/src-tauri/capabilities/default.json` (1 hunks)
* `frontend/src-tauri/src/lib.rs` (2 hunks)
* `frontend/src-tauri/tauri.conf.json` (1 hunks)
* `frontend/src/app.tsx` (2 hunks)
* `frontend/src/components/DeepLinkHandler.tsx` (1 hunks)
* `frontend/src/routeTree.gen.ts` (13 hunks)
* `frontend/src/routes/__root.tsx` (2 hunks)
* `frontend/src/routes/auth.$provider.callback.tsx` (2 hunks)
* `frontend/src/routes/desktop-auth.tsx` (1 hunks)
* `frontend/src/routes/login.tsx` (2 hunks)
* `frontend/src/types/opener.d.ts` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (2)</summary>

<details>
<summary>frontend/src/app.tsx (1)</summary><blockquote>

<details>
<summary>frontend/src/components/DeepLinkHandler.tsx (1)</summary>

* `DeepLinkHandler` (9-54)

</details>

</blockquote></details>
<details>
<summary>frontend/src/routes/desktop-auth.tsx (3)</summary><blockquote>

<details>
<summary>frontend/src/routes/__root.tsx (1)</summary>

* `Route` (18-28)

</details>
<details>
<summary>frontend/src/routes/login.tsx (1)</summary>

* `Route` (18-24)

</details>
<details>
<summary>frontend/src/routes/auth.$provider.callback.tsx (1)</summary>

* `Route` (9-11)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (3)</summary>

* GitHub Check: build-macos (universal-apple-darwin)
* GitHub Check: build-linux
* GitHub Check: Cloudflare Pages

</details>

<details>
<summary>🔇 Additional comments (21)</summary><blockquote>

<details>
<summary>frontend/src-tauri/Cargo.toml (1)</summary>

`29-30`: **Dependencies added for deep linking support.**

The addition of `tauri-plugin-deep-link` and `tauri-plugin-opener` plugins aligns perfectly with the PR's objective to transition from OAuth server to deep linking for authentication. These dependencies provide the necessary functionality to handle deep links and open external URLs.

</details>
<details>
<summary>frontend/src-tauri/tauri.conf.json (1)</summary>

`21-24`: **Deep link scheme configuration looks good.**

The configuration correctly sets up the desktop deep linking scheme to `cloud.opensecret.maple`, which matches the application's identifier. This configuration will allow the app to register as a handler for URLs using this scheme, enabling the OAuth deep link flow to work properly.

</details>
<details>
<summary>frontend/src/app.tsx (1)</summary>

`13-13`: **DeepLinkHandler integration is well-placed.**

The DeepLinkHandler component is properly imported and strategically placed inside the BillingServiceProvider but outside the InnerApp. This ensures it's active throughout the application lifecycle and can process authentication deep links regardless of the current route.



Also applies to: 92-92

</details>
<details>
<summary>frontend/src/types/opener.d.ts (1)</summary>

`1-10`: **Well-documented TypeScript declaration.**

The declaration file for the opener plugin is clear and well-documented with JSDoc comments. This provides proper TypeScript typings for the `openPath` function, which enhances developer experience with autocompletion and type checking when opening external URLs during the authentication flow.

</details>
<details>
<summary>frontend/src-tauri/capabilities/default.json (1)</summary>

`10-21`: <details>
<summary>✅ Verification successful</summary>

**Permission configuration for URL opening looks good.**

The addition of the opener permission correctly configures the specific URLs that can be opened by the application, which is necessary for the new deep linking authentication flow.



Run the following script to verify that these URLs match those used in the login flow:

---

🏁 Script executed:

```shell
#!/bin/bash
# Verify that the URLs in capabilities match those used in the login flow
grep -r "trymaple.ai/" frontend/src/routes/
```

Length of output: 265

---



</details>

**Verified URL Permission Configuration**  
The login flow in `frontend/src/routes/login.tsx` uses URLs like `https://trymaple.ai/desktop-auth?provider=github` and `https://trymaple.ai/desktop-auth?provider=google`, which correctly match the allowed pattern `"https://trymaple.ai/*"` defined in `frontend/src-tauri/capabilities/default.json`.

- The permission configuration (`updater:default` and `opener:allow-open-url` with the specified URLs) correctly supports the deep linking authentication flow.

</details>
<details>
<summary>frontend/src/routes/__root.tsx (2)</summary>

`15-15`: **Good addition of the provider parameter.**

This parameter will allow the app to specify which authentication provider is being used in the deep linking flow.

---

`25-26`: **Correct validation for the provider parameter.**

The validation logic follows the same pattern as other parameters, ensuring type safety.

</details>
<details>
<summary>frontend/package.json (1)</summary>

`29-31`: <details>
<summary>✅ Verification successful</summary>

**Dependencies correctly updated for the new authentication approach.**

The dependencies have been appropriately updated to support the transition from OAuth to deep linking:
- Removed the OAuth plugin
- Added deep link and opener plugins



---

🏁 Script executed:

```shell
#!/bin/bash
# Check if these plugins have compatible versions with the rest of the Tauri ecosystem
grep -r "tauri" frontend/package.json
```

Length of output: 192

---



</details>

**Updated Tauri Dependency Verification**

The dependencies in `frontend/package.json` have been reviewed and correctly updated to support the new deep linking authentication approach. Here are the key points:

- **Removed Dependency:** The outdated OAuth plugin has been removed.
- **Added Dependencies:** New plugins for deep linking and opener functionality are in place.
- **Version Consistency:**  
  - `@tauri-apps/api`: "^2.0.0"  
  - `@tauri-apps/plugin-deep-link`: "^2.0.0"  
  - `@tauri-apps/plugin-opener`: "^2.2.6"  
  - An additional dependency, `@tauri-apps/cli` at "^2.2.7", is also present and aligns with the Tauri ecosystem.
  
No further changes are required.

</details>
<details>
<summary>frontend/src-tauri/src/lib.rs (3)</summary>

`5-13`: **Error handling in deep link event emitter is robust.**

The function correctly logs incoming deep links and emits events to the frontend with appropriate error handling.

---

`21-22`: **Plugin initialization looks good.**

The deep link and opener plugins are properly initialized.

---

`24-38`: <details>
<summary>❓ Verification inconclusive</summary>

**Good error handling for URL scheme registration.**

The deep link handler setup and URL scheme registration are well-implemented with proper error handling.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if there are other URL scheme registrations that might conflict
cat src-tauri/tauri.conf.json | grep -A 10 "deep-link"
```

Length of output: 112

---



</details>

**Deep link handler and URL scheme registration appear correctly implemented.**

The snippet in `frontend/src-tauri/src/lib.rs` handles deep link events and registers the URL scheme with proper error logging. However, our check for additional URL scheme registrations in `src-tauri/tauri.conf.json` returned a "No such file or directory" error. This suggests that either the file is located elsewhere or no conflicting registrations exist in configuration. Please manually verify if URL scheme settings are defined in an alternative configuration file or if they are managed exclusively by the code.

</details>
<details>
<summary>frontend/src/routes/auth.$provider.callback.tsx (2)</summary>

`47-50`: **Auth callback consolidation is cleaner.**

The consolidation of GitHub and Google callback logic into a single ternary operation improves code readability.

---

`114-131`: **Desktop auth UI implementation is clean and user-friendly.**

The desktop-specific UI provides appropriate feedback to users during the authentication process with a clear message and loading indicator.

</details>
<details>
<summary>frontend/src/routes/desktop-auth.tsx (2)</summary>

`8-11`: **Interface definition is clear and type-safe.**

The `DesktopAuthSearchParams` interface provides good type safety for the route parameters.

---

`63-75`: **Loading UI provides good user experience.**

The loading UI is well designed, properly indicating to users that they're being redirected to complete authentication.

</details>
<details>
<summary>frontend/src/routeTree.gen.ts (6)</summary>

`1-9`: **LGTM! Auto-generated file with appropriate changes for the new route.**

This file is auto-generated by TanStack Router as indicated in the header comments, and should not be manually modified.

---

`19-19`: **New import added for desktop authentication route.**

The import for the desktop-auth route is correctly added, aligning with the PR objective of transitioning to a deep linking approach for authentication.

---

`59-64`: **Proper route configuration for desktop authentication.**

The `DesktopAuthRoute` is correctly configured following the same pattern as other routes, with appropriate id, path, and parent route reference.

---

`118-124`: **Correct module declaration for the new route.**

The desktop-auth route is properly registered in the module declaration with the necessary properties, making it accessible through the router.

---

`218-218`: **Complete integration into type system and route collections.**

The new desktop authentication route is consistently added to all necessary interfaces and collections, ensuring proper TypeScript typing throughout the application.



Also applies to: 233-233, 249-249, 266-266, 280-280, 294-294, 310-310, 323-323

---

`345-345`: **Route properly added to manifest.**

The desktop-auth route is correctly added to the route manifest, ensuring it will be properly recognized by the TanStack Router system.



Also applies to: 364-366

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-04T21:17:01Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>frontend/src/components/DeepLinkHandler.tsx (1)</summary><blockquote>

`10-58`: **Well-structured effect with proper cleanup.**

The component correctly:
1. Sets up the deep link handler on mount
2. Properly cleans up the event listener on unmount
3. Handles potential errors during setup and processing

This implementation addresses the previous comment about missing effect cleanup.

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>frontend/src-tauri/src/lib.rs (1)</summary><blockquote>

`21-38`: **Deep link setup looks good, but consider security logging practices.**

The deep link registration and handler setup are well-implemented. However, when working with potentially sensitive data in URLs:

1. Consider using a redacted URL in logs to avoid exposing tokens in your logging system
2. Ensure the URL scheme registration is consistent with what's configured in your application's build files

```diff
- log::info!("[Deep Link] Received: {}", url);
+ let redacted_url = if url.contains("token") {
+    // Simple redaction for token-containing URLs
+    url.replace(url.split("?").nth(1).unwrap_or(""), "[REDACTED]")
+ } else {
+    url.to_string()
+ };
+ log::info!("[Deep Link] Received: {}", redacted_url);
```

</blockquote></details>
<details>
<summary>frontend/src/components/DeepLinkHandler.tsx (2)</summary><blockquote>

`20-42`: **Consider additional security measures for token handling.**

The deep link processing looks good, but consider these enhancements for security:

1. Validate token format before storage (e.g., check if tokens appear to be properly formatted JWTs if applicable)
2. Implement CSRF protection by verifying a state parameter if your OAuth flow supports it
3. Consider using secure storage alternatives if available in your environment rather than localStorage

```diff
if (accessToken && refreshToken) {
  console.log("[Deep Link] Auth tokens received");
+  // Basic validation - ensure tokens look legitimate
+  if (!validateTokenFormat(accessToken) || !validateTokenFormat(refreshToken)) {
+    console.error("[Deep Link] Invalid token format received");
+    return;
+  }

  // Store the tokens in localStorage with consistent naming
  localStorage.setItem("access_token", accessToken);
  localStorage.setItem("refresh_token", refreshToken);

  // Refresh the app state to reflect the logged-in status
  window.location.href = "/"; // Reload the app
}

// Helper function to validate token format
+ function validateTokenFormat(token) {
+   // Basic check - tokens are usually non-empty strings with some minimum length
+   // Adjust based on your token format (JWT, opaque, etc.)
+   return token && typeof token === 'string' && token.length > 20;
+ }
```

---

`1-61`: **Consider adding user feedback for authentication events.**

Since the authentication happens silently through deep linking, users might benefit from visual feedback about the authentication state.

You could implement a simple toast notification or status indicator to show when:
1. Authentication is successful
2. Authentication fails
3. The app is waiting for authentication

This would improve user experience during the authentication flow.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between f4ecd974fc1381e50564d59be86f55e0d64ab3ec and 9d97dc2b2a6d54a18408176dafac498a2e0cc4dd.

</details>

<details>
<summary>⛔ Files ignored due to path filters (2)</summary>

* `frontend/bun.lock` is excluded by `!**/*.lock`
* `frontend/src-tauri/Cargo.lock` is excluded by `!**/*.lock`

</details>

<details>
<summary>📒 Files selected for processing (13)</summary>

* `frontend/package.json` (1 hunks)
* `frontend/src-tauri/Cargo.toml` (1 hunks)
* `frontend/src-tauri/capabilities/default.json` (1 hunks)
* `frontend/src-tauri/src/lib.rs` (2 hunks)
* `frontend/src-tauri/tauri.conf.json` (1 hunks)
* `frontend/src/app.tsx` (2 hunks)
* `frontend/src/components/DeepLinkHandler.tsx` (1 hunks)
* `frontend/src/routeTree.gen.ts` (13 hunks)
* `frontend/src/routes/__root.tsx` (2 hunks)
* `frontend/src/routes/auth.$provider.callback.tsx` (2 hunks)
* `frontend/src/routes/desktop-auth.tsx` (1 hunks)
* `frontend/src/routes/login.tsx` (2 hunks)
* `frontend/src/types/opener.d.ts` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (11)</summary>

* frontend/src-tauri/tauri.conf.json
* frontend/src/types/opener.d.ts
* frontend/src/routes/__root.tsx
* frontend/src/app.tsx
* frontend/src/routes/login.tsx
* frontend/src/routes/auth.$provider.callback.tsx
* frontend/src-tauri/capabilities/default.json
* frontend/src/routes/desktop-auth.tsx
* frontend/package.json
* frontend/src-tauri/Cargo.toml
* frontend/src/routeTree.gen.ts

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (3)</summary>

* GitHub Check: build-linux
* GitHub Check: build-macos (universal-apple-darwin)
* GitHub Check: Cloudflare Pages

</details>

<details>
<summary>🔇 Additional comments (1)</summary><blockquote>

<details>
<summary>frontend/src-tauri/src/lib.rs (1)</summary>

`5-13`: **Well-implemented deep link handler function.**

The function correctly logs the incoming URL and emits it to the frontend using the "deep-link-received" event. The error handling is comprehensive, logging both successful emissions and failures.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-07T17:20:35Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>frontend/src/routes/login.tsx (1)</summary><blockquote>

`116-147`: **Google login implementation duplicates GitHub login logic.**

This is a duplicate of the GitHub login implementation with only the provider name changed. As suggested earlier, consider refactoring both implementations into a single helper function to reduce duplication and improve maintainability.

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>frontend/src/routes/login.tsx (1)</summary><blockquote>

`83-114`: **Consider refactoring GitHub login to reduce duplication with Google login.**

The desktop authentication flow for GitHub is nearly identical to Google's implementation. Consider creating a shared helper function to handle both providers.

```diff
- const handleGitHubLogin = async () => {
+ const handleOAuthLogin = async (provider: string) => {
  try {
    const isTauriEnv = await isTauri();
-   console.log("[OAuth] Using", isTauriEnv ? "desktop" : "web", "flow");
+   console.log(`[OAuth] Using ${isTauriEnv ? "desktop" : "web"} flow for ${provider}`);

    if (isTauriEnv) {
      // For desktop, redirect to the web app's desktop-auth route
-     let desktopAuthUrl = "https://trymaple.ai/desktop-auth?provider=github";
+     let desktopAuthUrl = `https://trymaple.ai/desktop-auth?provider=${provider}`;

      // If there's a selected plan, add it to the URL
      if (selected_plan) {
        desktopAuthUrl += `&selected_plan=${encodeURIComponent(selected_plan)}`;
      }

      // Use the opener plugin by directly invoking the command
      invoke("plugin:opener|open_url", { url: desktopAuthUrl }).catch((error: Error) => {
        console.error("[OAuth] Failed to open external browser:", error);
        setError("Failed to open authentication page in browser");
      });
    } else {
      // Web flow remains unchanged
-     const { auth_url } = await os.initiateGitHubAuth("");
+     const { auth_url } = await (provider === "github" 
+       ? os.initiateGitHubAuth("") 
+       : os.initiateGoogleAuth(""));
      if (selected_plan) {
        sessionStorage.setItem("selected_plan", selected_plan);
      }
      window.location.href = auth_url;
    }
  } catch (error) {
    console.error(`Failed to initiate ${provider} login:`, error);
-   setError("Failed to initiate GitHub login. Please try again.");
+   setError(`Failed to initiate ${provider} login. Please try again.`);
  }
};

+ const handleGitHubLogin = () => handleOAuthLogin("github");
+ const handleGoogleLogin = () => handleOAuthLogin("google");
```

</blockquote></details>
<details>
<summary>frontend/src/routes/desktop-auth.tsx (2)</summary><blockquote>

`28-61`: **Consider improving error handling and UX for failed authentication.**

The current implementation logs errors but redirects to the login page without informing the user about what went wrong. Consider enhancing this:

```diff
try {
  // Existing code...
} catch (error) {
  console.error(`Failed to initiate ${provider} login:`, error);
  // Redirect to login page on error
- navigate({ to: "/login" });
+ navigate({ 
+   to: "/login", 
+   search: { 
+     error: `Failed to authenticate with ${provider}. Please try again.` 
+   }
+ });
}
```

Then update the login page to check for and display this error message.

---

`61-61`: **Optimize useEffect dependencies.**

The dependency array includes `os` which is an object. Since objects are reference types, this might cause unnecessary re-renders. Consider including only the specific methods you use.

```diff
- }, [os, provider, selected_plan, navigate]);
+ }, [provider, selected_plan, navigate]);
```

Or better yet, memoize the authentication functions:

```diff
+ const initiateGitHubAuth = useCallback(() => os.initiateGitHubAuth(""), [os]);
+ const initiateGoogleAuth = useCallback(() => os.initiateGoogleAuth(""), [os]);

// Then in the useEffect:
if (provider === "github") {
-  const result = await os.initiateGitHubAuth("");
+  const result = await initiateGitHubAuth();
  auth_url = result.auth_url;
} else if (provider === "google") {
-  const result = await os.initiateGoogleAuth("");
+  const result = await initiateGoogleAuth();
  auth_url = result.auth_url;
}

// Update dependency array
- }, [os, provider, selected_plan, navigate]);
+ }, [initiateGitHubAuth, initiateGoogleAuth, provider, selected_plan, navigate]);
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 9d97dc2b2a6d54a18408176dafac498a2e0cc4dd and 34b4c03ccfb057700e8bd8adaadb0d675df49834.

</details>

<details>
<summary>⛔ Files ignored due to path filters (2)</summary>

* `frontend/bun.lock` is excluded by `!**/*.lock`
* `frontend/src-tauri/Cargo.lock` is excluded by `!**/*.lock`

</details>

<details>
<summary>📒 Files selected for processing (12)</summary>

* `frontend/package.json` (1 hunks)
* `frontend/src-tauri/Cargo.toml` (1 hunks)
* `frontend/src-tauri/capabilities/default.json` (1 hunks)
* `frontend/src-tauri/src/lib.rs` (2 hunks)
* `frontend/src-tauri/tauri.conf.json` (1 hunks)
* `frontend/src/app.tsx` (2 hunks)
* `frontend/src/components/DeepLinkHandler.tsx` (1 hunks)
* `frontend/src/routeTree.gen.ts` (13 hunks)
* `frontend/src/routes/__root.tsx` (2 hunks)
* `frontend/src/routes/auth.$provider.callback.tsx` (2 hunks)
* `frontend/src/routes/desktop-auth.tsx` (1 hunks)
* `frontend/src/routes/login.tsx` (2 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (9)</summary>

* frontend/src/routes/__root.tsx
* frontend/src/app.tsx
* frontend/src-tauri/capabilities/default.json
* frontend/src-tauri/Cargo.toml
* frontend/src-tauri/tauri.conf.json
* frontend/package.json
* frontend/src/routes/auth.$provider.callback.tsx
* frontend/src/components/DeepLinkHandler.tsx
* frontend/src/routeTree.gen.ts

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (1)</summary>

<details>
<summary>frontend/src/routes/desktop-auth.tsx (4)</summary><blockquote>

<details>
<summary>frontend/src/routes/__root.tsx (1)</summary>

* `Route` (18-28)

</details>
<details>
<summary>frontend/src/routes/auth.$provider.callback.tsx (1)</summary>

* `Route` (9-11)

</details>
<details>
<summary>frontend/src/routes/login.tsx (1)</summary>

* `Route` (18-24)

</details>
<details>
<summary>frontend/src/components/ui/card.tsx (4)</summary>

* `Card` (56-56)
* `CardHeader` (56-56)
* `CardTitle` (56-56)
* `CardContent` (56-56)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (3)</summary>

* GitHub Check: build-macos (universal-apple-darwin)
* GitHub Check: build-linux
* GitHub Check: Cloudflare Pages

</details>

<details>
<summary>🔇 Additional comments (6)</summary><blockquote>

<details>
<summary>frontend/src-tauri/src/lib.rs (4)</summary>

`1-3`: **Import changes look good for deep linking support**

The additions of `Emitter` trait, `DeepLinkExt`, and `tauri_plugin_opener` imports align well with the PR's objective to replace OAuth with a deep linking approach.

---

`5-13`: **Well-structured deep link event handler**

The new `handle_deep_link_event` function properly handles incoming deep links by logging them and emitting them to the frontend. The error handling with match expressions follows Rust best practices.

---

`21-22`: **Appropriate plugin registration**

The deep link and opener plugins are properly registered in the Tauri builder, replacing the previous OAuth plugin. This change is aligned with the PR objective of moving authentication to a web-based flow.

---

`24-38`: **Well-implemented deep link setup logic**

The deep link handler is properly set up with:
- Appropriate handle cloning to avoid lifetime issues
- Proper event registration
- Custom URL scheme registration with error handling
- Clear comments explaining the workflow

This implementation correctly supports the PR objective of handling authentication tokens received via deep links from the web application.

</details>
<details>
<summary>frontend/src/routes/desktop-auth.tsx (2)</summary>

`7-11`: **Interface definition for search parameters looks good.**

The `DesktopAuthSearchParams` interface clearly defines the expected structure for search parameters, with `provider` as a required field and `selected_plan` as optional.

---

`63-75`: **Good use of loading UI with clear feedback.**

The loading UI provides clear feedback to the user about what's happening. The card component with spinner and descriptive text enhances user experience during the redirection process.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-07T17:41:19Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (4)</summary><blockquote>

<details>
<summary>frontend/src-tauri/src/lib.rs (1)</summary><blockquote>

`1-13`: **Well-structured deep link handler with good logging**

The implementation of the deep link handler is clean and follows good practices with proper error handling and logging. The code aligns well with the PR objectives of replacing OAuth with deep linking.

Consider adding basic URL validation before processing to ensure the received URLs are properly formatted and safe to handle.

</blockquote></details>
<details>
<summary>frontend/src/routes/desktop-auth.tsx (3)</summary><blockquote>

`8-11`: **Consider defining `provider` as a union type for stricter validation.**  
You may define `provider` as `'github' | 'google'` within the interface to ensure compile-time checks instead of plain strings. This can help prevent passing unsupported providers elsewhere in the codebase.  

```diff
-interface DesktopAuthSearchParams {
-  provider: string;
-  selected_plan?: string;
-}
+type SupportedProviders = 'github' | 'google';

+interface DesktopAuthSearchParams {
+  provider: SupportedProviders;
+  selected_plan?: string;
+}
```

---

`13-27`: **Avoid redundant error check for an unsupported provider.**  
Since you already throw an error in lines 18-20 if the provider is not 'github' or 'google', the additional check in lines 55-56 may be unnecessary. Consolidating these checks into the `validateSearch` function keeps the provider validation in one place.  

```diff
-} else {
-  throw new Error("Unsupported provider");
-}
```

---

`29-83`: **Enhance user-facing error handling in the catch block.**  
Currently, encountering an error simply logs it and redirects to the login page. Consider providing a more descriptive UI message or directing users to a dedicated error screen to reduce confusion.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 34b4c03ccfb057700e8bd8adaadb0d675df49834 and 678fb83d0dbeab62a876b09c72888a675cf496a6.

</details>

<details>
<summary>⛔ Files ignored due to path filters (2)</summary>

* `frontend/bun.lock` is excluded by `!**/*.lock`
* `frontend/src-tauri/Cargo.lock` is excluded by `!**/*.lock`

</details>

<details>
<summary>📒 Files selected for processing (12)</summary>

* `frontend/package.json` (1 hunks)
* `frontend/src-tauri/Cargo.toml` (1 hunks)
* `frontend/src-tauri/capabilities/default.json` (1 hunks)
* `frontend/src-tauri/src/lib.rs` (2 hunks)
* `frontend/src-tauri/tauri.conf.json` (1 hunks)
* `frontend/src/app.tsx` (2 hunks)
* `frontend/src/components/DeepLinkHandler.tsx` (1 hunks)
* `frontend/src/routeTree.gen.ts` (13 hunks)
* `frontend/src/routes/__root.tsx` (2 hunks)
* `frontend/src/routes/auth.$provider.callback.tsx` (2 hunks)
* `frontend/src/routes/desktop-auth.tsx` (1 hunks)
* `frontend/src/routes/login.tsx` (2 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (9)</summary>

* frontend/src/routes/__root.tsx
* frontend/src-tauri/tauri.conf.json
* frontend/src/app.tsx
* frontend/src-tauri/Cargo.toml
* frontend/src-tauri/capabilities/default.json
* frontend/src/routes/auth.$provider.callback.tsx
* frontend/src/components/DeepLinkHandler.tsx
* frontend/package.json
* frontend/src/routes/login.tsx

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (1)</summary>

<details>
<summary>frontend/src/routes/desktop-auth.tsx (4)</summary><blockquote>

<details>
<summary>frontend/src/routes/__root.tsx (1)</summary>

* `Route` (18-28)

</details>
<details>
<summary>frontend/src/routes/auth.$provider.callback.tsx (1)</summary>

* `Route` (9-11)

</details>
<details>
<summary>frontend/src/routes/login.tsx (1)</summary>

* `Route` (18-24)

</details>
<details>
<summary>frontend/src/components/ui/card.tsx (4)</summary>

* `Card` (56-56)
* `CardHeader` (56-56)
* `CardTitle` (56-56)
* `CardContent` (56-56)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (3)</summary>

* GitHub Check: build-macos (universal-apple-darwin)
* GitHub Check: build-linux
* GitHub Check: Cloudflare Pages

</details>

<details>
<summary>🔇 Additional comments (14)</summary><blockquote>

<details>
<summary>frontend/src-tauri/src/lib.rs (2)</summary>

`21-22`: **Appropriate plugin initialization**

The addition of the deep link and opener plugins correctly replaces the removed OAuth plugin, aligning with the PR's objective to transition to a deep linking approach for authentication.

---

`24-38`: <details>
<summary>❓ Verification inconclusive</summary>

**Good deep link setup with proper event handling**

The deep link setup is well-implemented with appropriate app handle cloning for callbacks and proper error checking. The handler correctly processes the first URL from incoming events.

Consider implementing a fallback mechanism or user notification if the URL scheme registration fails, especially as this is crucial for the authentication flow to work properly.



Run this script to check how critical the URL scheme registration is for the application flow:

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if there are any error handling or fallback mechanisms for deep link failures
# Look for related code in the frontend that might handle authentication fallbacks

# Search for deep link related code in frontend
echo "Searching for deep link related code in the frontend..."
rg -A 5 -B 5 "deep-link-received" --type ts --type tsx

# Check if there's any fallback authentication mechanism
echo "Checking for fallback authentication mechanisms..."
rg -A 3 "auth.*fallback|fallback.*auth" --type ts --type tsx
```

Length of output: 405

---



</details>

**Deep Link Setup Reviewed: Fallback Mechanism Consideration Needed**

The deep link setup in `frontend/src-tauri/src/lib.rs` (lines 24–38) is implemented robustly with proper handle cloning, event checking, and error logging for scheme registration. However, based on our recent investigation, there’s no clear evidence of any fallback logic or user notification for when the URL scheme registration fails—which is critical for maintaining a robust authentication flow.

- The handler correctly processes the incoming URL events.
- The custom URL scheme `"cloud.opensecret.maple"` is registered with error logging on failure.
- **Suggestion:** Please verify (manually or via additional tests) that a fallback mechanism or adequate user notification is implemented elsewhere to handle potential registration failures.

</details>
<details>
<summary>frontend/src/routeTree.gen.ts (12)</summary>

`19-19`: **Import statement for newly introduced desktop-auth route looks good.**  
No issues identified here.

---

`59-63`: **New `DesktopAuthRoute` definition is consistent.**  
This aligns well with the root route. No concerns.

---

`118-124`: **Route mapping for `'/desktop-auth'` is properly declared.**  
Everything looks correct with the path, ID, and parent route.

---

`218-218`: **FileRoutesByFullPath entry for `'/desktop-auth'` is correct.**  
No problems with the reference to `DesktopAuthRoute`.

---

`233-233`: **FileRoutesByTo entry for `'/desktop-auth'` is correct.**  
No concerns here.

---

`249-249`: **FileRoutesById entry for `'/desktop-auth'` is correct.**  
No changes needed.

---

`266-266`: **Union type addition for `'desktop-auth'` in `fullPaths` is correct.**  
All good.

---

`280-280`: **Union type addition for `'/desktop-auth'` in `to` property is valid.**  
No issues.

---

`294-294`: **Union type addition for `'/desktop-auth'` in `id` property is correct.**  
Looks fine.

---

`310-310`: **RootRouteChildren interface updated with `DesktopAuthRoute`.**  
Implementation is consistent with the route definitions.

---

`323-323`: **`rootRouteChildren` updated with `DesktopAuthRoute`.**  
No issues with this mapping.

---

`365-366`: **Manifest entry for `'/desktop-auth'` is correct.**  
Matches the rest of the route tree changes.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-07T18:03:11Z

Review comments:
diff_hunk: @@ -43,17 +43,40 @@ function OAuthCallback() {
 
       if (code && state) {
         try {
-          if (provider === "github") {
-            await handleGitHubCallback(code, state, "");
-          } else if (provider === "google") {
-            await handleGoogleCallback(code, state, "");
-          } else {
-            throw new Error("Unsupported provider");
+          // Get the auth token from localStorage
+          await (provider === "github"
+            ? handleGitHubCallback(code, state, "")
+            : handleGoogleCallback(code, state, ""));
+
path: frontend/src/routes/auth.$provider.callback.tsx
user: login: greptile-apps[bot]
body: logic: No branch for unsupported providers. Consider explicitly erroring out if provider is not GitHub or Google.

```suggestion
          // Get the auth token from localStorage
          if (provider !== "github" && provider !== "google") {
            throw new Error("Unsupported authentication provider");
          }
          await (provider === "github"
            ? handleGitHubCallback(code, state, "")
            : handleGoogleCallback(code, state, ""));
```
created_at: 2025-04-04T21:14:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -88,6 +111,26 @@ function OAuthCallback() {
     processCallback();
   }, [handleGitHubCallback, handleGoogleCallback, navigate, provider]);
 
+  // If this is a desktop auth flow, show a different UI
+  if (localStorage.getItem("redirect-to-native") === "true") {
+    return (
+      <Card className="max-w-md mx-auto mt-20">
+        <CardHeader>
+          <CardTitle>{formattedProvider} Authentication Successful</CardTitle>
+        </CardHeader>
+        <CardContent>
+          <p className="mb-4">
+            Authentication successful! Redirecting you back to the desktop app...
+          </p>
+          <div className="flex justify-center">
+            <Loader2 className="h-8 w-8 animate-spin" />
+          </div>
+        </CardContent>
+      </Card>
+    );
+  }
path: frontend/src/routes/auth.$provider.callback.tsx
user: login: greptile-apps[bot]
body: logic: Using localStorage.getItem for 'redirect-to-native' in render may cause inconsistencies. Consider storing it in state.
created_at: 2025-04-04T21:14:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,54 @@
+import { useEffect } from "react";
+import { isTauri } from "@tauri-apps/api/core";
+import { listen } from "@tauri-apps/api/event";
+
+// For direct deep link handling, we'll listen to our custom event
+// If we had the types installed, we would use:
+// import { onOpenUrl } from '@tauri-apps/plugin-deep-link';
+
+export function DeepLinkHandler() {
+  useEffect(() => {
+    const setupDeepLinkHandling = async () => {
+      try {
+        if (await isTauri()) {
+          // Listen for the custom event we emit from Rust
+          await listen<string>("deep-link-received", (event) => {
+            const url = event.payload;
+            console.log("[Deep Link] Received URL:", url);
+
+            try {
+              // Parse the URL to extract the tokens
+              const urlObj = new URL(url);
+              // The URL will look like: cloud.opensecret.maple://auth?access_token=...&refresh_token=...
+              const accessToken = urlObj.searchParams.get("access_token");
+              const refreshToken = urlObj.searchParams.get("refresh_token");
+
+              if (accessToken && refreshToken) {
+                console.log("[Deep Link] Auth tokens received");
+
+                // Store the tokens in localStorage with consistent naming
+                localStorage.setItem("access_token", accessToken);
+                localStorage.setItem("refresh_token", refreshToken);
+
+                // Refresh the app state to reflect the logged-in status
+                window.location.href = "/"; // Reload the app
+              } else {
+                console.error("[Deep Link] Missing tokens in deep link");
+              }
+            } catch (error) {
+              console.error("[Deep Link] Failed to process deep link:", error);
+            }
+          });
+
+          console.log("[Deep Link] Handler setup complete");
+        }
+      } catch (error) {
+        console.error("[Deep Link] Setup failed:", error);
+      }
+    };
+
+    setupDeepLinkHandling();
+  }, []);
path: frontend/src/components/DeepLinkHandler.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Effect cleanup is missing.**

The useEffect hook sets up event listeners but doesn't clean them up when the component unmounts.


Add a cleanup function to prevent memory leaks:

```diff
  useEffect(() => {
+    let unlisten: (() => void) | undefined;
    
    const setupDeepLinkHandling = async () => {
      try {
        if (await isTauri()) {
          // Listen for the custom event we emit from Rust
-          await listen<string>("deep-link-received", (event) => {
+          unlisten = await listen<string>("deep-link-received", (event) => {
            const url = event.payload;
            // Rest of the handler
          });
          
          console.log("[Deep Link] Handler setup complete");
        }
      } catch (error) {
        console.error("[Deep Link] Setup failed:", error);
      }
    };

    setupDeepLinkHandling();
+    
+    // Return cleanup function
+    return () => {
+      if (unlisten) unlisten();
+    };
  }, []);
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  useEffect(() => {
    let unlisten: (() => void) | undefined;
    
    const setupDeepLinkHandling = async () => {
      try {
        if (await isTauri()) {
          // Listen for the custom event we emit from Rust
          unlisten = await listen<string>("deep-link-received", (event) => {
            const url = event.payload;
            console.log("[Deep Link] Received URL:", url);

            try {
              // Parse the URL to extract the tokens
              const urlObj = new URL(url);
              // The URL will look like: cloud.opensecret.maple://auth?access_token=...&refresh_token=...
              const accessToken = urlObj.searchParams.get("access_token");
              const refreshToken = urlObj.searchParams.get("refresh_token");

              if (accessToken && refreshToken) {
                console.log("[Deep Link] Auth tokens received");

                // Store the tokens in localStorage with consistent naming
                localStorage.setItem("access_token", accessToken);
                localStorage.setItem("refresh_token", refreshToken);

                // Refresh the app state to reflect the logged-in status
                window.location.href = "/"; // Reload the app
              } else {
                console.error("[Deep Link] Missing tokens in deep link");
              }
            } catch (error) {
              console.error("[Deep Link] Failed to process deep link:", error);
            }
          });

          console.log("[Deep Link] Handler setup complete");
        }
      } catch (error) {
        console.error("[Deep Link] Setup failed:", error);
      }
    };

    setupDeepLinkHandling();
    
    // Return cleanup function
    return () => {
      if (unlisten) unlisten();
    };
  }, []);
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-04T21:17:00Z
author_association: NONE
diff_hunk: @@ -43,17 +43,40 @@ function OAuthCallback() {
 
       if (code && state) {
         try {
-          if (provider === "github") {
-            await handleGitHubCallback(code, state, "");
-          } else if (provider === "google") {
-            await handleGoogleCallback(code, state, "");
-          } else {
-            throw new Error("Unsupported provider");
+          // Get the auth token from localStorage
+          await (provider === "github"
+            ? handleGitHubCallback(code, state, "")
+            : handleGoogleCallback(code, state, ""));
+
+          // Check if this is a desktop auth flow
+          const isDesktopAuth = localStorage.getItem("redirect-to-native") === "true";
+
+          // Clear the flag
+          localStorage.removeItem("redirect-to-native");
+
+          if (isDesktopAuth) {
+            // This is a desktop auth flow - redirect to the desktop app with tokens
+            // Get tokens from localStorage where they're stored after auth
+            const accessToken = localStorage.getItem("access_token") || "";
+            const refreshToken = localStorage.getItem("refresh_token");
+
+            // Construct the deep link URL using the consistent token names
+            let deepLinkUrl = `cloud.opensecret.maple://auth?access_token=${encodeURIComponent(accessToken)}`;
+
+            if (refreshToken) {
+              deepLinkUrl += `&refresh_token=${encodeURIComponent(refreshToken)}`;
+            }
+
+            // Redirect to the deep link
+            setTimeout(() => {
+              window.location.href = deepLinkUrl;
+            }, 1000);
+
+            return; // Stop further processing
           }
path: frontend/src/routes/auth.$provider.callback.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Desktop auth flow implementation is robust, but token handling needs caution.**

The new desktop authentication flow is well designed. However, when constructing the deep link URL, security considerations should be kept in mind.


For added security, consider:
1. Not persisting tokens in localStorage after they've been passed to the desktop app
2. Adding a timeout for token validity

```diff
            setTimeout(() => {
              window.location.href = deepLinkUrl;
+              // Clear tokens from localStorage after redirecting
+              setTimeout(() => {
+                localStorage.removeItem("access_token");
+                localStorage.removeItem("refresh_token");
+              }, 1500); // Give a bit more time than the redirect
            }, 1000);
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
          // Check if this is a desktop auth flow
          const isDesktopAuth = localStorage.getItem("redirect-to-native") === "true";

          // Clear the flag
          localStorage.removeItem("redirect-to-native");

          if (isDesktopAuth) {
            // This is a desktop auth flow - redirect to the desktop app with tokens
            // Get tokens from localStorage where they're stored after auth
            const accessToken = localStorage.getItem("access_token") || "";
            const refreshToken = localStorage.getItem("refresh_token");

            // Construct the deep link URL using the consistent token names
            let deepLinkUrl = `cloud.opensecret.maple://auth?access_token=${encodeURIComponent(accessToken)}`;

            if (refreshToken) {
              deepLinkUrl += `&refresh_token=${encodeURIComponent(refreshToken)}`;
            }

            // Redirect to the deep link
            setTimeout(() => {
              window.location.href = deepLinkUrl;
              // Clear tokens from localStorage after redirecting
              setTimeout(() => {
                localStorage.removeItem("access_token");
                localStorage.removeItem("refresh_token");
              }, 1500); // Give a bit more time than the redirect
            }, 1000);

            return; // Stop further processing
          }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-04T21:17:00Z
author_association: NONE
diff_hunk: @@ -83,67 +82,29 @@ function LoginPage() {
 
   const handleGitHubLogin = async () => {
     try {
-      const { auth_url } = await os.initiateGitHubAuth("");
-      if (selected_plan) {
-        sessionStorage.setItem("selected_plan", selected_plan);
-      }
-
       const isTauriEnv = await isTauri();
-      // Log which OAuth flow we're using (desktop or web)
       console.log("[OAuth] Using", isTauriEnv ? "desktop" : "web", "flow");
 
       if (isTauriEnv) {
-        // Desktop app: Use the OAuth plugin
-        try {
-          // Start the OAuth server
-          await invoke<number>("start_oauth_server");
-
-          // Listen for the redirect URL
-          const unlisten = await listen<string>("oauth_redirect", async (event) => {
-            const url = event.payload;
-            console.log("[OAuth] Received redirect callback");
+        // For desktop, redirect to the web app's desktop-auth route
+        let desktopAuthUrl = "https://trymaple.ai/desktop-auth?provider=github";
path: frontend/src/routes/login.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Avoid hardcoding URLs in the codebase.**

The hardcoded URL `https://trymaple.ai/desktop-auth` makes the code less maintainable and harder to deploy to different environments. Consider using an environment variable or a configuration object.

```diff
- let desktopAuthUrl = "https://trymaple.ai/desktop-auth?provider=github";
+ // Import from a config file or use an environment variable
+ import { APP_URL } from "@/config";
+ let desktopAuthUrl = `${APP_URL}/desktop-auth?provider=github`;
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
        // For desktop, redirect to the web app's desktop-auth route
        // Import from a config file or use an environment variable
        import { APP_URL } from "@/config";
        let desktopAuthUrl = `${APP_URL}/desktop-auth?provider=github`;
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-07T17:41:18Z
author_association: NONE
diff_hunk: @@ -0,0 +1,76 @@
+import { createFileRoute, useNavigate } from "@tanstack/react-router";
+import { useEffect } from "react";
+import { useOpenSecret } from "@opensecret/react";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Loader2 } from "lucide-react";
+
+// Define the search parameters interface
+interface DesktopAuthSearchParams {
+  provider: string;
+  selected_plan?: string;
+}
+
+export const Route = createFileRoute("/desktop-auth")({
+  component: DesktopAuth,
+  validateSearch: (search: Record<string, unknown>): DesktopAuthSearchParams => ({
+    provider: typeof search.provider === "string" ? search.provider : "github",
+    selected_plan: typeof search.selected_plan === "string" ? search.selected_plan : undefined
+  })
+});
path: frontend/src/routes/desktop-auth.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Enhance provider validation in the route definition.**

The current implementation provides a default value of "github" but doesn't validate if the provider is supported. This could lead to the error in lines 47-49 being thrown unnecessarily.

```diff
validateSearch: (search: Record<string, unknown>): DesktopAuthSearchParams => {
  const provider = typeof search.provider === "string" ? search.provider : "github";
+ // Validate provider is supported
+ if (provider !== "github" && provider !== "google") {
+   throw new Error(`Unsupported provider: ${provider}`);
+ }
  return {
-   provider: typeof search.provider === "string" ? search.provider : "github",
+   provider,
    selected_plan: typeof search.selected_plan === "string" ? search.selected_plan : undefined
  };
}
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
export const Route = createFileRoute("/desktop-auth")({
  component: DesktopAuth,
  validateSearch: (search: Record<string, unknown>): DesktopAuthSearchParams => {
    const provider = typeof search.provider === "string" ? search.provider : "github";
    // Validate provider is supported
    if (provider !== "github" && provider !== "google") {
      throw new Error(`Unsupported provider: ${provider}`);
    }
    return {
      provider,
      selected_plan: typeof search.selected_plan === "string" ? search.selected_plan : undefined
    };
  }
});
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-07T17:41:18Z
author_association: NONE

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-04-04T21:12:59Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

This pull request replaces the OAuth-based authentication with deep linking and desktop authentication. The changes remove the old OAuth server functions and dependencies, introduce new deep link and opener plugins in both JavaScript and Rust, and update the Tauri configuration accordingly. New React components and routing artifacts are added to support handling deep link events and a desktop authentication flow. Minor improvements include updating permissions and search parameters.

## Changes

| File(s)                                                                                                                                                                | Change Summary                                                                                                                                                                                                                                                                                                                                           |
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `frontend/package.json`<br>`frontend/src-tauri/Cargo.toml`                                                                                                             | Removed OAuth-related dependencies and re-added/replaced with deep linking and opener plugins for both Node and Rust environments.                                                                                                                                                                                                                      |
| `frontend/src-tauri/tauri.conf.json`<br>`frontend/src-tauri/capabilities/default.json`                                                                                 | Added deep link configuration and a new permission entry allowing specific URL patterns for opening external resources.                                                                                                                                                                                                                                  |
| `frontend/src-tauri/src/lib.rs`                                                                                                                                         | Removed the asynchronous OAuth server function and replaced it with a synchronous deep link event handler. Updated plugin initialization to register the new deep link and opener plugins.                                                                                                                                                           |
| `frontend/src/app.tsx`<br>`frontend/src/components/DeepLinkHandler.tsx`                                                                                               | Introduced the `DeepLinkHandler` component in the UI that listens for deep link events, parses tokens from URL parameters, and manages local storage updates.                                                                                                                                                                                         |
| `frontend/src/routeTree.gen.ts`<br>`frontend/src/routes/__root.tsx`<br>`frontend/src/routes/desktop-auth.tsx`                                                            | Added a new desktop authentication route and updated routing interfaces and search parameters to include a new `provider` property, integrating the desktop auth flow into the overall routing structure.                                                                                                                                        |
| `frontend/src/routes/auth.$provider.callback.tsx`<br>`frontend/src/routes/login.tsx`                                                                                   | Modified the OAuth callback and login flows to account for desktop authentication. Introduced a check for a desktop flag, streamlined the callback with redirection logic via deep linking, and replaced the previous OAuth server based approach.                                                                                                   |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant E as External Source
    participant B as Tauri Backend
    participant FE as Frontend (DeepLinkHandler)
    
    E->>B: Send deep link URL
    B->>B: Process URL via handle_deep_link_event
    B->>FE: Emit "deep-link-received" event
    FE->>FE: Parse URL and extract tokens
    FE->>FE: Store tokens in localStorage and reload app
```

```mermaid
sequenceDiagram
    participant U as User
    participant LP as LoginPage
    participant O as Opener Plugin
    participant BA as Browser/OAuth Provider
    participant DA as DesktopAuthRoute
    
    U->>LP: Click Desktop Login
    LP->>O: Open authentication URL externally
    O->>BA: Redirect to OAuth provider for authentication
    BA->>DA: Return from provider via deep link
    DA->>DA: Set desktop auth flag and process tokens
    DA->>U: Redirect back to desktop application
```

## Possibly related PRs

- OpenSecretCloud/Maple#2: The changes in the main PR are related to the modifications in the `frontend/src/routes/auth.$provider.callback.tsx` file, as both PRs involve updates to the OAuth callback handling, specifically focusing on streamlining the process and removing dependencies on certain variables.

## Poem

> Hop, hop, I'm a rabbit with code so bright,  
> Skipping through modules from morning to night.  
> OAuth left behind, deep linking in view,  
> Desktop auth now shines with a fresh debut.  
> Carrots and code dance in a rhythmic spree,  
> Celebrating changes with hops of glee!  
> 🥕💻 Hip-hip, hooray for this tech jubilee!

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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= -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=OpenSecretCloud/Maple&utm_content=45):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai plan` to trigger planning for file edits and PR creation.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE
user: login: cloudflare-workers-and-pages[bot]
created_at: 2025-04-04T21:13:18Z
body: ## Deploying maple with &nbsp;<a href="https://pages.dev"><img alt="Cloudflare Pages" src="https://user-images.githubusercontent.com/23264/106598434-9e719e00-654f-11eb-9e59-6167043cfa01.png" width="16"></a> &nbsp;Cloudflare Pages

<table><tr><td><strong>Latest commit:</strong> </td><td>
<code>678fb83</code>
</td></tr>
<tr><td><strong>Status:</strong></td><td>&nbsp;✅&nbsp; Deploy successful!</td></tr>
<tr><td><strong>Preview URL:</strong></td><td>
<a href='https://5181252a.maple-ca8.pages.dev'>https://5181252a.maple-ca8.pages.dev</a>
</td></tr>
<tr><td><strong>Branch Preview URL:</strong></td><td>
<a href='https://external-oauth.maple-ca8.pages.dev'>https://external-oauth.maple-ca8.pages.dev</a>
</td></tr>
</table>

[View logs](https://dash.cloudflare.com/e6f61452146dfb34b56923b4de4bc098/pages/view/maple/5181252a-5afe-4445-9de1-090869d6f3e6)

author_association: NONE