> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/mercuryworkshop/scramjet/llms.txt
> Use this file to discover all available pages before exploring further.

# Service Worker

> How Scramjet intercepts and proxies requests using Service Workers

The `ScramjetServiceWorker` is the core component that intercepts all network requests and proxies them through the configured transport. This page explains how it works.

## Overview

Service Workers provide a programmable network proxy that runs in the browser. Scramjet leverages this to intercept fetch requests, decode URLs, fetch content, rewrite it, and return the modified response.

<Info>
  Service Workers run in a separate context from your web pages. They persist across page loads and can intercept requests from multiple clients (tabs/windows).
</Info>

## Class structure

The `ScramjetServiceWorker` class is defined in `src/worker/index.ts`:

```typescript theme={null}
export class ScramjetServiceWorker extends EventTarget {
  client: BareClient;              // Transport for making proxied requests
  config: ScramjetConfig;          // Current configuration
  syncPool: Record<number, (val?: any) => void> = {}; // Message queue
  synctoken: number = 0;           // Current sync token
  cookieStore: CookieStore;        // Cookie emulation
  serviceWorkers: FakeServiceWorker[]; // Fake SW registrations
}
```

## Initialization

When the Service Worker starts, it creates a `ScramjetServiceWorker` instance:

```typescript theme={null}
const scramjet = new ScramjetServiceWorker();

self.addEventListener("fetch", async (event) => {
  if (scramjet.route(event)) {
    event.respondWith(scramjet.fetch(event));
  }
});
```

The constructor initializes:

1. **BareClient** for transport
2. **CookieStore** for cookie emulation
3. **Message listeners** for client communication
4. **IndexedDB** to load saved cookies

### Loading configuration

The Service Worker loads its configuration from IndexedDB:

```typescript theme={null}
await scramjet.loadConfig();
```

This is called automatically by `scramjet.fetch()` if the config hasn't been loaded yet. The config contains:

* URL prefix for routing
* Feature flags
* Codec functions for URL encoding/decoding
* File paths for injected scripts

<Note>
  Configuration can be dynamically updated at runtime via messages from the controller. Changes are persisted to IndexedDB.
</Note>

## Request routing

The `route()` method determines if a request should be handled by Scramjet:

```typescript theme={null}
route({ request }: FetchEvent): boolean {
  if (request.url.startsWith(location.origin + this.config.prefix))
    return true;
  else if (request.url.startsWith(location.origin + this.config.files.wasm))
    return true;
  else return false;
}
```

Requests are routed if they:

* Start with the configured prefix (e.g., `/scramjet/`)
* Request the WASM rewriter file

## Request handling

The `fetch()` method orchestrates the entire proxy flow:

```typescript theme={null}
async fetch({ request, clientId }: FetchEvent): Promise<Response> {
  if (!this.config) await this.loadConfig();
  const client = await self.clients.get(clientId);
  return handleFetch.call(this, request, client);
}
```

The actual logic is in `handleFetch()` from `src/worker/fetch.ts`.

## Fetch flow

