File size: 3,552 Bytes
788eab4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 | // 100% Offline HTTP Client with Automated CSRF Protection and Network Audit Tracker
interface RequestAuditLog {
url: string;
method: string;
timestamp: string;
isExternal: boolean;
status: number;
}
class ApiAuditTracker {
private logs: RequestAuditLog[] = [];
private externalCount = 0;
record(url: string, method: string, status: number) {
const isExternal = !url.startsWith("/") && !url.startsWith("http://127.0.0.1") && !url.startsWith("http://localhost");
if (isExternal) {
this.externalCount++;
}
this.logs.unshift({
url,
method,
timestamp: new Date().toISOString(),
isExternal,
status,
});
if (this.logs.length > 100) {
this.logs.pop();
}
}
getExternalCount(): number {
return this.externalCount;
}
getLogs(): RequestAuditLog[] {
return [...this.logs];
}
clear() {
this.logs = [];
this.externalCount = 0;
}
}
export const auditTracker = new ApiAuditTracker();
let cachedCsrfToken: string | null = null;
export function setCsrfToken(token: string) {
cachedCsrfToken = token;
}
export function getCsrfToken(): string | null {
if (cachedCsrfToken) return cachedCsrfToken;
const match = document.cookie.match(/(?:^|; )csrf_token=([^;]*)/);
return match ? decodeURIComponent(match[1]) : null;
}
export async function request<T>(
url: string,
options: RequestInit = {}
): Promise<T> {
const method = (options.method || "GET").toUpperCase();
const headers = new Headers(options.headers || {});
// Automatic CSRF token injection on mutating verbs
if (["POST", "PUT", "DELETE", "PATCH"].includes(method)) {
const token = getCsrfToken();
if (token && !headers.has("X-CSRF-Token")) {
headers.set("X-CSRF-Token", token);
}
}
const response = await fetch(url, {
...options,
headers,
credentials: "include",
});
auditTracker.record(url, method, response.status);
// Auto-refresh CSRF cookie if returned
const returnedCsrf = response.headers.get("X-CSRF-Token");
if (returnedCsrf) {
cachedCsrfToken = returnedCsrf;
}
if (!response.ok) {
let errorDetail = `Request failed with status ${response.status}`;
try {
const errJson = await response.json();
errorDetail = errJson.detail || JSON.stringify(errJson);
} catch {
errorDetail = await response.text();
}
throw new Error(errorDetail);
}
// Handle empty responses
const contentType = response.headers.get("content-type");
if (contentType && contentType.includes("application/json")) {
return (await response.json()) as T;
}
return (await response.text()) as unknown as T;
}
export const api = {
get: <T>(url: string, signal?: AbortSignal) => request<T>(url, { method: "GET", signal }),
post: <T>(url: string, body?: any, signal?: AbortSignal) => {
const isFormData = body instanceof FormData;
return request<T>(url, {
method: "POST",
headers: isFormData ? {} : { "Content-Type": "application/json" },
body: isFormData ? body : JSON.stringify(body),
signal,
});
},
put: <T>(url: string, body?: any, signal?: AbortSignal) =>
request<T>(url, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
signal,
}),
delete: <T>(url: string, signal?: AbortSignal) => request<T>(url, { method: "DELETE", signal }),
};
|