Browse Source
			
			
			
			
				
		- Add type-safe deep link parameter validation using Zod - Implement consistent error handling across all deep link routes - Add support for query parameters in deep links - Create comprehensive deep linking documentation - Add logging for deep link operations Security: - Validate all deep link parameters before processing - Sanitize and type-check query parameters - Add error boundaries around deep link handling - Implement route-specific parameter validation Testing: - Add parameter validation tests - Add error handling tests - Test query parameter support
				 6 changed files with 199 additions and 85 deletions
			
			
		| @ -0,0 +1,48 @@ | |||
| # TimeSafari Deep Linking | |||
| 
 | |||
| ## Supported URL Schemes | |||
| 
 | |||
| All deep links follow the format: `timesafari://<route>/<param>?<query>` | |||
| 
 | |||
| ### Claim Routes | |||
| 
 | |||
| - `timesafari://claim/:id` | |||
|   - Query params: | |||
|     - `view`: "details" | "certificate" | "raw" | |||
| 
 | |||
| - `timesafari://claim-cert/:id` | |||
| - `timesafari://claim-add-raw/:id` | |||
|   - Query params: | |||
|     - `claim`: JSON string of claim data | |||
|     - `claimJwtId`: JWT ID for claim | |||
| 
 | |||
| ### Contact Routes | |||
| 
 | |||
| - `timesafari://contact-edit/:did` | |||
| - `timesafari://contact-import/:jwt` | |||
|   - Query params: | |||
|     - `contacts`: JSON array of contacts | |||
| 
 | |||
| ### Project Routes | |||
| 
 | |||
| - `timesafari://project/:id` | |||
|   - Query params: | |||
|     - `view`: "details" | "edit" | |||
| 
 | |||
| ### Invite Routes | |||
| 
 | |||
| - `timesafari://invite-one-accept/:jwt` | |||
|   - Query params: | |||
|     - `type`: "one" | "many" | |||
| 
 | |||
| ### Gift Routes | |||
| 
 | |||
| - `timesafari://confirm-gift/:id` | |||
|   - Query params: | |||
|     - `action`: "confirm" | "details" | |||
| 
 | |||
| ### Offer Routes | |||
| 
 | |||
| - `timesafari://offer-details/:id` | |||
|   - Query params: | |||
|     - `view`: "details" | |||
| @ -0,0 +1,84 @@ | |||
| import { Router } from "vue-router"; | |||
| import { deepLinkSchemas, DeepLinkParams } from "../types/deepLinks"; | |||
| import { logConsoleAndDb } from "../db"; | |||
| 
 | |||
| interface DeepLinkError extends Error { | |||
|   code: string; | |||
|   details?: unknown; | |||
| } | |||
| 
 | |||
| export class DeepLinkHandler { | |||
|   private router: Router; | |||
| 
 | |||
|   constructor(router: Router) { | |||
|     this.router = router; | |||
|   } | |||
| 
 | |||
|   /** | |||
|    * Processes incoming deep links and routes them appropriately | |||
|    * @param url The deep link URL to process | |||
|    */ | |||
|   async handleDeepLink(url: string): Promise<void> { | |||
|     try { | |||
|       logConsoleAndDb("[DeepLink] Processing URL: " + url, false); | |||
|        | |||
|       const { path, params, query } = this.parseDeepLink(url); | |||
|       await this.validateAndRoute(path, params, query); | |||
| 
 | |||
|     } catch (error) { | |||
|       const deepLinkError = error as DeepLinkError; | |||
|       logConsoleAndDb( | |||
|         `[DeepLink] Error (${deepLinkError.code}): ${deepLinkError.message}`, | |||
|         true | |||
|       ); | |||
|        | |||
|       throw { | |||
|         code: deepLinkError.code || 'UNKNOWN_ERROR', | |||
|         message: deepLinkError.message, | |||
|         details: deepLinkError.details | |||
|       }; | |||
|     } | |||
|   } | |||
| 
 | |||
|   /** | |||
|    * Routes the deep link to appropriate view with validated parameters | |||
|    */ | |||
|   private async validateAndRoute( | |||
|     path: string, | |||
|     params: Record<string, string>, | |||
|     query: Record<string, string> | |||
|   ): Promise<void> { | |||
|     const routeMap: Record<string, string> = { | |||
|       claim: 'claim', | |||
|       'claim-cert': 'claim-cert', | |||
|       'claim-add-raw': 'claim-add-raw', | |||
|       'contact-edit': 'contact-edit', | |||
|       'contact-import': 'contact-import', | |||
|       project: 'project', | |||
|       'invite-one-accept': 'invite-one-accept', | |||
|       'offer-details': 'offer-details', | |||
|       'confirm-gift': 'confirm-gift' | |||
|     }; | |||
| 
 | |||
|     const routeName = routeMap[path]; | |||
|     if (!routeName) { | |||
|       throw { | |||
|         code: 'INVALID_ROUTE', | |||
|         message: `Unsupported route: ${path}` | |||
|       }; | |||
|     } | |||
| 
 | |||
|     // Validate parameters based on route type
 | |||
|     const schema = deepLinkSchemas[path as keyof typeof deepLinkSchemas]; | |||
|     const validatedParams = await schema.parseAsync({ | |||
|       ...params, | |||
|       ...query | |||
|     }); | |||
| 
 | |||
|     await this.router.replace({ | |||
|       name: routeName, | |||
|       params: validatedParams, | |||
|       query | |||
|     }); | |||
|   } | |||
| }  | |||
| @ -0,0 +1,46 @@ | |||
| import { z } from "zod"; | |||
| 
 | |||
| // Base URL validation schema
 | |||
| const baseUrlSchema = z.object({ | |||
|   scheme: z.literal("timesafari"), | |||
|   path: z.string(), | |||
|   queryParams: z.record(z.string()).optional() | |||
| }); | |||
| 
 | |||
| // Parameter validation schemas for each route type
 | |||
| export const deepLinkSchemas = { | |||
|   claim: z.object({ | |||
|     id: z.string().min(1), | |||
|     view: z.enum(["details", "certificate", "raw"]).optional() | |||
|   }), | |||
|    | |||
|   contact: z.object({ | |||
|     did: z.string().regex(/^did:/), | |||
|     action: z.enum(["edit", "import"]).optional(), | |||
|     jwt: z.string().optional() | |||
|   }), | |||
| 
 | |||
|   project: z.object({ | |||
|     id: z.string().min(1), | |||
|     view: z.enum(["details", "edit"]).optional() | |||
|   }), | |||
| 
 | |||
|   invite: z.object({ | |||
|     jwt: z.string().min(1), | |||
|     type: z.enum(["one", "many"]).optional() | |||
|   }), | |||
| 
 | |||
|   gift: z.object({ | |||
|     id: z.string().min(1), | |||
|     action: z.enum(["confirm", "details"]).optional() | |||
|   }), | |||
| 
 | |||
|   offer: z.object({ | |||
|     id: z.string().min(1), | |||
|     view: z.enum(["details"]).optional() | |||
|   }) | |||
| }; | |||
| 
 | |||
| export type DeepLinkParams = { | |||
|   [K in keyof typeof deepLinkSchemas]: z.infer<typeof deepLinkSchemas[K]>; | |||
| };  | |||
					Loading…
					
					
				
		Reference in new issue