DEV Community

Cover image for Inside DotFetch v2.1.0: Auth Boundaries, WebView Security, and Request Architecture
freerave
freerave

Posted on

Inside DotFetch v2.1.0: Auth Boundaries, WebView Security, and Request Architecture

A deep dive into building DotFetch v2.1.0: Modular architecture in VS Code WebViews, RFC 6749 OAuth 2.0, backend-owned Basic Auth with environment variables, and safe credential isolation.


The Context Switching Problem

Context switching is one of the most frustrating parts of building backend services.

Every time you write an API endpoint in VS Code, you switch windows to a standalone API client, wait for it to load, configure environment variables, run the request, and switch back to your editor.

We created DotFetch to bring a lightweight, dark-themed HTTP client directly into your VS Code workspace โ€” native .env variable support, collections, cURL import/export, and zero UI bloat.

In v2.0.0, we built the initial request builder with structured parameter and header tables.

For v2.1.0, our focus shifted to engineering a hardened authentication and security layer designed for real-world API workflows. The challenge was resolving the architectural edge cases that appeared once authentication, persistence, and environment substitution started interacting.

Here is an inside look at how we designed and refactored DotFetch v2.1.0.


๐Ÿ—๏ธ 1. Architecture: Modular Frontend in a Sandboxed WebView

A common problem in VS Code extension development is stuffing DOM manipulation, business logic, and IPC handling into a single monolithic script.

In DotFetch v2.1.0, we structured the frontend into an MVC-inspired modular frontend architecture:

DotFetch Architecture
โ”œโ”€โ”€ Extension Host (Node.js Runtime)
โ”‚   โ”œโ”€โ”€ RequestService.ts       โ† Axios execution engine, SSL Agent, OAuth token fetcher
โ”‚   โ”œโ”€โ”€ DataManager.ts          โ† globalState persistence (History & Collections)
โ”‚   โ”œโ”€โ”€ EnvironmentManager.ts   โ† .env parsing & {{VAR}} substitution
โ”‚   โ””โ”€โ”€ webviewPanel.ts         โ† IPC Message Router & CSP builder
โ”‚
โ””โ”€โ”€ Frontend WebView (Browser Runtime - src/webview/)
    โ”œโ”€โ”€ main.js                 โ† Central Controller & event dispatcher
    โ”œโ”€โ”€ request.js              โ† Form state, live assembly & send logic
    โ”œโ”€โ”€ auth.js                 โ† Auth rendering, live previews & RFC validation
    โ”œโ”€โ”€ ui.js                   โ† Structured tables, response tabs & badges
    โ”œโ”€โ”€ shortcuts.js            โ† Configurable keyboard shortcuts
    โ”œโ”€โ”€ state.js                โ† Single source of truth & canonical schemas
    โ””โ”€โ”€ api.js                  โ† postMessage transport layer
Enter fullscreen mode Exit fullscreen mode

The Bundling Strategy under Strict CSP

DotFetch applies a strict Content-Security-Policy to its VS Code WebView, using nonce-based script execution:

<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'nonce-${nonce}'; ...">
Enter fullscreen mode Exit fullscreen mode

Bundling simplifies WebView resource loading and CSP management while keeping the source code modular during development.

We maintain clean ES modules in src/webview/ during development, and use esbuild to bundle src/webview/main.js into a single IIFE bundle (media/script.js) in ~30ms local build time in our development setup:

"esbuild-base": "esbuild ./src/extension.ts --bundle --outfile=out/extension.js --external:vscode --format=cjs --platform=node && esbuild ./src/webview/main.js --bundle --outfile=media/script.js --format=iife --platform=browser"
Enter fullscreen mode Exit fullscreen mode

This keeps the codebase modular and readable in source control while shipping a single, CSP-compliant script at runtime.


๐Ÿ” 2. The Authentication Suite

๐Ÿ”‘ API Key Authentication (Header vs. Query Mode)

APIs accept keys differently. Some require custom HTTP headers (X-API-Key: secret), while others expect query parameters (?api_key=secret).

We built an interactive UI that lets developers switch modes with real-time visual previews and a copy button:

// src/webview/auth.js
export function updateApiKeyPreview() {
    const preview = document.getElementById('auth-preview');
    if (!preview) return;
    const keyName = state.authConfig.keyName || '';
    const keyValue = state.authConfig.keyValue || '';
    const keyIn = state.authConfig.keyIn || 'header';

    if (keyName && keyValue) {
        const text = keyIn === 'header' ? `${keyName}: ${keyValue}` : `?${keyName}=${keyValue}`;
        preview.innerHTML = `<span>${text}</span><button type="button" class="tool-btn" id="copy-auth-preview">Copy</button>`;
        preview.classList.remove('hidden');
    }
}
Enter fullscreen mode Exit fullscreen mode

API Key auth with real-time masked header preview and instant response inspection.

When sent in Query Parameter mode, the headers table remains untouched and RequestService injects the key into the URL string on the Node.js side right before execution.


โšก OAuth 2.0 (Client Credentials Flow)