<Steps>
  <Step title="Parse query parameters">
    Extract metadata from the URL's query string:

    ```typescript theme={null}
    const extraParams: Record<string, string> = {};
    for (const [param, value] of requestUrl.searchParams.entries()) {
      switch (param) {
        case "type":      // Script type (module, classic)
          scriptType = value;
          break;
        case "topFrame":  // Top frame name
          topFrameName = value;
          break;
        case "parentFrame": // Parent frame name
          parentFrameName = value;
          break;
        default:
          extraParams[param] = value; // Form parameters
          break;
      }
      requestUrl.searchParams.delete(param);
    }
    ```
  </Step>

  <Step title="Decode URL">
    Convert the proxied URL back to the real destination:

    ```typescript theme={null}
    const url = new URL(unrewriteUrl(requestUrl));
    // Add back any extra params (e.g., from forms)
    for (const [param, value] of Object.entries(extraParams)) {
      url.searchParams.set(param, value);
    }
    ```
  </Step>

  <Step title="Build metadata context">
    Create the `URLMeta` object for rewriters:

    ```typescript theme={null}
    const meta: URLMeta = {
      origin: url,
      base: url,
      topFrameName,
      parentFrameName,
    };
    ```
  </Step>

  <Step title="Handle special URLs">
    Blob and data URLs are handled directly:

    ```typescript theme={null}
    if (requestUrl.pathname.startsWith(`${this.config.prefix}blob:`) ||
        requestUrl.pathname.startsWith(`${this.config.prefix}data:`)) {
      let dataUrl = requestUrl.pathname.substring(this.config.prefix.length);
      if (dataUrl.startsWith("blob:")) {
        dataUrl = unrewriteBlob(dataUrl);
      }
      const response = await fetch(dataUrl);
      // Rewrite body and return
    }
    ```
  </Step>

  <Step title="Process request headers">
    Rewrite headers for the real request:

    ```typescript theme={null}
    const headers = new ScramjetHeaders();
    for (const [key, value] of request.headers.entries()) {
      headers.set(key, value);
    }

    // Set correct Origin and Referer
    if (client && new URL(client.url).pathname.startsWith(config.prefix)) {
      const clientURL = new URL(unrewriteUrl(client.url));
      headers.set("Referer", clientURL.href);
      headers.set("Origin", clientURL.origin);
    }

    // Add cookies from the cookie store
    const cookies = this.cookieStore.getCookies(url, false);
    if (cookies.length) {
      headers.set("Cookie", cookies);
    }
    ```
  </Step>

  <Step title="Dispatch request event">
    Allow external code to modify the request:

    ```typescript theme={null}
    const ev = new ScramjetRequestEvent(
      url,
      headers.headers,
      request.body,
      request.method,
      request.destination,
      client
    );
    this.dispatchEvent(ev);
    ```
  </Step>

  <Step title="Fetch via transport">
    Use BareClient to make the actual request:

    ```typescript theme={null}
    const response = await this.client.fetch(ev.url, {
      method: ev.method,
      body: ev.body,
      headers: ev.requestHeaders,
      credentials: "omit",
      mode: request.mode === "cors" ? request.mode : "same-origin",
      cache: request.cache,
      redirect: "manual",
      duplex: "half",
    }) as BareResponseFetch;
    ```
  </Step>

  <Step title="Process response">
    Rewrite headers, handle cookies, and rewrite the body based on content type.
  </Step>
</Steps>

## Response handling

The `handleResponse()` function processes the fetched response:

### Header rewriting

```typescript theme={null}
const responseHeaders = await rewriteHeaders(
  response.rawHeaders,
  meta,
  bareClient,
  { get: getReferrerPolicy, set: storeReferrerPolicy }
);
```

This:

* Rewrites `Location` headers for redirects
* Processes `Set-Cookie` headers
* Removes `Permissions-Policy` headers that might block Scramjet features
* Handles CORS headers

### Cookie handling

```typescript theme={null}
const maybeHeaders = responseHeaders["set-cookie"] || [];

// Send cookies to clients
for (const cookie in maybeHeaders) {
  if (client) {
    const promise = swtarget.dispatch(client, {
      scramjet$type: "cookie",
      cookie,
      url: url.href,
    });
  }
}

// Store in the Service Worker's cookie jar
await cookieStore.setCookies(
  maybeHeaders instanceof Array ? maybeHeaders : [maybeHeaders],
  url
);
```

### Body rewriting

Based on the request destination, the response body is rewritten:

```typescript theme={null}
async function rewriteBody(
  response: BareResponseFetch,
  meta: URLMeta,
  destination: RequestDestination,
  workertype: string,
  cookieStore: CookieStore
): Promise<BodyType> {
  switch (destination) {
    case "iframe":
    case "document":
      if (response.headers.get("content-type")?.startsWith("text/html")) {
        return rewriteHtml(await response.text(), cookieStore, meta, true);
      }
      return response.body;
      
    case "script":
      return rewriteJs(
        new Uint8Array(await response.arrayBuffer()),
        response.finalURL,
        meta,
        workertype === "module"
      );
      
    case "style":
      return rewriteCss(await response.text(), meta);
      
    case "sharedworker":
    case "worker":
      return rewriteWorkers(
        new Uint8Array(await response.arrayBuffer()),
        workertype,
        response.finalURL,
        meta
      );
      
    default:
      return response.body;
  }
}
```

