Chrome Extension Master Launch & Interception Blueprint

Complete Architectural Blueprint: From Discovering Browser Redirect Malware to Building a Production Manifest V3 Extension and Publishing on the Chrome Web Store - The TUFHub Case Study by Mohit Arora

📋 Progress:
0 / 50 done
📍 Master Navigation Index

🦠 Step 0: Problem Origin Story - How Browser Redirect Malware Inspired TUFHub

This entire extension was born from a browser malware infection. Here is the exact sequence of events:

Core Technical Takeaway: The "world": "MAIN" content script declaration in Manifest V3 gives an extension the ability to monkey-patch window.fetch before the page's own JavaScript runs. This is the exact technique malware exploits - and the exact technique TUFHub uses legitimately.

🗂️ Section A: Full Project File Tree & Architecture Map

TUFHub/ ├── manifest.json MV3 entry point - minimal permissions ├── webpack.config.js ES module bundler - multi-entry to dist/ ├── package.json ├── scripts/ │ ├── background.js Service worker: OAuth + badge + message router │ ├── uploader.js GitHub REST API PUT + SHA conflict retry + README indexer │ └── tuf/ │ ├── interceptor.js MAIN world: window.fetch monkey-patch │ └── content.js ISOLATED world: DOM event bridge + toast + badge ├── popup/ │ ├── popup.html Extension toolbar popup - stats dashboard │ └── popup.js Stats read from storage + manual sync + disconnect ├── welcome/ │ ├── welcome.html Onboarding wizard - opens on first install │ └── welcome.js 3-step OAuth + repo create/select + completion ├── assets/ │ ├── icons/ │ │ ├── icon16.png │ │ ├── icon32.png │ │ ├── icon48.png │ │ └── icon128.png │ └── upi_qr.png UPI QR code - shown in popup donation modal ├── store-assets/ │ ├── small_promo_tile.png 440x280 - Pillow-generated │ ├── marquee_promo_tile.png 1400x560 - Pillow-generated │ └── screenshot_*.png 1280x800 - framed screenshots x5 ├── scripts/python/ Asset pipeline - NOT shipped in extension │ ├── make_glowing_store_cards.py │ ├── process_highres_framing.py │ └── make_themed_promo_tiles.py └── dist/ Webpack output - this folder gets zipped for Web Store ├── background.js ├── interceptor.js ├── content.js └── uploader.js
Why Webpack? Chrome extension service workers must be single-file bundles. Webpack takes the modular ES import chain and outputs flat dist/*.js files Chrome can load without a module server.

💬 Step 1: Full Session Milestone Summary (12 Core Milestones)

Chronological technical milestones achieved across the 12-hour build session:

  • Milestone 1 - Problem Discovery: Identified browser redirect malware injecting into TUF submission flow via Chrome DevTools Network tab. Pivoted the debugging session into building TUFHub using the same interception technique - legitimately.
  • Milestone 2 - Architecture Lock: Defined single-purpose scope (TUF accepted solution sync to GitHub), confirmed zero-server architecture (pure client-side, GitHub REST API only), and declared all 22 platform invariants.
  • Milestone 3 - Manifest V3 + Script World Isolation: Separated interceptor.js into "world": "MAIN" for fetch monkey-patching and content.js into "world": "ISOLATED" for chrome.runtime messaging. Bridged via Custom DOM Events (TUFHUB_ACCEPTED_SUBMISSION).
  • Milestone 4 - OAuth Implementation: Built dual-path GitHub OAuth: primary via chrome.identity.launchWebAuthFlow, and a tab-based fallback listener (chrome.tabs.onUpdated) to handle Brave Browser's privacy mode which blocks .chromiumapp.org redirect URIs.
  • Milestone 5 - Real-Time Sync Engine: Engineered 3-minute (180,000ms) debounce cooldown per problem slug in chrome.storage.local. Built SHA conflict handler in uploader.js for Git 409 Conflict errors via re-fetch and retry.
  • Milestone 6 - Directory Hierarchy Builder: Defined file path schema as {Category}/{Topic}/{problem-slug}/solution.{ext} with a per-problem README.md stub and a root README.md auto-indexer table tracking total problem counts per category.
  • Milestone 7 - UX Polish: Built floating toast notifications injected into TUF DOM (green success with checkmark, red error with [Fix it] deep-link button). Added animated green badge via chrome.action.setBadgeText with 5s auto-clear timeout.
  • Milestone 8 - Onboarding Wizard: Built 3-step welcome.html page (Step 1: Connect GitHub OAuth; Step 2: Select or Auto-Create repo; Step 3: Done with live test sync) that opens automatically on chrome.runtime.onInstalled.
  • Milestone 9 - Donation UX: Implemented 15th-sync donation nudge toast with PayPal link and UPI QR modal. Wording: "Enjoying TUFHub? Support the project ☕". Counter in syncCount incremented on each accepted sync.
  • Milestone 10 - Remote Config: Implemented Gist-based remote config (tufhub-config.json) fetched on session start for dynamic DOM selector updates without extension republishing.
  • Milestone 11 - Visual Asset Pipeline: Automated 1280x800 Web Store presentation cards with Python Pillow: Fire Orange + Royal Blue ambient radial glow halos, macOS dark browser chrome headers, Apple traffic light buttons, and vector green live status dots.
  • Milestone 12 - Web Store Launch: Completed README engineering (em-dash scrub, 1-row language table, collapsible accordions, ?v=5 Camo CDN cache-busting), submitted to Web Store, hosted Privacy Policy at https://Arora-Sir.github.io/tufhub-privacy.

📋 Step 2: 22-Point Locked Architectural Decision Matrix

These 22 decisions are locked invariants - they cannot change without breaking the extension or violating Web Store policy:

#System / ModuleLocked Choice & Rationale
1Platform ScopeTarget: takeuforward.org/plus only. Single-purpose keeps Web Store approval fast.
2GitHub OAuth AppClient ID: <YOUR_GITHUB_CLIENT_ID>, callback: https://<ext-id>.chromiumapp.org/. NEVER hardcode in source.
3World Isolationinterceptor.js in MAIN, content.js in ISOLATED. Cannot merge - CSP blocks chrome.runtime from MAIN world.
4OAuth FallbackTab URL listener (chrome.tabs.onUpdated) as fallback for Brave. Primary: launchWebAuthFlow.
5Repo Auto-CreationAuto-creates repo during onboarding. User can toggle to use existing repo.
6Repo VisibilityPrivate by default. User toggle in Step 2 of onboarding wizard.
7Directory Schema{Category}/{Topic}/{problem-slug}/solution.{ext} + README.md stub per problem.
8Root README IndexerAuto-maintained category table in repo root README tracking problem counts.
9Submission OverwriteLatest accepted submission always overwrites previous file. SHA fetched before PUT.
10Debounce Cooldown180,000ms (3 minutes) per problem slug. Prevents commit spam on re-submissions.
11Sync Feedback ToastIn-page floating toast: green check success / red X error with [Fix it] deep-link button.
12Toolbar BadgeGreen badge via chrome.action.setBadgeText on sync. Auto-clears after 5000ms.
13Storage Backendchrome.storage.local only - NOT sync. Code content too large for sync quota (100KB limit).
14Visual PaletteTUF Orange #f97316, Slate Dark #1e293b, glassmorphism cards.
15AdBlock SafetyBan on .social-* CSS class names. Use .tufhub-* to survive AdBlock/uBlock Origin.
16Donation CadenceToast nudge every 15th accepted sync. Counter stored as syncCount in local storage.
17Donation ChannelsPayPal: paypal.me/arorasir. UPI: mohit1998arora@yescred (QR modal on desktop).
18Onboarding Wizard3-step welcome.html: Connect GitHub / Select-Create Repo / Done. Opens on onInstalled.
19Remote ConfigGitHub Gist tufhub-config.json fetched on session load for live DOM selector updates.
20Privacy PolicyHosted at https://Arora-Sir.github.io/tufhub-privacy on GitHub Pages. Required for store.
21Disconnect Flow"Disconnect GitHub" button in popup clears all chrome.storage.local keys. Full state reset.
22Build and PackagingWebpack 5 production bundle to dist/, then ZIP uploaded to Web Store.

🧩 Step 3: Manifest V3 Deep Dive - Complete Configuration

Every permission is justified. Here is the complete manifest.json with inline rationale:

{
  "manifest_version": 3,
  "name": "TUFHub - TakeUForward to GitHub Sync",
  "version": "1.0.0",
  "description": "Auto-syncs accepted TakeUForward solutions to your GitHub repository.",
  "icons": {
    "16": "assets/icons/icon16.png",
    "32": "assets/icons/icon32.png",
    "48": "assets/icons/icon48.png",
    "128": "assets/icons/icon128.png"
  },
  "action": {
    "default_popup": "popup/popup.html",
    "default_icon": "assets/icons/icon48.png"
  },
  "background": {
    "service_worker": "dist/background.js",
    "type": "module"
  },
  "content_scripts": [
    {
      "matches": ["https://takeuforward.org/plus/*"],
      "js": ["dist/interceptor.js"],
      "run_at": "document_start",
      "world": "MAIN"
    },
    {
      "matches": ["https://takeuforward.org/plus/*"],
      "js": ["dist/content.js"],
      "run_at": "document_idle"
    }
  ],
  "permissions": ["storage", "identity", "tabs"],
  "host_permissions": [
    "https://takeuforward.org/*",
    "https://api.github.com/*",
    "https://github.com/*"
  ],
  "web_accessible_resources": [
    {
      "resources": ["welcome/welcome.html", "assets/*"],
      "matches": ["<all_urls>"]
    }
  ]
}
Why "world": "MAIN" for interceptor.js? Content scripts run in an ISOLATED JavaScript sandbox - they share the page DOM but NOT the page's window object. To monkey-patch window.fetch, the script MUST run in MAIN world. Trade-off: MAIN world scripts cannot access chrome.runtime APIs, which is why a second content.js in ISOLATED world handles chrome messaging.

🔬 Step 4: interceptor.js - Fetch Monkey-Patch (MAIN World)

This is the core of the extension. Runs in "world": "MAIN" at document_start to intercept TUF submission API responses before the page's React code processes them:

// interceptor.js - Runs in "world": "MAIN"
// WHY MAIN WORLD: Content scripts in ISOLATED world cannot access window.fetch
// or the page's JS context. MAIN world shares the page's window object.
(function () {
  'use strict';
  const _originalFetch = window.fetch;

  window.fetch = async function (...args) {
    const [resource] = args;
    const url = typeof resource === 'string' ? resource : resource?.url;
    const response = await _originalFetch.apply(this, args);

    if (url && /takeuforward\.org.*(submit|run_submission|submissions\/run)/i.test(url)) {
      try {
        const data = await response.clone().json();
        if (data && (data.status === 'AC' || data.status === 'Accepted' || data.verdict === 'Accepted')) {
          const payload = {
            problemSlug: data.problem_slug || extractSlugFromURL(url),
            language:    data.language || data.lang || 'cpp',
            code:        data.code || data.user_code || '',
            difficulty:  data.difficulty || 'Medium',
            topic:       data.topic || 'General',
            category:    data.category || 'DSA',
            timestamp:   Date.now(),
          };
          document.dispatchEvent(
            new CustomEvent('TUFHUB_ACCEPTED_SUBMISSION', { detail: payload })
          );
        }
      } catch (e) { /* Silent catch - never break the host page */ }
    }
    return response;
  };

  function extractSlugFromURL(url) {
    const match = url.match(/problems?\/([^/?#]+)/i);
    return match ? match[1].toLowerCase().replace(/\s+/g, '-') : 'unknown-problem';
  }
})();
Why clone the response? A fetch Response body is a readable stream that can only be consumed once. If we call response.json() directly, the page's React code will receive an already-consumed response and crash. response.clone() creates a second independent stream from the same data.

📡 Step 5: content.js - Toast Injector + Event Bridge (ISOLATED World)

Runs in the default ISOLATED world. Listens for DOM Events from interceptor.js and forwards payload to the background service worker:

// content.js - Runs in ISOLATED world (default)
// PURPOSE: Listen for DOM events from MAIN world, forward to background service worker,
//          and inject UI toast notifications into the TUF page.
(function () {
  'use strict';

  document.addEventListener('TUFHUB_ACCEPTED_SUBMISSION', async (event) => {
    const payload = event.detail;
    if (!payload || !payload.code) return;

    showToast('syncing', 'Syncing ' + payload.problemSlug + ' to GitHub...');

    try {
      const response = await chrome.runtime.sendMessage({ type: 'SYNC_SUBMISSION', data: payload });
      if (response && response.success) {
        showToast('success', 'Synced to GitHub! ✓ ' + payload.problemSlug);
        if (response.showDonationNudge) {
          setTimeout(() => showToast('donate', 'Enjoying TUFHub? Support the project ☕'), 4000);
        }
      } else {
        showToast('error', 'Sync failed: ' + (response?.error || 'Unknown error'), true);
      }
    } catch (err) {
      showToast('error', 'Extension error: ' + err.message, true);
    }
  });

  function showToast(type, message, showFix = false) {
    document.getElementById('tufhub-toast')?.remove();
    const colors = {
      syncing: { bg: '#1e3a5f', border: '#3b82f6', icon: '🔄' },
      success: { bg: '#052e16', border: '#10b981', icon: '✓' },
      error:   { bg: '#450a0a', border: '#ef4444', icon: '✗' },
      donate:  { bg: '#1c1205', border: '#f97316', icon: '☕' },
    };
    const c = colors[type] || colors.syncing;
    const toast = document.createElement('div');
    toast.id = 'tufhub-toast';
    toast.setAttribute('role', 'alert');
    toast.style.cssText =
      'position:fixed;bottom:24px;right:24px;z-index:2147483647;' +
      'background:' + c.bg + ';border:1px solid ' + c.border + ';' +
      'color:#f1f5f9;padding:14px 18px;border-radius:12px;' +
      'font-family:Inter,system-ui,sans-serif;font-size:14px;' +
      'max-width:340px;box-shadow:0 8px 24px rgba(0,0,0,.5);' +
      'display:flex;align-items:flex-start;gap:10px;';
    const icon = document.createElement('span');
    icon.textContent = c.icon;
    icon.style.cssText = 'font-size:18px;flex-shrink:0;margin-top:1px';
    const text = document.createElement('div');
    text.textContent = message;
    toast.append(icon, text);
    if (showFix) {
      const fix = document.createElement('button');
      fix.textContent = '[Fix it]';
      fix.style.cssText = 'background:none;border:none;color:#60a5fa;cursor:pointer;font-weight:700;padding:0 0 0 6px;font-size:13px';
      fix.addEventListener('click', () => chrome.runtime.sendMessage({ type: 'OPEN_POPUP' }));
      text.appendChild(fix);
    }
    document.body.appendChild(toast);
    setTimeout(() => toast.remove(), type === 'error' ? 8000 : 5000);
  }
})();
Why in-page toast instead of Chrome Notifications? Chrome Notifications appear outside the browser window and require the notifications permission. The in-page toast appears right next to the problem the user just solved, is contextually relevant, and requires no extra permission that could trigger Web Store reviewer scrutiny.

⚙️ Step 6: background.js - Service Worker, OAuth & Message Router

// background.js - MV3 Service Worker
import { uploadSubmission } from './uploader.js';

const CLIENT_ID = '<YOUR_GITHUB_CLIENT_ID>';
const REDIRECT_URI = 'https://' + chrome.runtime.id + '.chromiumapp.org/';

chrome.runtime.onInstalled.addListener((details) => {
  if (details.reason === 'install') {
    chrome.tabs.create({ url: chrome.runtime.getURL('welcome/welcome.html') });
  }
});

chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
  if (msg.type === 'SYNC_SUBMISSION') { handleSync(msg.data).then(sendResponse); return true; }
  if (msg.type === 'START_AUTH')      { startGitHubAuth().then(sendResponse); return true; }
  if (msg.type === 'GET_STATS')       { chrome.storage.local.get(['stats'], r => sendResponse(r.stats || {})); return true; }
  if (msg.type === 'DISCONNECT')      { chrome.storage.local.clear(() => sendResponse({ success: true })); return true; }
});

