import crypto from 'crypto'; /** * Generate a unique subscription ID */ export function generateSubscriptionId(): string { return `sub_${crypto.randomBytes(12).toString('base64url')}`; } /** * Generate webhook URL for third-party services */ export function generateWebhookUrl(publicUrl: string, subscriptionId: string): string { const baseUrl = publicUrl.endsWith('/') ? publicUrl.slice(0, -1) : publicUrl; return `${baseUrl}/webhooks/incoming/${subscriptionId}`; } /** * Parse URI template * Example: parseUriTemplate('github://repo/{owner}/{repo}', 'github://repo/octocat/hello') * Returns: { owner: 'octocat', repo: 'hello' } */ export function parseUriTemplate(template: string, uri: string): Record { const templateParts = template.split('/'); const uriParts = uri.split('/'); if (templateParts.length !== uriParts.length) { throw new Error(`URI ${uri} does not match template ${template}`); } const params: Record = {}; for (let i = 0; i < templateParts.length; i++) { const templatePart = templateParts[i]; const uriPart = uriParts[i]; if (templatePart.startsWith('{') && templatePart.endsWith('}')) { const paramName = templatePart.slice(1, -1); params[paramName] = uriPart; } else if (templatePart !== uriPart) { throw new Error(`URI ${uri} does not match template ${template}`); } } return params; } /** * Match URI against template */ export function matchUriTemplate(template: string, uri: string): boolean { try { parseUriTemplate(template, uri); return true; } catch { return false; } } /** * Create HMAC signature */ export function createHmacSignature( payload: any, secret: string, algorithm: 'sha256' | 'sha1' = 'sha256' ): string { const hmac = crypto.createHmac(algorithm, secret); const data = typeof payload === 'string' ? payload : JSON.stringify(payload); hmac.update(data); return `${algorithm}=${hmac.digest('hex')}`; } /** * Verify HMAC signature (timing-safe comparison) */ export function verifyHmacSignature( payload: any, signature: string, secret: string, algorithm: 'sha256' | 'sha1' = 'sha256' ): boolean { try { const expected = createHmacSignature(payload, secret, algorithm); return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)); } catch { return false; } } /** * Sleep utility for retry delays */ export function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * Exponential backoff calculator */ export function calculateBackoff(attempt: number, baseDelay: number): number { return baseDelay * Math.pow(2, attempt); } /** * Sanitize URL for logging (remove sensitive query params) */ export function sanitizeUrl(url: string): string { try { const urlObj = new URL(url); urlObj.searchParams.delete('token'); urlObj.searchParams.delete('secret'); urlObj.searchParams.delete('api_key'); return urlObj.toString(); } catch { return url; } } /** * Extract error message safely */ export function extractErrorMessage(error: unknown): string { if (error instanceof Error) { return error.message; } if (typeof error === 'string') { return error; } return 'Unknown error'; } /** * Validate URL format */ export function isValidUrl(url: string): boolean { try { new URL(url); return true; } catch { return false; } } /** * Validate HTTPS URL */ export function isHttpsUrl(url: string): boolean { try { const urlObj = new URL(url); return urlObj.protocol === 'https:'; } catch { return false; } }