Machine-to-machine APIs (Spotify, Twitch, Azure, Auth0) frequently rely on OAuth 2.0 Client Credentials.

Instead of writing separate shell scripts:

  1. Enter your Token URL, Client ID, Client Secret, and Scope.
  2. Click "โšก Fetch & Inject Token".
  3. DotFetch contacts the OAuth provider, extracts the access_token, displays an active token status card with expiration metadata, and injects Authorization: Bearer <token> into your live requests.
// src/requestService.ts - RFC 6749 ยง2.3.1 Compliance
public async fetchOAuthToken(message: any, webview: vscode.Webview) {
    const tokenUrl = this.environmentManager.substituteVariables(message.tokenUrl, env).trim();
    const clientId = this.environmentManager.substituteVariables(message.clientId, env).trim();
    const clientSecret = this.environmentManager.substituteVariables(message.clientSecret, env).trim();
    const scope = this.environmentManager.substituteVariables(message.scope, env).trim();

    const params = new URLSearchParams();
    params.append('grant_type', 'client_credentials');
    if (scope) { params.append('scope', scope); }

    const headers: Record<string, string> = {
        'Content-Type': 'application/x-www-form-urlencoded',
        'Accept': 'application/json'
    };

    // RFC 6749 ยง2.3.1: client_id & client_secret MUST be URL-encoded before Base64 encoding
    if (clientId && clientSecret) {
        const formEncode = (value: string) => encodeURIComponent(value).replace(/%20/g, '+');
        const basicCreds = Buffer.from(
            `${formEncode(clientId)}:${formEncode(clientSecret)}`,
            'utf8'
        ).toString('base64');
        headers['Authorization'] = `Basic ${basicCreds}`;
    }

    const response = await axios.post(tokenUrl, params.toString(), { headers, timeout: 15000 });
    // Returns access_token, token_type, expires_in to WebView...
}
Enter fullscreen mode Exit fullscreen mode

OAuth 2.0 Token Fetching with active token card, expiry tracking, and toast feedback


๐Ÿ›ก๏ธ SSL/TLS Toggle for Local Development