// PRIMARY OAuth path
async function startGitHubAuth() {
  const authURL = 'https://github.com/login/oauth/authorize?client_id=' + CLIENT_ID +
    '&redirect_uri=' + encodeURIComponent(REDIRECT_URI) + '&scope=repo';
  return new Promise((resolve) => {
    chrome.identity.launchWebAuthFlow({ url: authURL, interactive: true }, async (redirectURL) => {
      if (chrome.runtime.lastError || !redirectURL) {
        resolve({ success: false, error: 'Auth window closed or blocked' }); return;
      }
      const code = new URL(redirectURL).searchParams.get('code');
      const token = await exchangeCodeForToken(code);
      await chrome.storage.local.set({ token });
      resolve({ success: true });
    });
  });
}

// FALLBACK for Brave Browser (blocks *.chromiumapp.org redirects in strict privacy mode)
chrome.tabs.onUpdated.addListener(async (tabId, changeInfo) => {
  if (changeInfo.url && changeInfo.url.includes('.chromiumapp.org/?code=')) {
    const code = new URL(changeInfo.url).searchParams.get('code');
    if (code) {
      const token = await exchangeCodeForToken(code);
      await chrome.storage.local.set({ token });
      chrome.tabs.remove(tabId);
    }
  }
});

async function exchangeCodeForToken(code) {
  const res = await fetch('https://github.com/login/oauth/access_token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
    body: JSON.stringify({ client_id: CLIENT_ID, code }),
  });
  const data = await res.json();
  return data.access_token;
}