<Accordion title="HTML rewriting details">
  HTML rewriting injects Scramjet's client scripts into the `<head>` and rewrites:

  * URL attributes (`src`, `href`, `action`, etc.)
  * Inline `<script>` and `<style>` tags
  * Event handler attributes (`onclick`, etc.)
  * Import maps
  * `<base>` tags
  * Meta refresh tags
  * CSP meta tags (removed)
</Accordion>

<Accordion title="JavaScript rewriting details">
  JavaScript is rewritten using an oxc-based WASM rewriter that:

  * Wraps function calls to intercept APIs
  * Rewrites property accesses
  * Injects runtime helpers
  * Generates source maps for debugging
  * Handles both classic scripts and ES modules
</Accordion>

### Download interception

When the `interceptDownloads` flag is enabled, Scramjet can intercept file downloads:

```typescript theme={null}
if (isDownload(responseHeaders, destination) && !isRedirect(response)) {
  if (flagEnabled("interceptDownloads", url)) {
    const download: ScramjetDownload = {
      filename,
      url: url.href,
      type: responseHeaders["content-type"],
      body: response.body,
      length: Number(length),
    };
    
    // Send to controller client
    clis[0].postMessage({
      scramjet$type: "download",
      download,
    }, [response.body]);
    
    // Prevent the download from completing
    await new Promise(() => {});
  }
}
```

## Message passing

The Service Worker communicates with clients via `postMessage`:

### Receiving messages

```typescript theme={null}
addEventListener("message", async ({ data }: { data: MessageC2W }) => {
  if (!("scramjet$type" in data)) return;

  if ("scramjet$token" in data) {
    // Acknowledge message
    const cb = this.syncPool[data.scramjet$token];
    delete this.syncPool[data.scramjet$token];
    cb(data);
    return;
  }

  if (data.scramjet$type === "registerServiceWorker") {
    this.serviceWorkers.push(new FakeServiceWorker(data.port, data.origin));
  }

  if (data.scramjet$type === "cookie") {
    this.cookieStore.setCookies([data.cookie], new URL(data.url));
    const db = await openDB<ScramjetDB>("$scramjet", 1);
    await db.put("cookies", JSON.parse(this.cookieStore.dump()), "cookies");
  }

  if (data.scramjet$type === "loadConfig") {
    this.config = data.config;
  }
});
```

### Dispatching messages

The `dispatch()` method sends messages and waits for responses:

```typescript theme={null}
async dispatch(client: Client, data: MessageW2C): Promise<MessageC2W> {
  const token = this.synctoken++;
  let cb: (val: MessageC2W) => void;
  const promise: Promise<MessageC2W> = new Promise((r) => (cb = r));
  this.syncPool[token] = cb;
  data.scramjet$token = token;

  client.postMessage(data);

  return await promise;
}
```

## Cookie emulation

Scramjet emulates cookies using the `CookieStore` class because:

1. Service Workers cannot access `document.cookie`
2. Real cookies would leak the proxy origin
3. Cookie attributes (domain, path, etc.) need special handling

Cookies are:

* Stored in the Service Worker's `CookieStore` instance
* Persisted to IndexedDB
* Synced to clients via `postMessage`
* Added to outgoing requests via the `Cookie` header

## Events

The Service Worker dispatches custom events:

### ScramjetRequestEvent

Fired before making a request, allowing modification:

```typescript theme={null}
scramjet.addEventListener("request", (event) => {
  console.log("Requesting:", event.url.href);
  // Modify event.requestHeaders, event.body, etc.
});
```

### ScramjetHandleResponseEvent

Fired after receiving a response:

```typescript theme={null}
scramjet.addEventListener("handleResponse", (event) => {
  console.log("Received:", event.url.href, event.status);
  // Modify event.responseHeaders, event.responseBody, etc.
});
```

## Best practices

<Warning>
  Never modify the Service Worker config directly. Always use the controller's `modifyConfig()` method to ensure changes are persisted.
</Warning>

<Tip>
  Use the `request` event to add custom headers or authentication to proxied requests.
</Tip>

## Next steps

<CardGroup cols={2}>
  <Card title="URL rewriting" icon="link" href="/concepts/url-rewriting">
    Learn how URLs are encoded, decoded, and rewritten
  </Card>

  <Card title="Configuration" icon="sliders" href="/concepts/configuration">
    Explore Service Worker configuration options
  </Card>
</CardGroup>