When testing microservices locally with self-signed SSL certificates (https://localhost:8080), Node.js rejects the connection with certificate verification errors.

DotFetch v2.1.0 provides an SSL Toggle with a prominent toolbar warning badge (โš ๏ธ SSL Ignored):

const httpsAgent = message.sslVerify === false
    ? new https.Agent({ rejectUnauthorized: false })
    : undefined;
Enter fullscreen mode Exit fullscreen mode

To prevent accidental security gaps, every request load enforces a secure default (sslVerify: true).

SSL/TLS verification toggle with active toolbar warning badge and self-signed localhost 200 OK response


๐Ÿ› ๏ธ 3. Solving the Hard Architectural Edge Cases

The auth features were straightforward. The difficult part was keeping authentication, persistence, and request execution from contaminating each other.

๐Ÿงฉ 1. The "Draft vs. Wire" Headers Contamination

The Problem: When you send a request with Bearer Auth, the client injects Authorization: Bearer <token> into the outgoing wire headers. If the user clicked "Save to Collection", that injected header was saved into the raw headers table. Later, loading that request and switching to "No Auth" caused the old token to remain in manual headers and still be sent.

The Solution: We decoupled Draft Data (for storage) from Wire Data (for execution):

// src/webview/request.js
export function getRequestData({ forSend = false } = {}) {
    const rawHeaders = serializeHeaders();

    // Wire headers get auth injection; Saved draft headers stay clean
    const headers = forSend ? applyAuthHeaderToRawHeaders(rawHeaders) : rawHeaders;

    // Live send keeps in-memory secrets; Storage strips them
    const auth = forSend ? { ...state.authConfig } : createPersistableAuthConfig();

    return {
        method: document.getElementById('method')?.value || 'GET',
        url: constructFullUrl(),
        headers,
        auth,
        sslVerify: state.settings.sslVerify !== false
    };
}
Enter fullscreen mode Exit fullscreen mode

๐Ÿงฉ 2. Basic Auth + Environment Variables Base64 Deadlock

The Problem: The UI allows Username: {{API_USER}} and Password: {{API_PASS}}.

If the WebView constructs Authorization: Basic ${btoa(${user}:${pass})}, it produces Base64 ciphertext of the literal string {{API_USER}}:{{API_PASS}}. When the Extension Host receives it, it cannot substitute the environment variables because they are locked inside Base64.

The Solution: Make Basic Auth 100% Backend-Owned:

  1. The WebView strips any conflicting manual Authorization header.
  2. RequestService on the Extension Host performs variable substitution first, and then computes the Base64 header in Node.js runtime:
// src/requestService.ts
if (message.auth?.type === 'basic') {
    const rawUser = message.auth.username || '';
    const rawPass = message.auth.password || '';
    const user = this.environmentManager.substituteVariables(rawUser, selectedEnv);
    const pass = this.environmentManager.substituteVariables(rawPass, selectedEnv);
    if (user || pass) {
        const basicCreds = Buffer.from(`${user}:${pass}`, 'utf8').toString('base64');
        substitutedHeaders['Authorization'] = `Basic ${basicCreds}`;
    }
}
Enter fullscreen mode Exit fullscreen mode

๐Ÿงฉ 3. Zero-Persistence Plaintext Secrets across Collections, Favorites & History

The Problem: vscode.ExtensionContext.globalState is used for persisting collections and history. Storing raw passwords, tokens, API key values, or OAuth secrets as plaintext JSON in globalState is an unnecessary security risk.

The Solution: Plaintext runtime credentials are stripped before DotFetch persists request metadata to globalState across Collections, Favorites, and History:

// src/webview/state.js
export function createPersistableAuthConfig(authConfig = state.authConfig) {
    return {
        ...authConfig,
        password: '',
        token: '',
        keyValue: '',
        clientSecret: '',
        accessToken: '',
        expiresIn: null,
        tokenReceivedAt: null
    };
}
Enter fullscreen mode Exit fullscreen mode

When a request executes, sendRequest() passes both requestData (live wire credentials) and historyData (sanitized snapshot). On success, webviewPanel.ts persists only the sanitized snapshot:

// src/webviewPanel.ts
if (capturedResponse) {
    const safeRequest = message.historyData || {};
    const historyEntry: RequestData = {
        name: safeRequest.name,
        url: safeRequest.url,
        headers: safeRequest.headers, // Clean manual headers
        auth: safeRequest.auth,       // Stripped secrets
        status: capturedResponse.status,
        duration: capturedResponse.duration,
        createdAt: new Date().toISOString()
    };
    await this.dataManager.addHistory(historyEntry);
}
Enter fullscreen mode Exit fullscreen mode

๐Ÿงฉ 4. Guarding Against Memory-Exploding Payloads

The Problem: If an API endpoint returns a 50MB JSON payload, transferring that full payload across the VS Code postMessage IPC boundary will freeze the WebView and lag the editor.

The Solution: We implemented double-layered response protection:

  1. Network Layer: Axios enforces maxContentLength: 10 * 1024 * 1024 (10 MB).
  2. Display Layer: If the response exceeds 1 MB (DISPLAY_THRESHOLD), we compute accurate UTF-8 byte length via Buffer.byteLength() and truncate the WebView preview to the first 1000 characters with a helpful badge:
// src/requestService.ts
const serializedResponse = typeof response.data === 'string'
    ? response.data
    : JSON.stringify(response.data ?? '');

const responseSize = Buffer.byteLength(serializedResponse, 'utf8');
const isLarge = responseSize > RequestService.DISPLAY_THRESHOLD;

webview.postMessage({
    type: 'response',
    status: response.status,
    duration,
    size: responseSize,
    headers: response.headers,
    isLarge,
    data: isLarge
        ? `[Response too large: ${(responseSize / 1024).toFixed(2)} KB]\n\nFirst 1000 characters:\n${serializedResponse.substring(0, 1000)}...`
        : response.data
});
Enter fullscreen mode Exit fullscreen mode

๐Ÿงช 4. How We Verified It

Before tagging v2.1.0, we run our quality gates:

  • npm run compile
  • npm run typecheck
  • npm run lint

Then we validate the runtime test matrix manually across scenarios:

  • No Auth: Raw headers sent unchanged; no injected tokens
  • Basic Auth (Literal): Safe UTF-8 Base64 encoding; manual headers overridden
  • Basic Auth (Variables): {{USER}} & {{PASS}} substituted before Base64
  • Bearer Token: Injected on wire only; excluded from saved collections
  • API Key (Header): Custom header injection; live preview works
  • API Key (Query): Injected into URL query string; headers table untouched
  • OAuth 2.0 Flow: RFC 6749 URL encoding; active token card display; expired token blocks send
  • Save & Reload: Schema restored cleanly; zero header contamination
  • History Sanitization: Plaintext secrets omitted from globalState
  • SSL Toggle: Localhost bypasses SSL when disabled with active warning badge
  • Memory Guard: Payloads > 1MB safely truncated in preview
  • Cancel & Retry: AbortController halts execution; network retry with exponential backoff

๐Ÿ“Š Summary of What DotFetch v2.1.0 Delivers

Feature Area What You Get
Authentication Bearer Token, Basic Auth (env-aware), API Key (Header & Query), OAuth 2.0 Client Credentials
UI & Visibility Interactive eye toggles (๐Ÿ‘๏ธ / ๐Ÿ™ˆ), Response Headers Inspector table, Live previews
Security & Privacy Zero-persistence plaintext secret policy across Collections, Favorites & History, SSL Toggle
Performance 10MB payload size limits, 1MB large response truncation, modular WebView source bundled into a single CSP-compatible runtime
Code Health Zero TypeScript typecheck errors, strict ESLint pass, clean IPC separation

๐Ÿš€ Try DotFetch

DotFetch is free, open-source, and built for developers who appreciate clean tools.

Have feedback or feature suggestions for v2.2? Drop a comment below or open an issue on GitHub!

Top comments (0)