diff --git a/package-lock.json b/package-lock.json index ec2eb147..bd4628dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4538,14 +4538,14 @@ "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.3.28", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -11595,7 +11595,7 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/src/lib/advancedSearch.js b/src/lib/advancedSearch.ts similarity index 100% rename from src/lib/advancedSearch.js rename to src/lib/advancedSearch.ts diff --git a/src/lib/alertRulesDb.js b/src/lib/alertRulesDb.ts similarity index 100% rename from src/lib/alertRulesDb.js rename to src/lib/alertRulesDb.ts diff --git a/src/lib/alerts.js b/src/lib/alerts.ts similarity index 100% rename from src/lib/alerts.js rename to src/lib/alerts.ts diff --git a/src/lib/analytics.js b/src/lib/analytics.ts similarity index 100% rename from src/lib/analytics.js rename to src/lib/analytics.ts diff --git a/src/lib/anchors.js b/src/lib/anchors.ts similarity index 100% rename from src/lib/anchors.js rename to src/lib/anchors.ts diff --git a/src/lib/auditTrail.js b/src/lib/auditTrail.ts similarity index 100% rename from src/lib/auditTrail.js rename to src/lib/auditTrail.ts diff --git a/src/lib/cache.js b/src/lib/cache.ts similarity index 100% rename from src/lib/cache.js rename to src/lib/cache.ts diff --git a/src/lib/chartUtils.js b/src/lib/chartUtils.ts similarity index 100% rename from src/lib/chartUtils.js rename to src/lib/chartUtils.ts diff --git a/src/lib/config.js b/src/lib/config.ts similarity index 100% rename from src/lib/config.js rename to src/lib/config.ts diff --git a/src/lib/contractDevelopment.js b/src/lib/contractDevelopment.ts similarity index 100% rename from src/lib/contractDevelopment.js rename to src/lib/contractDevelopment.ts diff --git a/src/lib/contractInvoker.js b/src/lib/contractInvoker.ts similarity index 100% rename from src/lib/contractInvoker.js rename to src/lib/contractInvoker.ts diff --git a/src/lib/dex.js b/src/lib/dex.ts similarity index 100% rename from src/lib/dex.js rename to src/lib/dex.ts diff --git a/src/lib/encryption.js b/src/lib/encryption.ts similarity index 100% rename from src/lib/encryption.js rename to src/lib/encryption.ts diff --git a/src/lib/errorReporting.js b/src/lib/errorReporting.ts similarity index 100% rename from src/lib/errorReporting.js rename to src/lib/errorReporting.ts diff --git a/src/lib/externalExplorers.js b/src/lib/externalExplorers.ts similarity index 100% rename from src/lib/externalExplorers.js rename to src/lib/externalExplorers.ts diff --git a/src/lib/filters.js b/src/lib/filters.ts similarity index 78% rename from src/lib/filters.js rename to src/lib/filters.ts index e6855780..753b7d67 100644 --- a/src/lib/filters.js +++ b/src/lib/filters.ts @@ -1,9 +1,9 @@ -function toNumber(value, fallback = 0) { - const parsed = Number(value); +function toNumber(value: unknown, fallback = 0) { + const parsed = Number(value as any); return Number.isFinite(parsed) ? parsed : fallback; } -export function applyTransactionFilters(transactions = [], filters = {}) { +export function applyTransactionFilters(transactions: any[] = [], filters: any = {}) { return transactions.filter((tx) => { if (filters.status === "success" && !tx.successful) return false; if (filters.status === "failed" && tx.successful) return false; @@ -22,7 +22,7 @@ export function applyTransactionFilters(transactions = [], filters = {}) { }); } -export function applyOperationFilters(operations = [], filters = {}) { +export function applyOperationFilters(operations: any[] = [], filters: any = {}) { return operations.filter((op) => { if (filters.type && filters.type !== "all" && op.type !== filters.type) { return false; @@ -34,7 +34,7 @@ export function applyOperationFilters(operations = [], filters = {}) { }); } -export function applyAssetFilters(assets = [], filters = {}) { +export function applyAssetFilters(assets: any[] = [], filters: any = {}) { return assets.filter((asset) => { if (filters.verifiedOnly && !asset.is_verified) return false; if (filters.minHolders && toNumber(asset.num_accounts) < toNumber(filters.minHolders)) { diff --git a/src/lib/import.js b/src/lib/import.ts similarity index 100% rename from src/lib/import.js rename to src/lib/import.ts diff --git a/src/lib/multisig.js b/src/lib/multisig.ts similarity index 100% rename from src/lib/multisig.js rename to src/lib/multisig.ts diff --git a/src/lib/networkMonitoring.js b/src/lib/networkMonitoring.ts similarity index 100% rename from src/lib/networkMonitoring.js rename to src/lib/networkMonitoring.ts diff --git a/src/lib/notifications.js b/src/lib/notifications.ts similarity index 100% rename from src/lib/notifications.js rename to src/lib/notifications.ts diff --git a/src/lib/performanceMonitoring.js b/src/lib/performanceMonitoring.ts similarity index 100% rename from src/lib/performanceMonitoring.js rename to src/lib/performanceMonitoring.ts diff --git a/src/lib/portfolioAnalytics.js b/src/lib/portfolioAnalytics.ts similarity index 100% rename from src/lib/portfolioAnalytics.js rename to src/lib/portfolioAnalytics.ts diff --git a/src/lib/priceFeed.js b/src/lib/priceFeed.ts similarity index 100% rename from src/lib/priceFeed.js rename to src/lib/priceFeed.ts diff --git a/src/lib/rateLimiter.js b/src/lib/rateLimiter.ts similarity index 87% rename from src/lib/rateLimiter.js rename to src/lib/rateLimiter.ts index 7a2a78ee..d879cf69 100644 --- a/src/lib/rateLimiter.js +++ b/src/lib/rateLimiter.ts @@ -3,16 +3,79 @@ * Implements token bucket algorithm with intelligent request batching and queue management */ +type RequestPriority = 'high' | 'medium' | 'low'; + +type ThrottleMode = 'aggressive' | 'conservative'; + +interface RequestPayload { + url: string; + options?: RequestInit; + priority?: RequestPriority; + maxRetries?: number; + method?: string; +} + +interface QueuedRequest { + id: string; + request: RequestPayload; + identifier: string; + timestamp: number; + resolve?: (value: Response | PromiseLike) => void; + reject?: (reason?: any) => void; + priority: RequestPriority; + endpoint: string; + retryCount: number; + maxRetries: number; +} + +interface RateLimitBucket { + tokens: number; + lastRefill: number; + endpointUsage: Map; +} + +interface RateLimiterStats { + totalRequests: number; + queuedRequests: number; + batchedRequests: number; + rejectedRequests: number; + droppedRequests: number; + averageResponseTime: number; + endpointUsage: Map; +} + +interface RateLimiterOptions { + windowMs?: number; + maxRequests?: number; + batchSize?: number; + batchTimeout?: number; + throttleMode?: ThrottleMode; + maxQueueSize?: number; +} + class RateLimiter { - constructor(options = {}) { - this.windowMs = options.windowMs || 60000; // Default: 1 minute - this.maxRequests = options.maxRequests || 30; // Default: 30 requests per minute + private windowMs: number; + private maxRequests: number; + private buckets: Map; + private requestQueue: Map; + private batchSize: number; + private batchTimeout: number; + private throttleMode: ThrottleMode; + private maxQueueSize: number; + private priorityQueues: Record; + private cleanupInterval: ReturnType; + private processingInterval: ReturnType; + private statistics: RateLimiterStats; + + constructor(options: RateLimiterOptions = {}) { + this.windowMs = options.windowMs ?? 60000; // Default: 1 minute + this.maxRequests = options.maxRequests ?? 30; // Default: 30 requests per minute this.buckets = new Map(); // Store tokens per user/IP this.requestQueue = new Map(); // Request queues per endpoint type - this.batchSize = options.batchSize || 10; // Max batch size for request batching - this.batchTimeout = options.batchTimeout || 100; // Max wait time for batching (ms) - this.throttleMode = options.throttleMode || 'aggressive'; // 'aggressive' | 'conservative' - this.maxQueueSize = options.maxQueueSize || 100; // Maximum items in queue before dropping + this.batchSize = options.batchSize ?? 10; // Max batch size for request batching + this.batchTimeout = options.batchTimeout ?? 100; // Max wait time for batching (ms) + this.throttleMode = options.throttleMode ?? 'aggressive'; // 'aggressive' | 'conservative' + this.maxQueueSize = options.maxQueueSize ?? 100; // Maximum items in queue before dropping this.priorityQueues = { high: [], medium: [], @@ -45,6 +108,7 @@ class RateLimiter { bucket = { tokens: this.maxRequests - 1, lastRefill: now, + endpointUsage: new Map() }; this.buckets.set(identifier, bucket); diff --git a/src/lib/securityEvents.js b/src/lib/securityEvents.ts similarity index 86% rename from src/lib/securityEvents.js rename to src/lib/securityEvents.ts index 62731e69..d43235f0 100644 --- a/src/lib/securityEvents.js +++ b/src/lib/securityEvents.ts @@ -12,6 +12,27 @@ import { AuditSeverity, subscribeAudit, } from '../utils/audit.js'; +import type { + AuditSeverity as AuditSeverityType, + AuditCategory as AuditCategoryType, + AuditOutcome, +} from '../utils/audit.js'; + +export interface SecurityEventOptions { + actor?: string; + target?: string; + outcome?: AuditOutcome; + metadata?: Record; + severityOverride?: AuditSeverityType; +} + +interface SecurityAlert { + kind: string; + actor: string; + count: number; + windowMs: number; + action?: string; +} // ─── Canonical security event types ────────────────────────────────────────── @@ -91,31 +112,31 @@ function categoryFor(eventType) { // ─── Anomaly detection state ───────────────────────────────────────────────── -const _failedAuthByActor = new Map(); // actor -> timestamps -const _ratelimitHitsByAction = new Map(); // action -> timestamps +const _failedAuthByActor = new Map(); // actor -> timestamps +const _ratelimitHitsByAction = new Map(); // action -> timestamps const FAILED_AUTH_WINDOW_MS = 5 * 60_000; const FAILED_AUTH_THRESHOLD = 5; const RATE_LIMIT_WINDOW_MS = 10 * 60_000; const RATE_LIMIT_THRESHOLD = 10; -const _alertSubscribers = new Set(); +const _alertSubscribers = new Set<(alert: SecurityAlert) => void>(); -export function subscribeSecurityAlerts(handler) { +export function subscribeSecurityAlerts(handler: (alert: SecurityAlert) => void) { _alertSubscribers.add(handler); return () => _alertSubscribers.delete(handler); } -function emitAlert(alert) { +function emitAlert(alert: SecurityAlert) { for (const fn of _alertSubscribers) { try { fn(alert); } catch { /* swallow */ } } } -function pruneOlderThan(arr, cutoff) { +function pruneOlderThan(arr: number[], cutoff: number) { return arr.filter((t) => t >= cutoff); } -function checkAnomalies(eventType, payload) { +function checkAnomalies(eventType: string, payload: { actor?: string; metadata?: Record }) { const now = Date.now(); if (eventType === SecurityEventType.AUTH_LOGIN_FAILED) { @@ -146,7 +167,7 @@ function checkAnomalies(eventType, payload) { } if (eventType === SecurityEventType.RATE_LIMIT_HIT) { - const action = payload.metadata?.action || 'unknown'; + const action = typeof payload.metadata?.action === 'string' ? payload.metadata.action : 'unknown'; const list = pruneOlderThan( _ratelimitHitsByAction.get(action) ?? [], now - RATE_LIMIT_WINDOW_MS, @@ -187,7 +208,7 @@ function checkAnomalies(eventType, payload) { * }} [opts] * @returns {Promise} */ -export function trackSecurityEvent(eventType, opts = {}) { +export function trackSecurityEvent(eventType: string, opts: SecurityEventOptions = {}) { const severity = opts.severityOverride ?? SEVERITY_MAP[eventType] ?? AuditSeverity.INFO; const category = categoryFor(eventType); diff --git a/src/lib/stellar.ts b/src/lib/stellar.ts index 943e85be..bb2beec4 100644 --- a/src/lib/stellar.ts +++ b/src/lib/stellar.ts @@ -2,7 +2,7 @@ import * as StellarSdk from '@stellar/stellar-sdk' import { Cache, TTL } from './cache.js' import { rateLimiter } from './rateLimiter.js' import auditTrail from './auditTrail.js' -import { getCircuitBreaker } from './errorHandling/CircuitBreaker' +import { getCircuitBreaker, type CircuitState } from './errorHandling/CircuitBreaker' // ─── Cache setup ────────────────────────────────────────────────────────────── diff --git a/src/lib/storage.js b/src/lib/storage.ts similarity index 86% rename from src/lib/storage.js rename to src/lib/storage.ts index 886436fd..6533d43b 100644 --- a/src/lib/storage.js +++ b/src/lib/storage.ts @@ -19,18 +19,33 @@ const STORES = { OFFLINE_Q: 'offline-queue', // Queued writes for when back online }; +interface ApiCacheRecord { + key: string; + value: unknown; + expiresAt: number; + tag: string; + cachedAt: number; +} + +interface OfflineOp { + id?: number; + type: string; + payload: unknown; + queuedAt: number; +} + // ─── DB open ────────────────────────────────────────────────────────────────── -let _db = null; +let _db: IDBDatabase | null = null; -function openDB() { +function openDB(): Promise { if (_db) return Promise.resolve(_db); return new Promise((resolve, reject) => { const request = indexedDB.open(DB_NAME, DB_VERSION); request.onupgradeneeded = (event) => { - const db = event.target.result; + const db = (event.target as IDBOpenDBRequest).result; if (!db.objectStoreNames.contains(STORES.APP_STATE)) { db.createObjectStore(STORES.APP_STATE); @@ -66,18 +81,18 @@ function openDB() { // ─── Generic transaction helper ─────────────────────────────────────────────── -async function tx(storeName, mode, fn) { - const db = await openDB(); - const trans = db.transaction(storeName, mode); - const store = trans.objectStore(storeName); - return new Promise((resolve, reject) => { - const req = fn(store); +async function tx(storeName: string, mode: IDBTransactionMode, fn: (store: IDBObjectStore) => IDBRequest | void): Promise { + const db = await openDB(); + const trans = db.transaction(storeName, mode); + const store = trans.objectStore(storeName); + return new Promise((resolve, reject) => { + const req = fn(store) as IDBRequest | void; if (req && typeof req.onsuccess !== 'undefined') { req.onsuccess = () => resolve(req.result); - req.onerror = () => reject(req.error); + req.onerror = () => reject(req.error); } else { trans.oncomplete = () => resolve(); - trans.onerror = () => reject(trans.error); + trans.onerror = () => reject(trans.error); } }); } @@ -127,7 +142,7 @@ export async function clearStorage() { */ export async function getCachedApiResponse(key) { try { - const record = await tx(STORES.API_CACHE, 'readonly', (s) => s.get(key)); + const record = await tx(STORES.API_CACHE, 'readonly', (s) => s.get(key)); if (!record) return null; if (Date.now() > record.expiresAt) { // Expired — delete lazily @@ -173,9 +188,9 @@ export async function invalidateCacheByTag(tag) { await new Promise((resolve, reject) => { req.onsuccess = (e) => { - const cursor = e.target.result; + const cursor = (e.target as IDBRequest).result; if (cursor) { cursor.delete(); cursor.continue(); } - else resolve(); + else resolve(undefined); }; req.onerror = () => reject(req.error); }); @@ -195,9 +210,9 @@ export async function pruneExpiredApiCache() { await new Promise((resolve, reject) => { req.onsuccess = (e) => { - const cursor = e.target.result; + const cursor = (e.target as IDBRequest).result; if (cursor) { cursor.delete(); cursor.continue(); } - else resolve(); + else resolve(undefined); }; req.onerror = () => reject(req.error); }); @@ -210,7 +225,7 @@ export async function pruneExpiredApiCache() { * Add an operation to the offline queue (e.g. a transaction to submit later). * @param {{ type: string, payload: * }} op */ -export async function enqueueOfflineOp(op) { +export async function enqueueOfflineOp(op: Omit) { try { await tx(STORES.OFFLINE_Q, 'readwrite', (s) => s.add({ ...op, queuedAt: Date.now() })); } catch { /* ignore */ } @@ -218,11 +233,12 @@ export async function enqueueOfflineOp(op) { /** * Read all queued offline operations. - * @returns {Promise} + * @returns {Promise>} */ -export async function getOfflineQueue() { +export async function getOfflineQueue(): Promise { try { - return await tx(STORES.OFFLINE_Q, 'readonly', (s) => s.getAll()) ?? []; + const res = await tx(STORES.OFFLINE_Q, 'readonly', (s) => s.getAll()); + return (res as OfflineOp[] | undefined) ?? []; } catch { return []; } } @@ -230,7 +246,7 @@ export async function getOfflineQueue() { * Remove a processed operation from the queue by its auto-increment id. * @param {number} id */ -export async function dequeueOfflineOp(id) { +export async function dequeueOfflineOp(id: number) { try { await tx(STORES.OFFLINE_Q, 'readwrite', (s) => s.delete(id)); } catch { /* ignore */ } diff --git a/src/lib/streaming.js b/src/lib/streaming.ts similarity index 88% rename from src/lib/streaming.js rename to src/lib/streaming.ts index c0803d5e..e7df3ce9 100644 --- a/src/lib/streaming.js +++ b/src/lib/streaming.ts @@ -19,20 +19,22 @@ const MAX_RECONNECT_ATTEMPTS = 10 * any → disconnected (on explicit .disconnect()) */ class StreamManager { + private _closeStream: (() => void) | null = null + private _status: 'disconnected' | 'connecting' | 'connected' | 'reconnecting' | 'error' = 'disconnected' + private _reconnectAttempts = 0 + private _reconnectTimer: ReturnType | null = null + private _network: any | null = null + private _ledgerSubscribers: Set<(ledger: any) => void> = new Set() + private _statusSubscribers: Set<(status: any) => void> = new Set() + constructor() { - /** @type {(() => void) | null} */ this._closeStream = null - /** @type {'disconnected'|'connecting'|'connected'|'reconnecting'|'error'} */ this._status = 'disconnected' this._reconnectAttempts = 0 - /** @type {ReturnType | null} */ this._reconnectTimer = null - /** @type {string | null} */ this._network = null - /** Ledger callbacks */ this._ledgerSubscribers = new Set() - /** Status-change callbacks */ this._statusSubscribers = new Set() } @@ -44,9 +46,9 @@ class StreamManager { * @param {(ledger: object) => void} callback * @returns {() => void} */ - subscribe(callback) { + subscribe(callback: (ledger: any) => void): () => void { this._ledgerSubscribers.add(callback) - return () => this._ledgerSubscribers.delete(callback) + return () => { this._ledgerSubscribers.delete(callback); } } /** @@ -55,9 +57,9 @@ class StreamManager { * @param {(status: string) => void} callback * @returns {() => void} */ - onStatusChange(callback) { + onStatusChange(callback: (status: any) => void): () => void { this._statusSubscribers.add(callback) - return () => this._statusSubscribers.delete(callback) + return () => { this._statusSubscribers.delete(callback); } } /** @returns {'disconnected'|'connecting'|'connected'|'reconnecting'|'error'} */ @@ -70,7 +72,7 @@ class StreamManager { * Disconnects any existing stream first. * @param {string} [network='testnet'] */ - connect(network = 'testnet') { + connect(network: any = 'testnet') { if (this._network !== network && this._closeStream) { this.disconnect() } @@ -109,7 +111,7 @@ class StreamManager { _openStream() { this._setStatus('connecting') try { - const server = getServer(this._network) + const server = getServer(this._network as any) this._closeStream = server .ledgers() .cursor('now') diff --git a/src/lib/tests/auditTrail.test.js b/src/lib/tests/auditTrail.test.ts similarity index 100% rename from src/lib/tests/auditTrail.test.js rename to src/lib/tests/auditTrail.test.ts diff --git a/src/lib/transactionBuilder.js b/src/lib/transactionBuilder.ts similarity index 100% rename from src/lib/transactionBuilder.js rename to src/lib/transactionBuilder.ts diff --git a/src/lib/transactionTemplates.js b/src/lib/transactionTemplates.ts similarity index 100% rename from src/lib/transactionTemplates.js rename to src/lib/transactionTemplates.ts diff --git a/src/lib/transactionVerification.js b/src/lib/transactionVerification.ts similarity index 100% rename from src/lib/transactionVerification.js rename to src/lib/transactionVerification.ts diff --git a/src/lib/tutorialSystem.js b/src/lib/tutorialSystem.ts similarity index 100% rename from src/lib/tutorialSystem.js rename to src/lib/tutorialSystem.ts diff --git a/src/lib/wallet/devices.js b/src/lib/wallet/devices.ts similarity index 100% rename from src/lib/wallet/devices.js rename to src/lib/wallet/devices.ts diff --git a/src/lib/wallet/freighter.js b/src/lib/wallet/freighter.ts similarity index 100% rename from src/lib/wallet/freighter.js rename to src/lib/wallet/freighter.ts diff --git a/src/lib/wallet/ledger.js b/src/lib/wallet/ledger.ts similarity index 100% rename from src/lib/wallet/ledger.js rename to src/lib/wallet/ledger.ts diff --git a/src/lib/wallet/security.js b/src/lib/wallet/security.ts similarity index 100% rename from src/lib/wallet/security.js rename to src/lib/wallet/security.ts diff --git a/src/lib/websocket.js b/src/lib/websocket.ts similarity index 100% rename from src/lib/websocket.js rename to src/lib/websocket.ts diff --git a/src/utils/accessibility.js b/src/utils/accessibility.ts similarity index 100% rename from src/utils/accessibility.js rename to src/utils/accessibility.ts diff --git a/src/utils/analytics.js b/src/utils/analytics.ts similarity index 100% rename from src/utils/analytics.js rename to src/utils/analytics.ts diff --git a/src/utils/audit.js b/src/utils/audit.ts similarity index 84% rename from src/utils/audit.js rename to src/utils/audit.ts index 59d70a94..12904819 100644 --- a/src/utils/audit.js +++ b/src/utils/audit.ts @@ -20,7 +20,9 @@ export const AuditSeverity = Object.freeze({ MEDIUM: 'medium', HIGH: 'high', CRITICAL: 'critical', -}); +}) as const; + +export type AuditSeverity = (typeof AuditSeverity)[keyof typeof AuditSeverity]; export const AuditCategory = Object.freeze({ AUTH: 'auth', @@ -34,18 +36,66 @@ export const AuditCategory = Object.freeze({ SECURITY: 'security', ADMIN: 'admin', SYSTEM: 'system', -}); +}) as const; + +export type AuditCategory = (typeof AuditCategory)[keyof typeof AuditCategory]; + +export type AuditOutcome = 'success' | 'failure' | 'denied'; + +export interface AuditEntry { + id: string; + timestamp: string; + action: string; + category: AuditCategory; + severity: AuditSeverity; + actor: string | null; + target: string | null; + outcome: AuditOutcome; + metadata: Record; + sessionId: string; + url: string | null; + userAgent: string | null; + prevHash: string; + hash: string; +} + +export interface RecordAuditOptions { + action?: string; + category?: AuditCategory; + severity?: AuditSeverity; + actor?: string | null; + target?: string | null; + outcome?: AuditOutcome; + metadata?: Record; +} + +export interface AuditQueryOptions { + category?: AuditCategory; + severity?: AuditSeverity; + actor?: string; + search?: string; + since?: string | number; + until?: string | number; + limit?: number; +} + +export interface AuditStats { + total: number; + bySeverity: Partial>; + byCategory: Partial>; + byOutcome: Record; +} const STORAGE_KEY = 'audit-log'; const MAX_RING_SIZE = 1000; // ─── State ──────────────────────────────────────────────────────────────────── -const _ring = []; +const _ring: AuditEntry[] = []; let _lastHash = '0'; let _sessionId = `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`; let _hydrated = false; -const _subscribers = new Set(); +const _subscribers = new Set<(entry: AuditEntry) => void>(); // ─── Hashing (browser SubtleCrypto with fallback) ───────────────────────────── @@ -74,14 +124,14 @@ const SENSITIVE_FIELD_NAMES = new Set([ 'password', 'passphrase', 'token', 'apiKey', 'authorization', ]); -export function redactSensitive(value) { +export function redactSensitive(value: unknown): unknown { if (value == null) return value; if (typeof value === 'string') { return value.replace(SECRET_KEY_PATTERN, '[REDACTED_SECRET_KEY]'); } if (Array.isArray(value)) return value.map(redactSensitive); if (typeof value === 'object') { - const out = {}; + const out: Record = {}; for (const [k, v] of Object.entries(value)) { if (SENSITIVE_FIELD_NAMES.has(k)) { out[k] = '[REDACTED]'; @@ -102,7 +152,7 @@ async function hydrate() { try { const stored = await getStoredValue(STORAGE_KEY); if (Array.isArray(stored) && stored.length) { - _ring.push(...stored.slice(-MAX_RING_SIZE)); + _ring.push(...(stored.slice(-MAX_RING_SIZE) as AuditEntry[])); _lastHash = _ring[_ring.length - 1]?.hash ?? '0'; } } catch { @@ -124,12 +174,12 @@ hydrate(); // ─── Subscriptions ─────────────────────────────────────────────────────────── -export function subscribeAudit(handler) { +export function subscribeAudit(handler: (entry: AuditEntry) => void) { _subscribers.add(handler); return () => _subscribers.delete(handler); } -function notify(entry) { +function notify(entry: AuditEntry) { for (const fn of _subscribers) { try { fn(entry); } catch { /* swallow subscriber errors */ } } @@ -147,7 +197,7 @@ export async function recordAudit({ target = null, outcome = 'success', metadata = {}, -} = {}) { +}: RecordAuditOptions = {}): Promise { if (!action || typeof action !== 'string') { throw new Error('audit.record requires a string action'); } @@ -162,7 +212,7 @@ export async function recordAudit({ actor, target, outcome, - metadata: redactSensitive(metadata), + metadata: redactSensitive(metadata) as Record, sessionId: _sessionId, url: typeof window !== 'undefined' ? window.location.href : null, userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : null, diff --git a/src/utils/chartUtils.js b/src/utils/chartUtils.ts similarity index 100% rename from src/utils/chartUtils.js rename to src/utils/chartUtils.ts diff --git a/src/utils/collaboration.js b/src/utils/collaboration.ts similarity index 100% rename from src/utils/collaboration.js rename to src/utils/collaboration.ts diff --git a/src/utils/errorHandler.js b/src/utils/errorHandler.ts similarity index 100% rename from src/utils/errorHandler.js rename to src/utils/errorHandler.ts diff --git a/src/utils/export.js b/src/utils/export.ts similarity index 100% rename from src/utils/export.js rename to src/utils/export.ts diff --git a/src/utils/logger.js b/src/utils/logger.ts similarity index 100% rename from src/utils/logger.js rename to src/utils/logger.ts diff --git a/src/utils/monitoring.js b/src/utils/monitoring.ts similarity index 100% rename from src/utils/monitoring.js rename to src/utils/monitoring.ts diff --git a/src/utils/offline.js b/src/utils/offline.ts similarity index 100% rename from src/utils/offline.js rename to src/utils/offline.ts diff --git a/src/utils/preferences.js b/src/utils/preferences.ts similarity index 100% rename from src/utils/preferences.js rename to src/utils/preferences.ts diff --git a/src/utils/search.js b/src/utils/search.ts similarity index 100% rename from src/utils/search.js rename to src/utils/search.ts diff --git a/src/utils/searchUtils.js b/src/utils/searchUtils.ts similarity index 100% rename from src/utils/searchUtils.js rename to src/utils/searchUtils.ts diff --git a/src/utils/security.js b/src/utils/security.ts similarity index 100% rename from src/utils/security.js rename to src/utils/security.ts diff --git a/src/utils/stateSync.js b/src/utils/stateSync.ts similarity index 100% rename from src/utils/stateSync.js rename to src/utils/stateSync.ts diff --git a/tsconfig.json b/tsconfig.json index 69a995da..41f0acd9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -29,6 +29,7 @@ "include": [ "src/**/*.d.ts", "src/lib/**/*.ts", + "src/utils/**/*.ts", "src/hooks/**/*.ts", "src/components/**/*.ts", "src/components/**/*.tsx",