baa-conductor

git clone 

baa-conductor / plugins / baa-firefox
im_wower  ·  2026-03-29

page-interceptor.js

  1(function () {
  2  const previousRuntime = window.__baaFirefoxIntercepted__;
  3  const hasManagedRuntime = !!previousRuntime
  4    && typeof previousRuntime === "object"
  5    && typeof previousRuntime.teardown === "function";
  6  const legacyIntercepted = previousRuntime === true
  7    || (hasManagedRuntime && previousRuntime.legacyIntercepted === true);
  8
  9  if (hasManagedRuntime) {
 10    try {
 11      previousRuntime.teardown();
 12    } catch (_) {}
 13  }
 14
 15  const BODY_LIMIT = 5000;
 16  const originalFetch = hasManagedRuntime ? previousRuntime.originalFetch : window.fetch;
 17  const originalXhrOpen = hasManagedRuntime ? previousRuntime.originalXhrOpen : XMLHttpRequest.prototype.open;
 18  const originalXhrSend = hasManagedRuntime ? previousRuntime.originalXhrSend : XMLHttpRequest.prototype.send;
 19  const originalXhrSetRequestHeader = hasManagedRuntime ? previousRuntime.originalXhrSetRequestHeader : XMLHttpRequest.prototype.setRequestHeader;
 20  const activeProxyControllers = new Map();
 21  const cleanupHandlers = [];
 22
 23  function addWindowListener(type, listener) {
 24    window.addEventListener(type, listener);
 25    cleanupHandlers.push(() => {
 26      window.removeEventListener(type, listener);
 27    });
 28  }
 29
 30  function hostnameMatches(hostname, hosts) {
 31    return hosts.some((host) => hostname === host || hostname.endsWith(`.${host}`));
 32  }
 33
 34  function isLikelyStaticPath(pathname = "") {
 35    const lower = pathname.toLowerCase();
 36    return lower.startsWith("/_next/")
 37      || lower.startsWith("/assets/")
 38      || lower.startsWith("/static/")
 39      || lower.startsWith("/images/")
 40      || lower.startsWith("/fonts/")
 41      || lower.startsWith("/favicon")
 42      || /\.(?:js|mjs|css|map|png|jpe?g|gif|svg|webp|ico|woff2?|ttf|otf|mp4|webm|txt)$/i.test(lower);
 43  }
 44
 45  const PLATFORM_RULES = [
 46    {
 47      platform: "claude",
 48      pageHosts: ["claude.ai"],
 49      requestHosts: ["claude.ai"],
 50      matchesPageHost(hostname) {
 51        return hostnameMatches(hostname, this.pageHosts);
 52      },
 53      matchesRequestHost(hostname) {
 54        return hostnameMatches(hostname, this.requestHosts);
 55      },
 56      shouldTrack(pathname) {
 57        return pathname.includes("/api/");
 58      },
 59      isSse(pathname, contentType) {
 60        return contentType.includes("text/event-stream") || pathname.endsWith("/completion");
 61      }
 62    },
 63    {
 64      platform: "chatgpt",
 65      pageHosts: ["chatgpt.com", "chat.openai.com"],
 66      requestHosts: ["chatgpt.com", "chat.openai.com", "openai.com", "oaiusercontent.com"],
 67      matchesPageHost(hostname) {
 68        return hostnameMatches(hostname, this.pageHosts);
 69      },
 70      matchesRequestHost(hostname) {
 71        return hostnameMatches(hostname, this.requestHosts);
 72      },
 73      shouldTrack(pathname) {
 74        const lower = pathname.toLowerCase();
 75        return pathname.includes("/backend-api/")
 76          || pathname.includes("/backend-anon/")
 77          || pathname.includes("/public-api/")
 78          || lower.includes("/conversation")
 79          || lower.includes("/models")
 80          || lower.includes("/files")
 81          || (!isLikelyStaticPath(pathname) && (lower.includes("/api/") || lower.includes("/backend")));
 82      },
 83      isSse(pathname, contentType) {
 84        const lower = pathname.toLowerCase();
 85        return contentType.includes("text/event-stream")
 86          || lower.includes("/conversation")
 87          || lower.includes("/backend-api/conversation");
 88      }
 89    },
 90    {
 91      platform: "gemini",
 92      pageHosts: ["gemini.google.com"],
 93      requestHosts: ["gemini.google.com"],
 94      matchesPageHost(hostname) {
 95        return hostnameMatches(hostname, this.pageHosts);
 96      },
 97      matchesRequestHost(hostname) {
 98        return hostnameMatches(hostname, this.requestHosts);
 99      },
100      shouldTrack(pathname) {
101        const lower = pathname.toLowerCase();
102        return pathname.startsWith("/_/")
103          || pathname.includes("/api/")
104          || lower.includes("bardchatui")
105          || lower.includes("streamgenerate")
106          || lower.includes("generatecontent")
107          || lower.includes("modelresponse")
108          || (!isLikelyStaticPath(pathname) && lower.includes("assistant"));
109      },
110      isSse(pathname, contentType) {
111        return contentType.includes("text/event-stream") || pathname.toLowerCase().includes("streamgenerate");
112      }
113    }
114  ];
115
116  function findPageRule(hostname) {
117    return PLATFORM_RULES.find((rule) => rule.matchesPageHost(hostname)) || null;
118  }
119
120  function findRequestRule(hostname) {
121    return PLATFORM_RULES.find((rule) => rule.matchesRequestHost(hostname)) || null;
122  }
123
124  const pageRule = findPageRule(location.hostname);
125  if (!pageRule) return;
126
127  function getRequestContext(url) {
128    try {
129      const parsed = new URL(url, location.href);
130      const rule = findRequestRule(parsed.hostname);
131      if (!rule || rule.platform !== pageRule.platform) return null;
132      return { parsed, rule };
133    } catch (_) {
134      return null;
135    }
136  }
137
138  function shouldTrack(url) {
139    const context = getRequestContext(url);
140    return context ? context.rule.shouldTrack(context.parsed.pathname) : false;
141  }
142
143  function readHeaders(headersLike) {
144    const out = {};
145    try {
146      const headers = new Headers(headersLike || {});
147      headers.forEach((value, key) => {
148        out[key] = value;
149      });
150    } catch (_) {}
151    return out;
152  }
153
154  function readRawHeaders(rawHeaders) {
155    const out = {};
156    for (const line of String(rawHeaders || "").split(/\r?\n/)) {
157      const index = line.indexOf(":");
158      if (index <= 0) continue;
159      const name = line.slice(0, index).trim().toLowerCase();
160      const value = line.slice(index + 1).trim();
161      if (name) out[name] = value;
162    }
163    return out;
164  }
165
166  function trim(text) {
167    if (text == null) return null;
168    return text.length > BODY_LIMIT ? text.slice(0, BODY_LIMIT) : text;
169  }
170
171  function trimToNull(value) {
172    if (typeof value !== "string") return null;
173    const normalized = value.trim();
174    return normalized ? normalized : null;
175  }
176
177  function describeError(error) {
178    if (typeof error === "string") {
179      return trimToNull(error) || "unknown_error";
180    }
181
182    return trimToNull(error?.message) || String(error || "unknown_error");
183  }
184
185  function emit(type, detail, rule = pageRule) {
186    window.dispatchEvent(new CustomEvent(type, {
187      detail: {
188        platform: rule.platform,
189        ...detail
190      }
191    }));
192  }
193
194  function emitNet(detail, rule = pageRule) {
195    emit("__baa_net__", detail, rule);
196  }
197
198  function emitSse(detail, rule = pageRule) {
199    emit("__baa_sse__", detail, rule);
200  }
201
202  function emitDiagnostic(event, detail = {}, rule = pageRule) {
203    emit("__baa_diagnostic__", {
204      event,
205      ...detail
206    }, rule);
207  }
208
209  function isForbiddenProxyHeader(name) {
210    const lower = String(name || "").toLowerCase();
211    return lower === "accept-encoding"
212      || lower === "connection"
213      || lower === "content-length"
214      || lower === "cookie"
215      || lower === "host"
216      || lower === "origin"
217      || lower === "referer"
218      || lower === "user-agent"
219      || lower.startsWith("sec-");
220  }
221
222  emit("__baa_ready__", {
223    platform: pageRule.platform,
224    url: location.href,
225    source: "page-interceptor"
226  }, pageRule);
227
228  try { console.log("[BAA]", "interceptor_active", pageRule.platform, location.href.slice(0, 120)); } catch (_) {}
229  emitDiagnostic("interceptor_active", {
230    source: "page-interceptor",
231    url: location.href
232  }, pageRule);
233
234  function trimBodyValue(body) {
235    try {
236      if (body == null) return null;
237      if (typeof body === "string") return trim(body);
238      if (body instanceof URLSearchParams) return trim(body.toString());
239      if (body instanceof FormData) {
240        const pairs = [];
241        for (const [key, value] of body.entries()) {
242          pairs.push([key, typeof value === "string" ? value : `[${value?.constructor?.name || "binary"}]`]);
243        }
244        return trim(JSON.stringify(pairs));
245      }
246      if (body instanceof Blob) return `[blob ${body.type || "application/octet-stream"} ${body.size}]`;
247      if (body instanceof ArrayBuffer) return `[arraybuffer ${body.byteLength}]`;
248      if (ArrayBuffer.isView(body)) return `[typedarray ${body.byteLength}]`;
249      return trim(JSON.stringify(body));
250    } catch (_) {
251      return null;
252    }
253  }
254
255  async function readRequestBody(input, init) {
256    try {
257      if (init && Object.prototype.hasOwnProperty.call(init, "body")) {
258        return trimBodyValue(init.body);
259      }
260      if (input instanceof Request && !input.bodyUsed) {
261        return trim(await input.clone().text());
262      }
263    } catch (_) {}
264    return null;
265  }
266
267  async function readResponseText(response) {
268    try {
269      return trim(await response.clone().text());
270    } catch (_) {
271      return null;
272    }
273  }
274
275  async function streamSse(url, method, requestBody, response, startedAt, rule) {
276    try { console.log("[BAA]", "sse_stream_start", method, url.slice(0, 120)); } catch (_) {}
277    emitDiagnostic("sse_stream_start", {
278      method,
279      source: "page-interceptor",
280      url
281    }, rule);
282    let buffer = "";
283    const decoder = new TextDecoder();
284    const emitBufferedChunk = () => {
285      if (!buffer.trim()) return false;
286
287      emitSse({
288        url,
289        method,
290        reqBody: requestBody,
291        chunk: buffer,
292        ts: Date.now()
293      }, rule);
294      buffer = "";
295      return true;
296    };
297
298    try {
299      const clone = response.clone();
300      if (!clone.body) return;
301
302      const reader = clone.body.getReader();
303
304      while (true) {
305        const { done, value } = await reader.read();
306        if (done) break;
307        buffer += decoder.decode(value, { stream: true });
308
309        const chunks = buffer.split("\n\n");
310        buffer = chunks.pop() || "";
311
312        for (const chunk of chunks) {
313          if (!chunk.trim()) continue;
314          emitSse({
315            url,
316            method,
317            reqBody: requestBody,
318            chunk,
319            ts: Date.now()
320          }, rule);
321        }
322      }
323
324      buffer += decoder.decode();
325      emitBufferedChunk();
326
327      const duration = Date.now() - startedAt;
328      try { console.log("[BAA]", "sse_stream_done", method, url.slice(0, 120), "duration=" + duration + "ms"); } catch (_) {}
329      emitDiagnostic("sse_stream_done", {
330        duration,
331        method,
332        source: "page-interceptor",
333        url
334      }, rule);
335      emitSse({
336        url,
337        method,
338        reqBody: requestBody,
339        done: true,
340        ts: Date.now(),
341        duration
342      }, rule);
343    } catch (error) {
344      buffer += decoder.decode();
345      emitBufferedChunk();
346
347      emitSse({
348        url,
349        method,
350        reqBody: requestBody,
351        error: describeError(error),
352        ts: Date.now(),
353        duration: Date.now() - startedAt
354      }, rule);
355    }
356  }
357
358  async function streamProxyResponse(detail, response, startedAt, rule, requestBody) {
359    const contentType = response.headers.get("content-type") || "";
360    const shouldSplitChunks = contentType.includes("text/event-stream");
361    const streamId = detail.stream_id || detail.streamId || detail.id;
362    const proxySource = trimToNull(detail.source) || "proxy";
363    let seq = 0;
364
365    emitSse({
366      id: detail.id,
367      stream_id: streamId,
368      method: detail.method,
369      open: true,
370      reqBody: requestBody,
371      source: proxySource,
372      status: response.status,
373      ts: Date.now(),
374      url: detail.url
375    }, rule);
376
377    try {
378      if (!response.body) {
379        emitSse(
380          response.status >= 400
381            ? {
382                error: `upstream_status_${response.status}`,
383                id: detail.id,
384                method: detail.method,
385                reqBody: requestBody,
386                source: proxySource,
387                status: response.status,
388                stream_id: streamId,
389                ts: Date.now(),
390                url: detail.url
391              }
392            : {
393                id: detail.id,
394                stream_id: streamId,
395                done: true,
396                duration: Date.now() - startedAt,
397                method: detail.method,
398                reqBody: requestBody,
399                source: proxySource,
400                status: response.status,
401                ts: Date.now(),
402                url: detail.url
403              },
404          rule
405        );
406        return;
407      }
408
409      const reader = response.body.getReader();
410      const decoder = new TextDecoder();
411      let buffer = "";
412
413      while (true) {
414        const { done, value } = await reader.read();
415        if (done) break;
416
417        buffer += decoder.decode(value, { stream: true });
418        const chunks = shouldSplitChunks ? buffer.split("\n\n") : [buffer];
419        buffer = shouldSplitChunks ? (chunks.pop() || "") : "";
420
421        for (const chunk of chunks) {
422          if (!chunk.trim()) continue;
423          seq += 1;
424          emitSse({
425            chunk,
426            id: detail.id,
427            method: detail.method,
428            reqBody: requestBody,
429            seq,
430            source: proxySource,
431            status: response.status,
432            stream_id: streamId,
433            ts: Date.now(),
434            url: detail.url
435          }, rule);
436        }
437      }
438
439      buffer += decoder.decode();
440      if (buffer.trim()) {
441        seq += 1;
442        emitSse({
443          chunk: buffer,
444          id: detail.id,
445          method: detail.method,
446          reqBody: requestBody,
447          seq,
448          source: proxySource,
449          status: response.status,
450          stream_id: streamId,
451          ts: Date.now(),
452          url: detail.url
453        }, rule);
454      }
455
456      emitSse(
457        response.status >= 400
458          ? {
459              error: `upstream_status_${response.status}`,
460              id: detail.id,
461              method: detail.method,
462              reqBody: requestBody,
463              seq,
464              source: proxySource,
465              status: response.status,
466              stream_id: streamId,
467              ts: Date.now(),
468              url: detail.url
469            }
470          : {
471              done: true,
472              duration: Date.now() - startedAt,
473              id: detail.id,
474              method: detail.method,
475              reqBody: requestBody,
476              seq,
477              source: proxySource,
478              status: response.status,
479              stream_id: streamId,
480              ts: Date.now(),
481              url: detail.url
482            },
483        rule
484      );
485    } catch (error) {
486      emitSse({
487        error: error.message,
488        id: detail.id,
489        method: detail.method,
490        reqBody: requestBody,
491        source: proxySource,
492        seq,
493        status: response.status,
494        stream_id: streamId,
495        ts: Date.now(),
496        url: detail.url
497      }, rule);
498    }
499  }
500
501  if (!legacyIntercepted) {
502    addWindowListener("__baa_proxy_request__", async (event) => {
503      let detail = event.detail || {};
504      if (typeof detail === "string") {
505        try {
506          detail = JSON.parse(detail);
507        } catch (_) {
508          detail = {};
509        }
510      }
511
512      const id = detail.id;
513      const method = String(detail.method || "GET").toUpperCase();
514      const rawPath = detail.path || detail.url || location.href;
515      const responseMode = String(detail.response_mode || detail.responseMode || "buffered").toLowerCase();
516      const proxySource = trimToNull(detail.source) || "proxy";
517
518      if (!id) return;
519
520      const proxyAbortController = new AbortController();
521      activeProxyControllers.set(id, proxyAbortController);
522
523      try {
524        const url = new URL(rawPath, location.origin).href;
525        try { console.log("[BAA]", "proxy_fetch", method, new URL(url).pathname); } catch (_) {}
526        const context = getRequestContext(url);
527        const headers = new Headers();
528        const startedAt = Date.now();
529
530        for (const [name, value] of Object.entries(detail.headers || {})) {
531          if (!name || value == null || value === "") continue;
532          if (isForbiddenProxyHeader(name)) continue;
533          headers.set(String(name).toLowerCase(), String(value));
534        }
535
536        let body = null;
537        if (method !== "GET" && method !== "HEAD" && Object.prototype.hasOwnProperty.call(detail, "body")) {
538          if (typeof detail.body === "string") {
539            body = detail.body;
540          } else if (detail.body != null) {
541            if (!headers.has("content-type")) headers.set("content-type", "application/json");
542            body = JSON.stringify(detail.body);
543          }
544        }
545
546        const response = await originalFetch.call(window, url, {
547          method,
548          headers,
549          body,
550          credentials: "include",
551          signal: proxyAbortController.signal
552        });
553        const resHeaders = readHeaders(response.headers);
554        const contentType = response.headers.get("content-type") || "";
555        const isSse = context ? context.rule.isSse(context.parsed.pathname, contentType) : false;
556        const reqHeaders = readHeaders(headers);
557        const reqBody = typeof body === "string" ? trim(body) : null;
558
559        if (responseMode === "sse") {
560          emitNet({
561            url,
562            method,
563          reqHeaders,
564          reqBody,
565          status: response.status,
566          resHeaders,
567          resBody: null,
568          duration: Date.now() - startedAt,
569          sse: true,
570          source: proxySource
571        }, pageRule);
572
573          const replayDetail = {
574            ...detail,
575            id,
576            method,
577            stream_id: detail.stream_id || detail.streamId || id,
578            source: proxySource,
579            url
580          };
581          await streamProxyResponse(replayDetail, response, startedAt, pageRule, reqBody);
582          return;
583        }
584
585        const responseBody = await response.text();
586        const trimmedResponseBody = trim(responseBody);
587
588        emitNet({
589          url,
590          method,
591          reqHeaders,
592          reqBody,
593          status: response.status,
594          resHeaders,
595          resBody: isSse && pageRule.platform !== "gemini" ? null : trimmedResponseBody,
596          duration: Date.now() - startedAt,
597          sse: isSse,
598          source: proxySource
599        }, pageRule);
600
601        if (isSse && trimmedResponseBody) {
602          emitSse({
603            url,
604            method,
605            reqBody,
606            chunk: trimmedResponseBody,
607            source: proxySource,
608            ts: Date.now()
609          }, pageRule);
610          emitSse({
611            url,
612            method,
613            reqBody,
614            done: true,
615            source: proxySource,
616            ts: Date.now(),
617            duration: Date.now() - startedAt
618          }, pageRule);
619        }
620
621        emit("__baa_proxy_response__", {
622          id,
623          platform: pageRule.platform,
624          url,
625          method,
626          ok: response.ok,
627          status: response.status,
628          body: responseBody
629        }, pageRule);
630      } catch (error) {
631        emitNet({
632          url: rawPath,
633          method,
634          reqHeaders: readHeaders(detail.headers || {}),
635          reqBody: typeof detail.body === "string" ? trim(detail.body) : trimBodyValue(detail.body),
636          error: error.message,
637          source: proxySource
638        }, pageRule);
639
640        if (responseMode === "sse") {
641          emitSse({
642            error: error.message,
643            id,
644            method,
645            reqBody: typeof detail.body === "string" ? trim(detail.body) : trimBodyValue(detail.body),
646            source: proxySource,
647            status: null,
648            stream_id: detail.stream_id || detail.streamId || id,
649            ts: Date.now(),
650            url: rawPath
651          }, pageRule);
652          return;
653        }
654
655        emit("__baa_proxy_response__", {
656          id,
657          platform: pageRule.platform,
658          url: rawPath,
659          method,
660          ok: false,
661          error: error.message
662        }, pageRule);
663      } finally {
664        activeProxyControllers.delete(id);
665      }
666    });
667
668    addWindowListener("__baa_proxy_cancel__", (event) => {
669      let detail = event.detail || {};
670
671      if (typeof detail === "string") {
672        try {
673          detail = JSON.parse(detail);
674        } catch (_) {
675          detail = {};
676        }
677      }
678
679      const id = detail?.id || detail?.requestId;
680      if (!id) return;
681
682      const controller = activeProxyControllers.get(id);
683      if (!controller) return;
684
685      activeProxyControllers.delete(id);
686      controller.abort(detail?.reason || "browser_request_cancelled");
687    });
688  }
689
690  window.fetch = async function patchedFetch(input, init) {
691    const url = input instanceof Request ? input.url : String(input);
692    const context = getRequestContext(url);
693    if (!context || !context.rule.shouldTrack(context.parsed.pathname)) {
694      return originalFetch.apply(this, arguments);
695    }
696
697    const method = ((init && init.method) || (input instanceof Request ? input.method : "GET")).toUpperCase();
698    const startedAt = Date.now();
699    const reqHeaders = readHeaders(init && init.headers ? init.headers : (input instanceof Request ? input.headers : null));
700    const reqBody = await readRequestBody(input, init);
701    emitDiagnostic("fetch_intercepted", {
702      method,
703      source: "page-interceptor",
704      url
705    }, context.rule);
706
707    try {
708      const response = await originalFetch.apply(this, arguments);
709      const resHeaders = readHeaders(response.headers);
710      const contentType = response.headers.get("content-type") || "";
711      const isSse = context.rule.isSse(context.parsed.pathname, contentType);
712      try { console.log("[BAA]", "fetch", method, context.parsed.pathname, isSse ? "SSE" : "buffered", response.status); } catch (_) {}
713
714      if (isSse) {
715        emitNet({
716          url,
717          method,
718          reqHeaders,
719          reqBody,
720          status: response.status,
721          resHeaders,
722          duration: Date.now() - startedAt,
723          sse: true,
724          source: "page"
725        }, context.rule);
726        streamSse(url, method, reqBody, response, startedAt, context.rule);
727        return response;
728      }
729
730      const resBody = await readResponseText(response);
731      emitNet({
732        url,
733        method,
734        reqHeaders,
735        reqBody,
736        status: response.status,
737        resHeaders,
738        resBody,
739        duration: Date.now() - startedAt,
740        source: "page"
741      }, context.rule);
742      return response;
743    } catch (error) {
744      emitNet({
745        url,
746        method,
747        reqHeaders,
748        reqBody,
749        error: error.message,
750        duration: Date.now() - startedAt,
751        source: "page"
752      }, context.rule);
753      throw error;
754    }
755  };
756
757  XMLHttpRequest.prototype.open = function patchedOpen(method, url) {
758    this.__baaMethod = String(method || "GET").toUpperCase();
759    this.__baaUrl = typeof url === "string" ? url : String(url);
760    this.__baaRequestHeaders = {};
761    return originalXhrOpen.apply(this, arguments);
762  };
763
764  XMLHttpRequest.prototype.setRequestHeader = function patchedSetRequestHeader(name, value) {
765    if (this.__baaRequestHeaders && name) {
766      this.__baaRequestHeaders[String(name).toLowerCase()] = String(value || "");
767    }
768    return originalXhrSetRequestHeader.apply(this, arguments);
769  };
770
771  XMLHttpRequest.prototype.send = function patchedSend(body) {
772    const url = this.__baaUrl;
773    const context = getRequestContext(url);
774    if (!context || !context.rule.shouldTrack(context.parsed.pathname)) {
775      return originalXhrSend.apply(this, arguments);
776    }
777
778    const method = this.__baaMethod || "GET";
779    try { console.log("[BAA]", "xhr", method, context.parsed.pathname); } catch (_) {}
780    const reqBody = trimBodyValue(body);
781    const reqHeaders = { ...(this.__baaRequestHeaders || {}) };
782    const startedAt = Date.now();
783    let finalized = false;
784    let terminalError = null;
785
786    const finalize = () => {
787      if (finalized) return;
788      finalized = true;
789
790      const resHeaders = readRawHeaders(this.getAllResponseHeaders());
791      const contentType = String(this.getResponseHeader("content-type") || "");
792      const duration = Date.now() - startedAt;
793      const error = terminalError || (this.status === 0 ? "xhr_failed" : null);
794      const isSse = context.rule.isSse(context.parsed.pathname, contentType);
795      let responseText = null;
796
797      try {
798        responseText = typeof this.responseText === "string" ? trim(this.responseText) : null;
799      } catch (_) {
800        responseText = null;
801      }
802
803      emitNet({
804        url,
805        method,
806        reqHeaders,
807        reqBody,
808        status: this.status || null,
809        resHeaders,
810        resBody: isSse && context.rule.platform !== "gemini" ? null : responseText,
811        error,
812        duration,
813        sse: isSse,
814        source: "xhr"
815      }, context.rule);
816
817      if (isSse && responseText) {
818        emitSse({
819          url,
820          method,
821          reqBody,
822          chunk: responseText,
823          ts: Date.now()
824        }, context.rule);
825      }
826
827      if (isSse) {
828        emitSse({
829          url,
830          method,
831          reqBody,
832          done: true,
833          ts: Date.now(),
834          duration
835        }, context.rule);
836      }
837    };
838
839    this.addEventListener("error", () => {
840      terminalError = "xhr_error";
841    }, { once: true });
842    this.addEventListener("abort", () => {
843      terminalError = "xhr_aborted";
844    }, { once: true });
845    this.addEventListener("timeout", () => {
846      terminalError = "xhr_timeout";
847    }, { once: true });
848    this.addEventListener("loadend", finalize, { once: true });
849
850    return originalXhrSend.apply(this, arguments);
851  };
852
853  const runtime = {
854    legacyIntercepted,
855    originalFetch,
856    originalXhrOpen,
857    originalXhrSend,
858    originalXhrSetRequestHeader,
859    teardown() {
860      for (const controller of activeProxyControllers.values()) {
861        try {
862          controller.abort("observer_reinject");
863        } catch (_) {}
864      }
865      activeProxyControllers.clear();
866
867      window.fetch = originalFetch;
868      XMLHttpRequest.prototype.open = originalXhrOpen;
869      XMLHttpRequest.prototype.send = originalXhrSend;
870      XMLHttpRequest.prototype.setRequestHeader = originalXhrSetRequestHeader;
871
872      while (cleanupHandlers.length > 0) {
873        const cleanup = cleanupHandlers.pop();
874        try {
875          cleanup();
876        } catch (_) {}
877      }
878
879      if (window.__baaFirefoxIntercepted__ === runtime) {
880        if (legacyIntercepted) {
881          window.__baaFirefoxIntercepted__ = true;
882          return;
883        }
884
885        try {
886          delete window.__baaFirefoxIntercepted__;
887        } catch (_) {
888          window.__baaFirefoxIntercepted__ = null;
889        }
890      }
891    }
892  };
893
894  window.__baaFirefoxIntercepted__ = runtime;
895})();