async function handleSync(submission) {
  const { token, repoName } = await chrome.storage.local.get(['token','repoName']);
  if (!token) return { success: false, error: 'Not authenticated. Please reconnect GitHub.' };
  if (!repoName) return { success: false, error: 'No repository selected. Open settings.' };

  const result = await uploadSubmission(token, repoName, submission);
  if (result.success) {
    const { stats = { solved:0, easy:0, medium:0, hard:0 } } = await chrome.storage.local.get('stats');
    stats.solved++;
    const d = (submission.difficulty || '').toLowerCase();
    if (d) stats[d] = (stats[d] || 0) + 1;
    await chrome.storage.local.set({ stats });

    chrome.action.setBadgeText({ text: '✓' });
    chrome.action.setBadgeBackgroundColor({ color: '#10b981' });
    setTimeout(() => chrome.action.setBadgeText({ text: '' }), 5000);

    const { syncCount = 0 } = await chrome.storage.local.get('syncCount');
    await chrome.storage.local.set({ syncCount: syncCount + 1 });
    if ((syncCount + 1) % 15 === 0) result.showDonationNudge = true;
  }
  return result;
}
Why does Brave Browser break OAuth? Brave Shields in strict mode classifies *.chromiumapp.org as a tracking domain and blocks redirects to it. The tab URL listener bypasses this by watching for the redirect URL in chrome.tabs.onUpdated before the page renders.

