完成扩展
This commit is contained in:
719
extension/content/modules/FetchInterceptorModule.js
Normal file
719
extension/content/modules/FetchInterceptorModule.js
Normal file
@@ -0,0 +1,719 @@
|
||||
/**
|
||||
* ============================================================================
|
||||
* Fetch Interceptor Module - Network Traffic Spy
|
||||
* ============================================================================
|
||||
* 功能:
|
||||
* 1. Hook window.fetch 和 XMLHttpRequest
|
||||
* 2. 實時分析 Stripe/Adyen/PayPal 等支付網關的響應
|
||||
* 3. 檢測支付狀態 (成功/失敗/3DS 挑戰)
|
||||
* 4. 廣播事件供其他模塊訂閱
|
||||
* 5. 支持請求/響應修改 (用於高級繞過)
|
||||
*
|
||||
* 作者: LO & ENI
|
||||
* 使用方式:
|
||||
* const spy = new FetchInterceptorModule(config);
|
||||
* spy.init();
|
||||
* ============================================================================
|
||||
*/
|
||||
|
||||
class FetchInterceptorModule {
|
||||
constructor(config = {}) {
|
||||
this.config = {
|
||||
enabled: true,
|
||||
debug: false,
|
||||
|
||||
// 監控的網關列表
|
||||
targetGateways: [
|
||||
'stripe.com',
|
||||
'adyen.com',
|
||||
'paypal.com',
|
||||
'checkout.com',
|
||||
'gog.com',
|
||||
'braintreegateway.com'
|
||||
],
|
||||
|
||||
// 響應分析規則
|
||||
analyzeRules: {
|
||||
stripe: {
|
||||
successIndicators: ['succeeded', 'paid'],
|
||||
failureIndicators: ['error', 'declined'],
|
||||
challengeIndicators: ['requires_action', 'requires_source_action']
|
||||
},
|
||||
adyen: {
|
||||
successIndicators: ['Authorised'],
|
||||
failureIndicators: ['Refused', 'Error'],
|
||||
challengeIndicators: ['threeDS2', 'redirect']
|
||||
}
|
||||
},
|
||||
|
||||
// 請求修改 Hook (高級用途)
|
||||
requestModifier: null, // function(url, options) => options
|
||||
responseModifier: null, // function(url, response) => response
|
||||
|
||||
// 日誌保存
|
||||
logRequests: true,
|
||||
maxLogEntries: 100,
|
||||
|
||||
...config
|
||||
};
|
||||
|
||||
// 內部狀態
|
||||
this.originalFetch = null;
|
||||
this.originalXHROpen = null;
|
||||
this.originalXHRSend = null;
|
||||
this.interceptedRequests = [];
|
||||
this.listeners = new Map();
|
||||
|
||||
// 綁定方法
|
||||
this.handleFetch = this.handleFetch.bind(this);
|
||||
this.handleXHRLoad = this.handleXHRLoad.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化攔截器
|
||||
*/
|
||||
init() {
|
||||
// 防止重複加載
|
||||
if (window.__FETCH_INTERCEPTOR_LOADED__) {
|
||||
this.warn('Interceptor already loaded. Skipping.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.log('Initializing Network Interceptor...');
|
||||
|
||||
// 1. Hook Fetch API
|
||||
this.hookFetch();
|
||||
|
||||
// 2. Hook XMLHttpRequest
|
||||
this.hookXHR();
|
||||
|
||||
// 3. 設置消息監聽
|
||||
this.setupMessageListener();
|
||||
|
||||
// 4. 暴露全局 API
|
||||
this.exposeGlobalAPI();
|
||||
|
||||
// 5. 標記已加載
|
||||
window.__FETCH_INTERCEPTOR_LOADED__ = true;
|
||||
|
||||
this.success('Network Interceptor Active. The Spy is Listening.');
|
||||
}
|
||||
|
||||
/**
|
||||
* 銷毀攔截器
|
||||
*/
|
||||
destroy() {
|
||||
if (this.originalFetch) {
|
||||
window.fetch = this.originalFetch;
|
||||
}
|
||||
if (this.originalXHROpen) {
|
||||
XMLHttpRequest.prototype.open = this.originalXHROpen;
|
||||
}
|
||||
if (this.originalXHRSend) {
|
||||
XMLHttpRequest.prototype.send = this.originalXHRSend;
|
||||
}
|
||||
|
||||
window.__FETCH_INTERCEPTOR_LOADED__ = false;
|
||||
this.log('Interceptor destroyed');
|
||||
}
|
||||
|
||||
/**
|
||||
* ========================================================================
|
||||
* Fetch API Hook
|
||||
* ========================================================================
|
||||
*/
|
||||
|
||||
hookFetch() {
|
||||
if (this.originalFetch) return;
|
||||
|
||||
this.originalFetch = window.fetch;
|
||||
const self = this;
|
||||
|
||||
window.fetch = async function(...args) {
|
||||
return await self.handleFetch(this, args, self.originalFetch);
|
||||
};
|
||||
|
||||
this.log('Fetch API hooked');
|
||||
}
|
||||
|
||||
async handleFetch(context, args, originalFetch) {
|
||||
const [resource, config = {}] = args;
|
||||
const url = this.extractUrl(resource);
|
||||
const method = config.method || 'GET';
|
||||
|
||||
// 記錄請求
|
||||
const requestId = this.logRequest('fetch', method, url, config.body);
|
||||
|
||||
try {
|
||||
// 如果配置了請求修改器,應用它
|
||||
let modifiedConfig = config;
|
||||
if (this.config.requestModifier && this.isTargetUrl(url)) {
|
||||
modifiedConfig = await this.config.requestModifier(url, config);
|
||||
this.log('Request modified by custom hook');
|
||||
}
|
||||
|
||||
// 執行原始請求
|
||||
const response = await originalFetch.apply(context, [resource, modifiedConfig]);
|
||||
|
||||
// 克隆響應以供分析 (response.body 只能讀取一次)
|
||||
const clonedResponse = response.clone();
|
||||
|
||||
// 異步分析響應
|
||||
this.analyzeResponse(url, clonedResponse, requestId).catch(err => {
|
||||
this.error('Response analysis failed:', err);
|
||||
});
|
||||
|
||||
// 如果配置了響應修改器
|
||||
if (this.config.responseModifier && this.isTargetUrl(url)) {
|
||||
return await this.config.responseModifier(url, response);
|
||||
}
|
||||
|
||||
return response;
|
||||
|
||||
} catch (error) {
|
||||
this.error('Fetch error:', error);
|
||||
this.updateRequestLog(requestId, { error: error.message });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ========================================================================
|
||||
* XMLHttpRequest Hook
|
||||
* ========================================================================
|
||||
*/
|
||||
|
||||
hookXHR() {
|
||||
if (this.originalXHROpen) return;
|
||||
|
||||
const self = this;
|
||||
const XHR = XMLHttpRequest.prototype;
|
||||
|
||||
this.originalXHROpen = XHR.open;
|
||||
this.originalXHRSend = XHR.send;
|
||||
|
||||
// Hook open() 以捕獲 URL
|
||||
XHR.open = function(method, url, ...rest) {
|
||||
this.__interceptor_url = url;
|
||||
this.__interceptor_method = method;
|
||||
return self.originalXHROpen.apply(this, [method, url, ...rest]);
|
||||
};
|
||||
|
||||
// Hook send() 以捕獲請求體和響應
|
||||
XHR.send = function(body) {
|
||||
const xhr = this;
|
||||
const url = xhr.__interceptor_url;
|
||||
const method = xhr.__interceptor_method;
|
||||
|
||||
// 記錄請求
|
||||
const requestId = self.logRequest('xhr', method, url, body);
|
||||
|
||||
// 監聽加載完成
|
||||
xhr.addEventListener('load', function() {
|
||||
self.handleXHRLoad(xhr, url, requestId);
|
||||
});
|
||||
|
||||
// 監聽錯誤
|
||||
xhr.addEventListener('error', function() {
|
||||
self.updateRequestLog(requestId, { error: 'XHR request failed' });
|
||||
});
|
||||
|
||||
return self.originalXHRSend.apply(this, [body]);
|
||||
};
|
||||
|
||||
this.log('XMLHttpRequest hooked');
|
||||
}
|
||||
|
||||
handleXHRLoad(xhr, url, requestId) {
|
||||
try {
|
||||
const responseText = xhr.responseText;
|
||||
const status = xhr.status;
|
||||
|
||||
// 更新日誌
|
||||
this.updateRequestLog(requestId, {
|
||||
status: status,
|
||||
responseSize: responseText ? responseText.length : 0
|
||||
});
|
||||
|
||||
// 分析響應
|
||||
if (this.isTargetUrl(url)) {
|
||||
this.analyzeResponseText(url, responseText, status);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.error('XHR load handler error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ========================================================================
|
||||
* 響應分析引擎
|
||||
* ========================================================================
|
||||
*/
|
||||
|
||||
async analyzeResponse(url, response, requestId) {
|
||||
if (!this.isTargetUrl(url)) return;
|
||||
|
||||
try {
|
||||
const contentType = response.headers.get('content-type');
|
||||
|
||||
if (contentType && contentType.includes('application/json')) {
|
||||
const text = await response.text();
|
||||
this.analyzeResponseText(url, text, response.status);
|
||||
|
||||
// 更新日誌
|
||||
this.updateRequestLog(requestId, {
|
||||
status: response.status,
|
||||
analyzed: true
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.error('Response analysis error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
analyzeResponseText(url, text, status) {
|
||||
if (!text) return;
|
||||
|
||||
try {
|
||||
const data = JSON.parse(text);
|
||||
const urlLower = url.toLowerCase();
|
||||
|
||||
// Stripe 分析
|
||||
if (urlLower.includes('stripe.com') || urlLower.includes('payment_intent') || urlLower.includes('charges')) {
|
||||
this.analyzeStripeResponse(url, data, status);
|
||||
}
|
||||
|
||||
// Adyen 分析
|
||||
if (urlLower.includes('adyen.com') || urlLower.includes('checkout')) {
|
||||
this.analyzeAdyenResponse(url, data, status);
|
||||
}
|
||||
|
||||
// PayPal 分析
|
||||
if (urlLower.includes('paypal.com')) {
|
||||
this.analyzePayPalResponse(url, data, status);
|
||||
}
|
||||
|
||||
// 通用分析 (基於關鍵字)
|
||||
this.analyzeGenericResponse(url, data, status);
|
||||
|
||||
} catch (error) {
|
||||
// 不是 JSON,忽略
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stripe 專用分析器
|
||||
*/
|
||||
analyzeStripeResponse(url, data, status) {
|
||||
this.log('Analyzing Stripe response...');
|
||||
|
||||
// 成功
|
||||
if (data.status === 'succeeded' || data.paid === true || data.status === 'paid') {
|
||||
this.success('💳 Stripe Payment SUCCEEDED');
|
||||
this.broadcastEvent('PAYMENT_SUCCESS', {
|
||||
gateway: 'stripe',
|
||||
url: url,
|
||||
details: {
|
||||
id: data.id,
|
||||
amount: data.amount,
|
||||
currency: data.currency,
|
||||
status: data.status
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 需要驗證 (3DS Challenge)
|
||||
else if (data.status === 'requires_action' || data.status === 'requires_source_action') {
|
||||
this.warn('🔐 Stripe 3D Secure Challenge Detected');
|
||||
|
||||
const redirectUrl = data.next_action?.redirect_to_url?.url ||
|
||||
data.next_action?.use_stripe_sdk?.stripe_js;
|
||||
|
||||
this.broadcastEvent('3DS_CHALLENGE', {
|
||||
gateway: 'stripe',
|
||||
url: url,
|
||||
redirectUrl: redirectUrl,
|
||||
challengeType: data.next_action?.type
|
||||
});
|
||||
}
|
||||
|
||||
// 失敗
|
||||
else if (data.error || data.status === 'failed') {
|
||||
const errorCode = data.error?.code || data.error?.decline_code || 'unknown';
|
||||
const errorMessage = data.error?.message || 'Payment failed';
|
||||
|
||||
this.warn(`❌ Stripe Payment FAILED: ${errorCode}`);
|
||||
this.broadcastEvent('PAYMENT_FAILED', {
|
||||
gateway: 'stripe',
|
||||
url: url,
|
||||
code: errorCode,
|
||||
message: errorMessage,
|
||||
declineCode: data.error?.decline_code
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adyen 專用分析器
|
||||
*/
|
||||
analyzeAdyenResponse(url, data, status) {
|
||||
this.log('Analyzing Adyen response...');
|
||||
|
||||
// 成功
|
||||
if (data.resultCode === 'Authorised') {
|
||||
this.success('💳 Adyen Payment AUTHORISED');
|
||||
this.broadcastEvent('PAYMENT_SUCCESS', {
|
||||
gateway: 'adyen',
|
||||
url: url,
|
||||
details: data
|
||||
});
|
||||
}
|
||||
|
||||
// 3DS 挑戰
|
||||
else if (data.action && (data.action.type === 'threeDS2' || data.action.type === 'redirect')) {
|
||||
this.warn('🔐 Adyen 3DS Challenge Detected');
|
||||
this.broadcastEvent('3DS_CHALLENGE', {
|
||||
gateway: 'adyen',
|
||||
url: url,
|
||||
action: data.action
|
||||
});
|
||||
}
|
||||
|
||||
// 失敗
|
||||
else if (data.resultCode === 'Refused' || data.resultCode === 'Error') {
|
||||
this.warn(`❌ Adyen Payment REFUSED: ${data.refusalReason || 'unknown'}`);
|
||||
this.broadcastEvent('PAYMENT_FAILED', {
|
||||
gateway: 'adyen',
|
||||
url: url,
|
||||
code: data.refusalReason,
|
||||
resultCode: data.resultCode
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PayPal 專用分析器
|
||||
*/
|
||||
analyzePayPalResponse(url, data, status) {
|
||||
if (data.status === 'COMPLETED' || data.state === 'approved') {
|
||||
this.success('💳 PayPal Payment COMPLETED');
|
||||
this.broadcastEvent('PAYMENT_SUCCESS', {
|
||||
gateway: 'paypal',
|
||||
url: url,
|
||||
details: data
|
||||
});
|
||||
} else if (data.name === 'INSTRUMENT_DECLINED') {
|
||||
this.warn('❌ PayPal Payment DECLINED');
|
||||
this.broadcastEvent('PAYMENT_FAILED', {
|
||||
gateway: 'paypal',
|
||||
url: url,
|
||||
code: data.name
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用分析器 (關鍵字匹配)
|
||||
*/
|
||||
analyzeGenericResponse(url, data, status) {
|
||||
const dataStr = JSON.stringify(data).toLowerCase();
|
||||
|
||||
// 成功關鍵字
|
||||
const successKeywords = ['success', 'approved', 'authorized', 'completed', 'paid'];
|
||||
if (successKeywords.some(kw => dataStr.includes(kw))) {
|
||||
this.success('💳 Generic Payment Success Detected');
|
||||
this.broadcastEvent('PAYMENT_SUCCESS', {
|
||||
gateway: 'generic',
|
||||
url: url
|
||||
});
|
||||
}
|
||||
|
||||
// 失敗關鍵字
|
||||
const failureKeywords = ['declined', 'rejected', 'failed', 'refused', 'insufficient'];
|
||||
if (failureKeywords.some(kw => dataStr.includes(kw))) {
|
||||
this.warn('❌ Generic Payment Failure Detected');
|
||||
this.broadcastEvent('PAYMENT_FAILED', {
|
||||
gateway: 'generic',
|
||||
url: url
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ========================================================================
|
||||
* 事件廣播系統
|
||||
* ========================================================================
|
||||
*/
|
||||
|
||||
broadcastEvent(eventType, payload) {
|
||||
const message = {
|
||||
type: `NETWORK_SPY_${eventType}`,
|
||||
...payload,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
// postMessage 廣播
|
||||
window.postMessage(message, '*');
|
||||
|
||||
// CustomEvent 廣播
|
||||
const event = new CustomEvent('NETWORK_SPY_EVENT', {
|
||||
detail: message
|
||||
});
|
||||
window.dispatchEvent(event);
|
||||
|
||||
// 調用內部監聽器
|
||||
if (this.listeners.has(eventType)) {
|
||||
this.listeners.get(eventType).forEach(callback => {
|
||||
try {
|
||||
callback(payload);
|
||||
} catch (error) {
|
||||
this.error('Listener callback error:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.log(`Event broadcasted: ${eventType}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加事件監聽器
|
||||
*/
|
||||
on(eventType, callback) {
|
||||
if (!this.listeners.has(eventType)) {
|
||||
this.listeners.set(eventType, []);
|
||||
}
|
||||
this.listeners.get(eventType).push(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* ========================================================================
|
||||
* 請求日誌系統
|
||||
* ========================================================================
|
||||
*/
|
||||
|
||||
logRequest(type, method, url, body) {
|
||||
if (!this.config.logRequests) return null;
|
||||
|
||||
const requestId = `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
const entry = {
|
||||
id: requestId,
|
||||
type: type,
|
||||
method: method,
|
||||
url: url,
|
||||
bodySize: body ? (typeof body === 'string' ? body.length : 'N/A') : 0,
|
||||
timestamp: Date.now(),
|
||||
status: null,
|
||||
analyzed: false
|
||||
};
|
||||
|
||||
this.interceptedRequests.push(entry);
|
||||
|
||||
// 限制日誌大小
|
||||
if (this.interceptedRequests.length > this.config.maxLogEntries) {
|
||||
this.interceptedRequests.shift();
|
||||
}
|
||||
|
||||
return requestId;
|
||||
}
|
||||
|
||||
updateRequestLog(requestId, updates) {
|
||||
if (!requestId) return;
|
||||
|
||||
const entry = this.interceptedRequests.find(e => e.id === requestId);
|
||||
if (entry) {
|
||||
Object.assign(entry, updates);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 獲取請求日誌
|
||||
*/
|
||||
getRequestLog(filter = {}) {
|
||||
let logs = [...this.interceptedRequests];
|
||||
|
||||
if (filter.gateway) {
|
||||
logs = logs.filter(log => log.url.includes(filter.gateway));
|
||||
}
|
||||
|
||||
if (filter.method) {
|
||||
logs = logs.filter(log => log.method === filter.method);
|
||||
}
|
||||
|
||||
if (filter.analyzed !== undefined) {
|
||||
logs = logs.filter(log => log.analyzed === filter.analyzed);
|
||||
}
|
||||
|
||||
return logs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空日誌
|
||||
*/
|
||||
clearLog() {
|
||||
this.interceptedRequests = [];
|
||||
this.log('Request log cleared');
|
||||
}
|
||||
|
||||
/**
|
||||
* ========================================================================
|
||||
* 工具方法
|
||||
* ========================================================================
|
||||
*/
|
||||
|
||||
extractUrl(resource) {
|
||||
if (typeof resource === 'string') {
|
||||
return resource;
|
||||
} else if (resource instanceof Request) {
|
||||
return resource.url;
|
||||
} else if (resource instanceof URL) {
|
||||
return resource.toString();
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
isTargetUrl(url) {
|
||||
if (!url) return false;
|
||||
const urlLower = url.toLowerCase();
|
||||
return this.config.targetGateways.some(gateway =>
|
||||
urlLower.includes(gateway.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* ========================================================================
|
||||
* 消息監聽
|
||||
* ========================================================================
|
||||
*/
|
||||
|
||||
setupMessageListener() {
|
||||
window.addEventListener('message', (event) => {
|
||||
const data = event.data;
|
||||
|
||||
if (data?.type === 'INTERCEPTOR_UPDATE_CONFIG') {
|
||||
this.updateConfig(data.config);
|
||||
}
|
||||
|
||||
if (data?.type === 'INTERCEPTOR_GET_LOGS') {
|
||||
window.postMessage({
|
||||
type: 'INTERCEPTOR_LOGS_RESPONSE',
|
||||
logs: this.interceptedRequests
|
||||
}, '*');
|
||||
}
|
||||
|
||||
if (data?.type === 'INTERCEPTOR_CLEAR_LOGS') {
|
||||
this.clearLog();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新配置
|
||||
*/
|
||||
updateConfig(newConfig) {
|
||||
Object.assign(this.config, newConfig);
|
||||
this.log('Config updated:', this.config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 獲取狀態
|
||||
*/
|
||||
getStatus() {
|
||||
return {
|
||||
enabled: this.config.enabled,
|
||||
totalRequests: this.interceptedRequests.length,
|
||||
targetGateways: this.config.targetGateways,
|
||||
listeners: Array.from(this.listeners.keys())
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* ========================================================================
|
||||
* 全局 API
|
||||
* ========================================================================
|
||||
*/
|
||||
|
||||
exposeGlobalAPI() {
|
||||
window.networkSpy = {
|
||||
module: this,
|
||||
on: (event, callback) => this.on(event, callback),
|
||||
getLogs: (filter) => this.getRequestLog(filter),
|
||||
clearLogs: () => this.clearLog(),
|
||||
getStatus: () => this.getStatus(),
|
||||
updateConfig: (cfg) => this.updateConfig(cfg)
|
||||
};
|
||||
|
||||
this.log('Global API exposed as window.networkSpy');
|
||||
}
|
||||
|
||||
/**
|
||||
* ========================================================================
|
||||
* 日誌工具
|
||||
* ========================================================================
|
||||
*/
|
||||
|
||||
log(...args) {
|
||||
if (this.config.debug) {
|
||||
console.log('[NetworkSpy]', ...args);
|
||||
}
|
||||
}
|
||||
|
||||
success(msg) {
|
||||
console.log(`%c[NetworkSpy SUCCESS] ${msg}`, 'color: green; font-weight: bold;');
|
||||
}
|
||||
|
||||
warn(msg) {
|
||||
console.log(`%c[NetworkSpy WARN] ${msg}`, 'color: orange; font-weight: bold;');
|
||||
}
|
||||
|
||||
error(...args) {
|
||||
console.error('[NetworkSpy ERROR]', ...args);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ============================================================================
|
||||
* 全局暴露與自動初始化
|
||||
* ============================================================================
|
||||
*/
|
||||
window.FetchInterceptorModule = FetchInterceptorModule;
|
||||
|
||||
// 自動初始化選項
|
||||
if (typeof INTERCEPTOR_AUTO_INIT !== 'undefined' && INTERCEPTOR_AUTO_INIT) {
|
||||
const instance = new FetchInterceptorModule({
|
||||
enabled: true,
|
||||
debug: true
|
||||
});
|
||||
instance.init();
|
||||
window.__networkSpy = instance;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// Extension Compatibility Wrapper
|
||||
// ============================================================================
|
||||
window.addEventListener('message', (event) => {
|
||||
if (event.source !== window) return;
|
||||
const message = event.data;
|
||||
|
||||
if (message && message.type === 'INIT_MODULE' && message.moduleName === 'fetchSpy') {
|
||||
if (window.__fetchSpyInstance) return;
|
||||
try {
|
||||
const instance = new FetchInterceptorModule(message.config);
|
||||
instance.init();
|
||||
window.__fetchSpyInstance = instance;
|
||||
console.log('[Extension] Fetch Spy initialized');
|
||||
} catch (error) {
|
||||
console.error('[Extension] Fetch Spy init failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
if (message && message.type === 'DESTROY_MODULE' && message.instanceKey === '__fetchSpyInstance') {
|
||||
const instance = window.__fetchSpyInstance;
|
||||
if (instance && typeof instance.destroy === 'function') {
|
||||
instance.destroy();
|
||||
delete window.__fetchSpyInstance;
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user