/** * Validation utilities and helpers */ import { z } from 'zod'; import { JWT_ID_PATTERN, ERROR_CODES } from './constants'; import { PollingError } from './types'; import { StarredProjectsResponseSchema, DeepLinkParamsSchema, ErrorResponseSchema, RateLimitResponseSchema } from './schemas'; /** * Validate JWT ID format */ export function validateJwtId(jwtId: string): boolean { return JWT_ID_PATTERN.test(jwtId); } /** * Compare JWT IDs lexicographically */ export function compareJwtIds(a: string, b: string): number { if (!validateJwtId(a) || !validateJwtId(b)) { throw new Error('Invalid JWT ID format'); } return a.localeCompare(b); } /** * Extract timestamp from JWT ID */ export function extractJwtTimestamp(jwtId: string): number { const match = jwtId.match(JWT_ID_PATTERN); if (!match || !match[1]) { throw new Error('Invalid JWT ID format'); } return parseInt(match[1], 10); } /** * Validate starred projects response */ export function validateStarredProjectsResponse(data: unknown): boolean { return StarredProjectsResponseSchema.safeParse(data).success; } /** * Validate deep link parameters */ export function validateDeepLinkParams(params: unknown): boolean { return DeepLinkParamsSchema.safeParse(params).success; } /** * Validate error response */ export function validateErrorResponse(data: unknown): boolean { return ErrorResponseSchema.safeParse(data).success; } /** * Validate rate limit response */ export function validateRateLimitResponse(data: unknown): boolean { return RateLimitResponseSchema.safeParse(data).success; } /** * Create response schema validator */ export function createResponseValidator(schema: z.ZodSchema): { validate: (data: unknown) => data is T; transformError: (error: unknown) => PollingError; } { return { validate: (data: unknown): data is T => schema.safeParse(data).success, transformError: (error: unknown): PollingError => ({ code: ERROR_CODES.VALIDATION_ERROR, message: (error as Error).message || 'Validation failed', retryable: false }) }; } /** * Safe parse with error transformation */ export function safeParseWithError( schema: z.ZodSchema, data: unknown ): { success: true; data: T } | { success: false; error: string } { const result = schema.safeParse(data); if (result.success) { return { success: true, data: result.data }; } return { success: false, error: result.error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ') }; } /** * Validate idempotency key format */ export function validateIdempotencyKey(key: string): boolean { // UUID v4 format const uuidV4Pattern = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; return uuidV4Pattern.test(key); } /** * Generate idempotency key */ export function generateIdempotencyKey(): string { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { const r = Math.random() * 16 | 0; const v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } /** * Hash DID for privacy in logs */ export function hashDid(did: string): string { // Simple hash for privacy (use crypto in production) const hash = did.split('').reduce((a, b) => { a = ((a << 5) - a) + b.charCodeAt(0); return a & a; }, 0); return `did:hash:${Math.abs(hash).toString(16)}`; } /** * Redact PII from logs */ export function redactPii(data: unknown): unknown { const redacted = JSON.parse(JSON.stringify(data)); // Redact DID patterns if (typeof redacted === 'string') { return redacted.replace(/did:key:[a-zA-Z0-9]+/g, (match) => hashDid(match)); } if (typeof redacted === 'object' && redacted !== null) { for (const key in redacted) { if (typeof redacted[key] === 'string') { redacted[key] = redacted[key].replace(/did:key:[a-zA-Z0-9]+/g, (match: string) => hashDid(match)); } else if (typeof redacted[key] === 'object') { redacted[key] = redactPii(redacted[key]); } } } return redacted; }