⚡ Step 7: uploader.js - GitHub API + SHA Conflict Retry + README Indexer

// uploader.js - GitHub REST API File Uploader
const GITHUB_API = 'https://api.github.com';
const LANG_EXT = { 'c++':'cpp',cpp:'cpp',java:'java',python:'py',python3:'py',
  javascript:'js',js:'js',typescript:'ts','c#':'cs',csharp:'cs',go:'go',rust:'rs',
  kotlin:'kt',swift:'swift',sql:'sql' };

export async function uploadSubmission(token, repoName, submission) {
  const { problemSlug, language, code, difficulty, topic, category } = submission;
  const ext  = LANG_EXT[language?.toLowerCase()] || 'txt';
  const cat  = sanitize(category);
  const top  = sanitize(topic);
  const slug = sanitize(problemSlug);

  // Check debounce (3 minutes per slug)
  const dbKey = 'db_' + slug;
  const { [dbKey]: last } = await chrome.storage.local.get(dbKey);
  if (last && Date.now() - last < 180000)
    return { success: false, error: 'Debounce cooldown (3 min). Already synced recently.' };

  const { login } = await getUser(token);
  const headers = { Authorization: 'token ' + token, 'Content-Type': 'application/json',
    Accept: 'application/vnd.github.v3+json' };

  const base = GITHUB_API + '/repos/' + login + '/' + repoName + '/contents/';
  const filePath = cat + '/' + top + '/' + slug + '/solution.' + ext;

  // Upload solution file (with SHA conflict retry)
  const sr = await putFile(base + filePath, headers,
    'Add ' + slug + ' [' + language + '] [TUFHub]',
    btoa(unescape(encodeURIComponent(code))));
  if (!sr.ok) return { success: false, error: 'GitHub API ' + sr.status };

  // Upload per-problem README stub
  const rm = '# ' + slug.replace(/-/g,'  ') + '\n\n**Difficulty:** ' + difficulty + ' | **Topic:** ' + topic;
  await putFile(base + cat + '/' + top + '/' + slug + '/README.md', headers,
    'Add ' + slug + ' README [TUFHub]', btoa(unescape(encodeURIComponent(rm))));

  await chrome.storage.local.set({ [dbKey]: Date.now() });
  return { success: true, filePath };
}

// PUT with automatic 409/422 SHA conflict retry
async function putFile(url, headers, message, content) {
  let body = { message, content, committer: { name:'TUFHub', email:'tufhub@noreply' } };
  let res = await fetch(url, { method:'PUT', headers, body: JSON.stringify(body) });
  if (res.status === 409 || res.status === 422) {
    const g = await fetch(url, { headers });
    if (g.ok) { body.sha = (await g.json()).sha; }
    res = await fetch(url, { method:'PUT', headers, body: JSON.stringify(body) });
  }
  return res;
}

