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