- TypeScript: DeepLinkRouter with URL parsing and handler registration - Swift: BLDeepLinkRouter with iOS URL handling and Logger integration - Kotlin: DeepLinkRouter with Android Uri parsing and handler mapping - Common screen constants: broadcasts, surveys, settings, profile, etc.
215 lines
5.9 KiB
TypeScript
215 lines
5.9 KiB
TypeScript
/**
|
|
* Network interceptor — capture HTTP requests/responses
|
|
*
|
|
* @module network
|
|
*/
|
|
|
|
import type { NetworkRequest } from './types.js';
|
|
|
|
// DOM type declarations for ESLint
|
|
type RequestInfo = string | Request | URL;
|
|
type HeadersInit = Headers | Record<string, string> | string[][];
|
|
|
|
export interface NetworkInterceptorOptions {
|
|
/** URL patterns to include (default: all) */
|
|
includePatterns?: RegExp[];
|
|
/** URL patterns to exclude */
|
|
excludePatterns?: RegExp[];
|
|
/** Max request body size to capture (default: 100KB) */
|
|
maxBodySize?: number;
|
|
/** Whether to capture request headers (default: true) */
|
|
captureRequestHeaders?: boolean;
|
|
/** Whether to capture response headers (default: true) */
|
|
captureResponseHeaders?: boolean;
|
|
/** Sanitize header values matching these patterns */
|
|
sensitiveHeaderPatterns?: RegExp[];
|
|
}
|
|
|
|
/**
|
|
* Interceptor for capturing network requests
|
|
*/
|
|
export class NetworkInterceptor {
|
|
private options: Required<NetworkInterceptorOptions>;
|
|
private originalFetch: typeof fetch;
|
|
private isActive = false;
|
|
private pendingRequests = new Map<string, NetworkRequest>();
|
|
private onRequest: (request: NetworkRequest) => void;
|
|
|
|
constructor(
|
|
onRequest: (request: NetworkRequest) => void,
|
|
options: NetworkInterceptorOptions = {}
|
|
) {
|
|
this.onRequest = onRequest;
|
|
this.options = {
|
|
includePatterns: options.includePatterns ?? [],
|
|
excludePatterns: options.excludePatterns ?? [],
|
|
maxBodySize: options.maxBodySize ?? 100 * 1024,
|
|
captureRequestHeaders: options.captureRequestHeaders ?? true,
|
|
captureResponseHeaders: options.captureResponseHeaders ?? true,
|
|
sensitiveHeaderPatterns: options.sensitiveHeaderPatterns ?? [
|
|
/authorization/i,
|
|
/cookie/i,
|
|
/token/i,
|
|
/api-key/i,
|
|
],
|
|
};
|
|
this.originalFetch = globalThis.fetch.bind(globalThis);
|
|
}
|
|
|
|
/**
|
|
* Start intercepting fetch calls
|
|
*/
|
|
start(): void {
|
|
if (this.isActive) return;
|
|
this.isActive = true;
|
|
|
|
globalThis.fetch = this.interceptedFetch.bind(this);
|
|
}
|
|
|
|
/**
|
|
* Stop intercepting fetch calls
|
|
*/
|
|
stop(): void {
|
|
if (!this.isActive) return;
|
|
this.isActive = false;
|
|
|
|
globalThis.fetch = this.originalFetch;
|
|
}
|
|
|
|
/**
|
|
* Check if URL should be captured
|
|
*/
|
|
private shouldCapture(url: string): boolean {
|
|
// Check excludes first
|
|
for (const pattern of this.options.excludePatterns) {
|
|
if (pattern.test(url)) return false;
|
|
}
|
|
|
|
// If includes specified, must match one
|
|
if (this.options.includePatterns.length > 0) {
|
|
for (const pattern of this.options.includePatterns) {
|
|
if (pattern.test(url)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Sanitize headers
|
|
*/
|
|
private sanitizeHeaders(
|
|
headers: HeadersInit | undefined
|
|
): Record<string, string> {
|
|
const sanitized: Record<string, string> = {};
|
|
const headerEntries = headers instanceof Headers
|
|
? Array.from(headers.entries())
|
|
: typeof headers === 'object' && headers !== null
|
|
? Object.entries(headers)
|
|
: [];
|
|
|
|
for (const [key, value] of headerEntries) {
|
|
const isSensitive = this.options.sensitiveHeaderPatterns.some(p =>
|
|
p.test(key)
|
|
);
|
|
sanitized[key] = isSensitive ? '[REDACTED]' : value;
|
|
}
|
|
|
|
return sanitized;
|
|
}
|
|
|
|
/**
|
|
* Generate request ID
|
|
*/
|
|
private generateId(): string {
|
|
return `${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 9)}`;
|
|
}
|
|
|
|
/**
|
|
* Intercepted fetch implementation
|
|
*/
|
|
private async interceptedFetch(
|
|
input: RequestInfo | URL,
|
|
init?: RequestInit
|
|
): Promise<Response> {
|
|
const url = input.toString();
|
|
const shouldCapture = this.shouldCapture(url);
|
|
|
|
const requestId = this.generateId();
|
|
const startTime = new Date().toISOString();
|
|
|
|
// Create request record if capturing
|
|
if (shouldCapture) {
|
|
const request: NetworkRequest = {
|
|
id: requestId,
|
|
url: url.slice(0, 2048), // Limit URL length
|
|
method: (init?.method ?? 'GET').toUpperCase(),
|
|
requestHeaders: this.options.captureRequestHeaders
|
|
? this.sanitizeHeaders(init?.headers)
|
|
: {},
|
|
startTime,
|
|
};
|
|
|
|
// Capture request body if present and not too large
|
|
if (init?.body && typeof init.body === 'string') {
|
|
if (init.body.length <= this.options.maxBodySize) {
|
|
request.requestBody = init.body.slice(0, this.options.maxBodySize);
|
|
}
|
|
}
|
|
|
|
this.pendingRequests.set(requestId, request);
|
|
}
|
|
|
|
try {
|
|
const response = await this.originalFetch(input, init);
|
|
|
|
// Update with response info if capturing
|
|
if (shouldCapture) {
|
|
const request = this.pendingRequests.get(requestId);
|
|
if (request) {
|
|
request.status = response.status;
|
|
request.endTime = new Date().toISOString();
|
|
request.durationMs = new Date(request.endTime).getTime() - new Date(startTime).getTime();
|
|
|
|
if (this.options.captureResponseHeaders) {
|
|
request.responseHeaders = this.sanitizeHeaders(
|
|
Object.fromEntries(response.headers.entries())
|
|
);
|
|
}
|
|
|
|
// Don't capture response body (too large/complex)
|
|
// Just record that we received a response
|
|
|
|
this.pendingRequests.delete(requestId);
|
|
this.onRequest(request);
|
|
}
|
|
}
|
|
|
|
return response;
|
|
} catch (error) {
|
|
// Record error if capturing
|
|
if (shouldCapture) {
|
|
const request = this.pendingRequests.get(requestId);
|
|
if (request) {
|
|
request.endTime = new Date().toISOString();
|
|
request.durationMs = new Date(request.endTime).getTime() - new Date(startTime).getTime();
|
|
request.error = error instanceof Error ? error.message : String(error);
|
|
|
|
this.pendingRequests.delete(requestId);
|
|
this.onRequest(request);
|
|
}
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if interceptor is active
|
|
*/
|
|
isRunning(): boolean {
|
|
return this.isActive;
|
|
}
|
|
}
|