async function getUser(token) {
  const r = await fetch(GITHUB_API + '/user', { headers:{ Authorization:'token ' + token } });
  return r.json();
}

function sanitize(s) {
  return (s || 'General').toLowerCase().replace(/\s+/g,'-').replace(/[^a-z0-9-]/g,'');
}
Why btoa(unescape(encodeURIComponent(code)))? GitHub Contents API requires Base64. Plain btoa(code) crashes on Unicode characters (common in code comments). The unescape(encodeURIComponent(...)) wrapper converts Unicode to a Latin-1 compatible byte string before Base64 encoding.

🎨 Step 8: popup.js Dashboard + welcome.html Onboarding Wizard

popup.js - Stats Dashboard

// popup.js - Extension toolbar popup
document.addEventListener('DOMContentLoaded', async () => {
  const { stats, token, repoName } = await chrome.storage.local.get(['stats','token','repoName']);
  const s = stats || { solved:0, easy:0, medium:0, hard:0 };
  document.getElementById('stat-solved').textContent = s.solved;
  document.getElementById('stat-easy').textContent   = s.easy   || 0;
  document.getElementById('stat-medium').textContent = s.medium || 0;
  document.getElementById('stat-hard').textContent   = s.hard   || 0;

  const banner = document.getElementById('auth-banner');
  if (!token) {
    banner.textContent = '⚠️ Not connected to GitHub';
    banner.style.background = '#450a0a';
  } else {
    banner.textContent = '✓ Connected | Repo: ' + (repoName || 'Not set');
    banner.style.background = '#052e16';
  }

  document.getElementById('btn-connect').addEventListener('click', async () => {
    const res = await chrome.runtime.sendMessage({ type: 'START_AUTH' });
    if (res.success) location.reload();
    else alert('Auth failed: ' + res.error);
  });

  document.getElementById('btn-disconnect').addEventListener('click', async () => {
    if (confirm('Disconnect GitHub? All local data will be cleared.')) {
      await chrome.runtime.sendMessage({ type: 'DISCONNECT' });
      location.reload();
    }
  });

  document.getElementById('btn-donate-upi').addEventListener('click', () => {
    document.getElementById('upi-modal').style.display = 'flex';
  });
});

welcome.js - 3-Step Onboarding Wizard

// welcome.js
let step = 0;
const steps = ['step-connect', 'step-repo', 'step-done'];
const show = (n) => steps.forEach((id, i) =>
  document.getElementById(id).style.display = i === n ? 'block' : 'none');

// Step 1: Connect GitHub OAuth
document.getElementById('btn-start-auth').addEventListener('click', async () => {
  const res = await chrome.runtime.sendMessage({ type: 'START_AUTH' });
  if (res.success) show(++step);
  else alert('Authentication failed: ' + res.error);
});

// Step 2: Create or select repository
document.getElementById('btn-create-repo').addEventListener('click', async () => {
  const name = document.getElementById('repo-name').value.trim() || 'TUF-Solutions';
  const isPrivate = document.getElementById('repo-private').checked;
  const { token } = await chrome.storage.local.get('token');
  const res = await fetch('https://api.github.com/user/repos', {
    method: 'POST',
    headers: { Authorization: 'token ' + token, 'Content-Type': 'application/json' },
    body: JSON.stringify({ name, private: isPrivate, auto_init: true }),
  });
  if (res.ok || res.status === 422) { // 422 = repo already exists
    await chrome.storage.local.set({ repoName: name });
    show(++step);
  } else {
    alert('Could not create repo. Check GitHub permissions.');
  }
});

show(0);

📦 Step 9: webpack.config.js - Build Pipeline

// webpack.config.js
const path = require('path');

module.exports = {
  mode: 'production',
  entry: {
    background:  './scripts/background.js',
    interceptor: './scripts/tuf/interceptor.js',
    content:     './scripts/tuf/content.js',
    uploader:    './scripts/uploader.js',
    popup:       './popup/popup.js',
    welcome:     './welcome/welcome.js',
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].js',
    clean: true,
  },
  optimization: {
    splitChunks: false,   // CRITICAL: each extension script must be self-contained
    runtimeChunk: false,
  },
  resolve: { extensions: ['.js'] },
};

Build Commands

# Install dependencies
npm install webpack webpack-cli --save-dev

# Production build (ready for Web Store)
npx webpack --mode production

# Development build (with source maps)
npx webpack --mode development

# Package for submission (PowerShell)
Compress-Archive -Path dist,manifest.json,popup,welcome,assets -DestinationPath TUFHub_v1.0.0.zip -Force
Why splitChunks: false? Webpack's default code splitting creates shared chunk files. Chrome extension content scripts cannot dynamically import chunks - they must be fully self-contained. Disabling chunk splitting ensures each output file is standalone.

🐛 Step 10: Debugging Log & Lessons Learned

Every significant bug encountered during the 12-hour build session, its root cause, and the exact fix:

#Bug SymptomRoot CauseFix Applied
1window.fetch not interceptedScript in default ISOLATED world cannot access page's window objectMoved interceptor.js to "world": "MAIN" in manifest content_scripts
2chrome.runtime undefined in interceptor.jsMAIN world scripts cannot access chrome.* APIs (CSP restriction)Kept chrome.runtime calls only in content.js (ISOLATED world). Used Custom DOM Event bridge.
3OAuth callback never received in Brave BrowserBrave Shields blocks redirects to *.chromiumapp.org as tracker domainAdded chrome.tabs.onUpdated listener as fallback - catches redirect URL before page renders
4Git returns 422 on file PUTAttempting to create a file that already exists without providing its SHAGET existing file first to extract SHA, then include sha field in PUT body for overwrite
5Git returns 409 Conflict on rapid double-submitRace condition: two simultaneous PUTs get same stale SHA, second one failsAdded 3-minute per-problem debounce cooldown in chrome.storage.local
6Toast immediately hidden by AdBlock / uBlock OriginToast div had class .social-share - matched AdBlock cosmetic filterRenamed all CSS classes: .tufhub-toast, .network-btn, .tufhub-icon
7Extension toolbar badge not showingIcon path in manifest wrong - icon48.png not found in output dirFixed icon paths, added CopyWebpackPlugin to copy assets to dist/
8chrome.storage.sync quota exceededSync storage has 100KB total quota. Code content exceeds it immediatelySwitched all storage to chrome.storage.local (5MB+ quota)
9Webpack bundle missing background.js from dist/Entry configured as array instead of named objectChanged to named entry object: entry: { background: './scripts/background.js' }
10btoa(code) throws on Unicode charactersbtoa() only handles Latin-1. Code comments often contain UnicodeUsed btoa(unescape(encodeURIComponent(code)))
11Service worker terminates before upload completesMV3 service workers idle-terminate after ~30sUsed event.waitUntil() pattern and kept all operations under 25s
12README images not updating on GitHub after pushGitHub Camo CDN aggressively caches image URLsAppended ?v=5 cache-buster to all image URLs in README

🛒 Step 11: Chrome Web Store Submission Walkthrough

Pre-Submission Checklist

  • 1. Create Chrome Web Store Developer Account (one-time $5 registration fee)
  • 2. Run production webpack build: npx webpack --mode production
  • 3. ZIP the submission: dist/ + manifest.json + popup/ + welcome/ + assets/ (exclude node_modules/ and Python scripts)
  • 4. Drag-drop the ZIP into chrome://extensions in Developer Mode and test all flows before submitting
  • 5. Ensure Privacy Policy is live at a permanent public URL before submitting

Store Listing Fields Used

FieldValueTip
Extension NameTUFHub - TakeUForward to GitHub SyncInclude primary keyword in name
Short DescriptionAuto-sync your TakeUForward accepted solutions to GitHub. Zero setup, zero servers.132 char max - pack keywords
CategoryDeveloper ToolsMost accurate for coding tool extensions
Privacy Policyhttps://Arora-Sir.github.io/tufhub-privacyREQUIRED - will be rejected without it
Single Purpose"This extension has one purpose: to automatically commit accepted TakeUForward problem solutions to the user's connected GitHub repository."Must match actual behavior exactly
Most Common Rejection Reasons:
1. Remote code execution - extension loads external JS. All code must be bundled in the ZIP.
2. Single-purpose violation - extension must do one clearly defined thing.
3. Misleading capabilities - store listing must not overstate what the extension does.
4. Missing privacy policy - no exceptions, even for extensions that collect no data.

🖼️ Step 12: Store Graphics Pipeline - Python Pillow Automation

Asset Specifications

AssetSizePurposeKey Design Elements
Icon128x128 PNGExtension icon in all contextsTUF Orange circle, GitHub Octocat center
Small Promo Tile440x280 PNGWeb Store listing thumbnailFire Orange left glow, Royal Blue right glow
Marquee Promo Tile1400x560 PNGWeb Store featured bannerBoth glows, hero text, screenshot mockup
Screenshots x51280x800 PNGIn-store gallery screenshotsmacOS dark browser header, glow halos around real screenshot

Color Palette

  • Background: #0b1120 - Deep Navy matching TUF platform dark mode
  • Fire Orange glow: #f97316 alpha 90/255 for radial ellipse fill
  • Royal Blue glow: #2563eb alpha 80/255 for radial ellipse fill
  • macOS header bar: #1e293b background, #334155 URL bar
  • Traffic lights: Red #ff5f57, Yellow #ffbd2e, Green #28c840
  • Status dot: #22c55e green dot for "Active & Syncing" label

make_glowing_store_cards.py - Screenshot Framing Script

from PIL import Image, ImageDraw, ImageFilter
import os

W, H = 1280, 800
INPUT_DIR  = './screenshots_raw'
OUTPUT_DIR = './store-assets/screenshots'

def create_card(ss_path, out_path):
    bg = Image.new('RGBA', (W, H), (11, 17, 32, 255))

    # Fire Orange glow (top-right)
    g1 = Image.new('RGBA', (W, H), (0,0,0,0))
    ImageDraw.Draw(g1).ellipse((int(W*.55), -150, int(W*1.3), int(H*.75)), fill=(249,115,22,90))
    bg = Image.alpha_composite(bg, g1.filter(ImageFilter.GaussianBlur(130)))

    # Royal Blue glow (bottom-left)
    g2 = Image.new('RGBA', (W, H), (0,0,0,0))
    ImageDraw.Draw(g2).ellipse((-150, int(H*.4), int(W*.5), int(H*1.2)), fill=(37,99,235,80))
    bg = Image.alpha_composite(bg, g2.filter(ImageFilter.GaussianBlur(130)))

    # macOS browser chrome header
    draw = ImageDraw.Draw(bg)
    HEADER_H = 52
    draw.rounded_rectangle([60, 80, W-60, 80+HEADER_H], radius=12, fill=(30,41,59,255))
    for i, color in enumerate([(255,95,87),(255,189,46),(40,200,64)]):
        cx = 90 + i*24
        draw.ellipse([cx-7, 80+HEADER_H//2-7, cx+7, 80+HEADER_H//2+7], fill=color)
    draw.rounded_rectangle([160, 89, W-180, 123], radius=7, fill=(51,65,85,255))
    draw.text((180, 97), '●  takeuforward.org/plus', fill=(34,197,94))

    # Composite screenshot into content area
    if os.path.exists(ss_path):
        ss = Image.open(ss_path).convert('RGBA')
        ss.thumbnail((W-120, H-80-HEADER_H-60), Image.LANCZOS)
        bg.paste(ss, ((W-ss.width)//2, 80+HEADER_H+2), ss)

    bg.convert('RGB').save(out_path, 'PNG', optimize=True)
    print('Created:', out_path)

os.makedirs(OUTPUT_DIR, exist_ok=True)
for i, f in enumerate(sorted(os.listdir(INPUT_DIR))):
    if f.endswith(('.png','.jpg')):
        create_card(os.path.join(INPUT_DIR, f), os.path.join(OUTPUT_DIR, f'screenshot_{i+1:02d}.png'))

📝 Step 13: README Engineering - Documentation Standards

DecisionWhy
1-row language support tableGitHub renders multi-column tables better in single rows; avoids ugly wrapping on narrow screens
?v=5 on all image URLsGitHub Camo CDN caches images aggressively. Cache-buster forces re-fetch after screenshots are updated.
<details><summary> accordionsKeeps README scannable. Full code examples hidden by default to avoid overwhelming first-time readers.
Em-dash ban (use : or -)Em-dashes render inconsistently across GitHub markdown renderers and Samsung Notes. Banned globally.
Footer: "Crafted with ❤️ for Problem Solvers by Mohit Arora"Personal branding. Chosen as the warmest phrasing after iterating on several options.

Supported Languages Table (1-Row Format)

## 💻 Supported Languages

| **C++** (`.cpp`) | **Java** (`.java`) | **Python** (`.py`) | **JavaScript** (`.js`) | **C#** (`.cs`) | **Go** (`.go`) | **SQL** (`.sql`) |
| :---: | :---: | :---: | :---: | :---: | :---: | :---: |

📖 Step 14: Q&A Decision Ledger - Why Every Decision Was Made

Key technical questions raised during the session and the architectural decisions they produced:

Question
Why use "world": "MAIN" instead of a standard content script?
Decision Made
Content scripts run in an ISOLATED JavaScript sandbox - they share the page DOM but NOT the page's window object. To monkey-patch window.fetch, the script MUST run in MAIN world. Trade-off: MAIN world scripts cannot access chrome.runtime, which is why a second content.js in ISOLATED world handles the chrome messaging bridge.
Question
Why does Brave Browser fail the standard OAuth flow?
Decision Made
Brave Shields in strict mode classifies *.chromiumapp.org as a tracking domain and blocks redirects to it. The launchWebAuthFlow API relies on this domain for the OAuth callback. Solution: open auth in a new tab and watch chrome.tabs.onUpdated for the code= query param in the URL before the page renders.
Question
Why 3 minutes for the debounce cooldown? Why not 30 seconds or 10 minutes?
Decision Made
TUF allows re-submitting immediately. 30 seconds is too short - a user editing and resubmitting could still hit it. 10 minutes is too long if the user genuinely improves their solution. 3 minutes was the practical middle ground - long enough to prevent API spam from rapid retries, short enough not to block genuine re-attempts.
Question
Why use chrome.storage.local instead of chrome.storage.sync?
Decision Made
Sync storage has a 100KB total quota and 8KB per-item limit. Solution code for competitive programming problems can be 5-50KB per file. Storing even a few solutions in sync storage would immediately exceed the quota. Local storage has a 5MB+ quota and is appropriate for this use case.
Question
Why is the toast using .tufhub-toast and not .social-share?
Decision Made
AdBlock and uBlock Origin maintain cosmetic filter lists that block any element with CSS classes matching patterns like .social-*, .share-*, .donate-*. The toast and donation button would be invisibly hidden for the majority of users who have ad blockers. All class names were prefixed with tufhub- to avoid false positives.
Question
Why use GitHub Gist for remote config instead of a repository file?
Decision Made
Gists are directly fetched via raw URL without requiring OAuth authentication. A repository file fetch would require the user's token, creating a chicken-and-egg problem (config needed before auth, but auth needed for config). Gist with public access solves this cleanly.
Question
Why is the donation nudge triggered every 15th sync?
Decision Made
After 15 accepted syncs, the user has tangibly benefited from the extension - they have 15 problems committed to GitHub. They are most likely to feel the value at that point. First-use is too early (no proven value). Time-based (after a week) is disconnected from actual usage patterns.
Question
Why clone the fetch Response body? What breaks if you don't?
Decision Made
A Response body is a ReadableStream that can only be consumed once. If the interceptor calls response.json(), the stream is exhausted. When the page's React code then tries to read the same response, it gets 'body has already been consumed'. response.clone() creates an independent copy of the stream, allowing both the interceptor and the original page code to read the response without conflict.

✅ Step 15: 50-Point Master Checklist for Building ANY Chrome Extension

A complete engineering checklist derived from the TUFHub build session. Use this as a template for any production Chrome Extension:

Phase A: Ideation, Scope & Manifest V3 Core (Points 1-10)

  • 1. Write a one-sentence 'Single Purpose Statement' that clearly describes what the extension does and nothing else.
  • 2. Define exact host_permissions - list ONLY the domains the extension actually communicates with.
  • 3. List all browser permissions needed and write a one-line justification for each (storage, identity, tabs, etc.).
  • 4. Decide: zero-server vs. backend-server architecture. Zero-server preferred for privacy-first tools.
  • 5. Specify manifest_version: 3 - MV2 is deprecated and will stop working.
  • 6. Decide background script type: event-driven service worker (MV3) vs. persistent background page (deprecated).
  • 7. Identify if content scripts need MAIN world (fetch/window access) or ISOLATED world (chrome APIs) - you may need both.
  • 8. Plan the user's onboarding flow: what must they configure before the extension is useful?
  • 9. Draft the privacy policy - what data is collected, where it goes, and how it is deleted.
  • 10. Identify all CSS class names that could trigger AdBlock filters (.social-*, .share-*, .donate-*) and rename them.

Phase B: Service Worker & State Management (Points 11-20)

  • 11. Use chrome.storage.local for all user data. Never store sensitive data in chrome.storage.sync (100KB limit).
  • 12. Handle service worker termination: MV3 service workers idle-terminate. Keep all operations under 25 seconds.
  • 13. Use chrome.runtime.onInstalled to open onboarding page only on first install (check details.reason === 'install').
  • 14. Return true from chrome.runtime.onMessage listener if the response is async (keeps message channel open).
  • 15. Implement a 'Disconnect / Reset' flow that clears all stored data (token, stats, settings) in one operation.
  • 16. Add chrome.action.setBadgeText for visual feedback on sync success/error state without requiring popup open.
  • 17. Always check for auth token before performing API calls. Return a clear error message if missing.
  • 18. Implement OAuth token refresh or re-auth flow for when tokens expire.
  • 19. Never log sensitive data (tokens, private code) to console.log - logs are visible in chrome://extensions devtools.
  • 20. Test service worker termination by waiting 30s idle and then triggering an action - ensure state reloads from storage correctly.

Phase C: Network Interception & World Isolation (Points 21-30)

  • 21. Always clone the Response before reading: response.clone().json() - never consume the original stream.
  • 22. Wrap all interception logic in try/catch with silent catch - never throw errors that could break the host page.
  • 23. Store the original fetch reference immediately: const _originalFetch = window.fetch before any page scripts modify it.
  • 24. Bridge MAIN to ISOLATED via document.dispatchEvent(new CustomEvent(...)) - the ONLY safe communication channel.
  • 25. Use specific URL pattern matching (regex) - never intercept ALL fetch requests, only exact API endpoints needed.
  • 26. Test interception with both window.fetch AND XMLHttpRequest - some platforms use XHR even today.
  • 27. For OAuth: implement both launchWebAuthFlow (primary) and tab URL listener fallback (for Brave and strict privacy browsers).
  • 28. Implement debounce per resource identifier (problem slug, URL, ID) to prevent duplicate API calls.
  • 29. Handle GitHub API 409 Conflict: GET existing file SHA, then retry PUT with sha field included in body.
  • 30. Use btoa(unescape(encodeURIComponent(content))) for Base64 encoding any content that may contain Unicode.

Phase D: Content Scripts, DOM & UX (Points 31-40)

  • 31. Inject UI elements with unique, prefixed IDs to avoid conflicts with host page DOM (id="tufhub-toast" not id="toast").
  • 32. Always use z-index: 2147483647 (max int32) for injected overlays to ensure they appear above host page elements.
  • 33. Add CSS animations via injected <style> tags, not external CSS files (content script CSS can cause FOUC).
  • 34. Auto-dismiss toasts (5s for success, 8s for error). Include manual close button for accessibility.
  • 35. Add role="alert" to notification elements for screen reader accessibility.
  • 36. Use inline CSS (style.cssText) for injected elements - extension CSS files are not guaranteed to load in all contexts.
  • 37. Remove any pre-existing injected elements before re-injecting: document.getElementById('tufhub-toast')?.remove().
  • 38. For popup UI: use DOMContentLoaded not load event - popup has no external resources to wait for.
  • 39. Implement micro-interactions: hover scale(1.05) with cubic-bezier easing, active press scale(0.97).
  • 40. Test all UI elements with AdBlock Plus, uBlock Origin, and Brave Shields enabled.

Phase E: Build, Store Graphics & Submission Audit (Points 41-50)

  • 41. Use Webpack 5 multi-entry build with splitChunks: false - extension scripts must be self-contained bundles.
  • 42. Verify the ZIP package contains: manifest.json at root, all scripts in dist/, and all referenced assets.
  • 43. Test the packed ZIP by drag-dropping it into chrome://extensions in Developer Mode before submitting.
  • 44. Create icons in all 4 required sizes: 16px, 32px, 48px, 128px. Test at 16px - must be recognizable.
  • 45. Generate 5 screenshots at 1280x800. Use Python Pillow to add browser chrome framing and ambient glow halos.
  • 46. Create small promo tile (440x280) and marquee promo tile (1400x560) with consistent visual identity.
  • 47. Host Privacy Policy at a permanent public URL (GitHub Pages works). Include: what is collected, not collected, storage duration, deletion method.
  • 48. Write permission justification text for EVERY permission requested - the review form requires it for each.
  • 49. Self-audit: search all source files for eval(, innerHTML =, document.write( - all will cause rejection.
  • 50. After approval: monitor reviews, respond to issues via GitHub Issues, and use remote config (Gist) to patch DOM selectors without republishing.