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
This entire extension was born from a browser malware infection. Here is the exact sequence of events:
chrome://extensions/?id=... - confirmed injected content script running in MAIN world with unrestricted fetch access."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.
dist/*.js files Chrome can load without a module server.
Chronological technical milestones achieved across the 12-hour build session:
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).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.chrome.storage.local. Built SHA conflict handler in uploader.js for Git 409 Conflict errors via re-fetch and retry.{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.chrome.action.setBadgeText with 5s auto-clear timeout.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.syncCount incremented on each accepted sync.tufhub-config.json) fetched on session start for dynamic DOM selector updates without extension republishing.?v=5 Camo CDN cache-busting), submitted to Web Store, hosted Privacy Policy at https://Arora-Sir.github.io/tufhub-privacy.These 22 decisions are locked invariants - they cannot change without breaking the extension or violating Web Store policy:
| # | System / Module | Locked Choice & Rationale |
|---|---|---|
| 1 | Platform Scope | Target: takeuforward.org/plus only. Single-purpose keeps Web Store approval fast. |
| 2 | GitHub OAuth App | Client ID: <YOUR_GITHUB_CLIENT_ID>, callback: https://<ext-id>.chromiumapp.org/. NEVER hardcode in source. |
| 3 | World Isolation | interceptor.js in MAIN, content.js in ISOLATED. Cannot merge - CSP blocks chrome.runtime from MAIN world. |
| 4 | OAuth Fallback | Tab URL listener (chrome.tabs.onUpdated) as fallback for Brave. Primary: launchWebAuthFlow. |
| 5 | Repo Auto-Creation | Auto-creates repo during onboarding. User can toggle to use existing repo. |
| 6 | Repo Visibility | Private by default. User toggle in Step 2 of onboarding wizard. |
| 7 | Directory Schema | {Category}/{Topic}/{problem-slug}/solution.{ext} + README.md stub per problem. |
| 8 | Root README Indexer | Auto-maintained category table in repo root README tracking problem counts. |
| 9 | Submission Overwrite | Latest accepted submission always overwrites previous file. SHA fetched before PUT. |
| 10 | Debounce Cooldown | 180,000ms (3 minutes) per problem slug. Prevents commit spam on re-submissions. |
| 11 | Sync Feedback Toast | In-page floating toast: green check success / red X error with [Fix it] deep-link button. |
| 12 | Toolbar Badge | Green badge via chrome.action.setBadgeText on sync. Auto-clears after 5000ms. |
| 13 | Storage Backend | chrome.storage.local only - NOT sync. Code content too large for sync quota (100KB limit). |
| 14 | Visual Palette | TUF Orange #f97316, Slate Dark #1e293b, glassmorphism cards. |
| 15 | AdBlock Safety | Ban on .social-* CSS class names. Use .tufhub-* to survive AdBlock/uBlock Origin. |
| 16 | Donation Cadence | Toast nudge every 15th accepted sync. Counter stored as syncCount in local storage. |
| 17 | Donation Channels | PayPal: paypal.me/arorasir. UPI: mohit1998arora@yescred (QR modal on desktop). |
| 18 | Onboarding Wizard | 3-step welcome.html: Connect GitHub / Select-Create Repo / Done. Opens on onInstalled. |
| 19 | Remote Config | GitHub Gist tufhub-config.json fetched on session load for live DOM selector updates. |
| 20 | Privacy Policy | Hosted at https://Arora-Sir.github.io/tufhub-privacy on GitHub Pages. Required for store. |
| 21 | Disconnect Flow | "Disconnect GitHub" button in popup clears all chrome.storage.local keys. Full state reset. |
| 22 | Build and Packaging | Webpack 5 production bundle to dist/, then ZIP uploaded to Web Store. |
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>"]
}
]
}
"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.
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';
}
})();
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.
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);
}
})();
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.
// 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;
}
*.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.
// 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,'');
}
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.
// 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
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);
// 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'] },
};
# 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
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.
Every significant bug encountered during the 12-hour build session, its root cause, and the exact fix:
| # | Bug Symptom | Root Cause | Fix Applied |
|---|---|---|---|
| 1 | window.fetch not intercepted | Script in default ISOLATED world cannot access page's window object | Moved interceptor.js to "world": "MAIN" in manifest content_scripts |
| 2 | chrome.runtime undefined in interceptor.js | MAIN world scripts cannot access chrome.* APIs (CSP restriction) | Kept chrome.runtime calls only in content.js (ISOLATED world). Used Custom DOM Event bridge. |
| 3 | OAuth callback never received in Brave Browser | Brave Shields blocks redirects to *.chromiumapp.org as tracker domain | Added chrome.tabs.onUpdated listener as fallback - catches redirect URL before page renders |
| 4 | Git returns 422 on file PUT | Attempting to create a file that already exists without providing its SHA | GET existing file first to extract SHA, then include sha field in PUT body for overwrite |
| 5 | Git returns 409 Conflict on rapid double-submit | Race condition: two simultaneous PUTs get same stale SHA, second one fails | Added 3-minute per-problem debounce cooldown in chrome.storage.local |
| 6 | Toast immediately hidden by AdBlock / uBlock Origin | Toast div had class .social-share - matched AdBlock cosmetic filter | Renamed all CSS classes: .tufhub-toast, .network-btn, .tufhub-icon |
| 7 | Extension toolbar badge not showing | Icon path in manifest wrong - icon48.png not found in output dir | Fixed icon paths, added CopyWebpackPlugin to copy assets to dist/ |
| 8 | chrome.storage.sync quota exceeded | Sync storage has 100KB total quota. Code content exceeds it immediately | Switched all storage to chrome.storage.local (5MB+ quota) |
| 9 | Webpack bundle missing background.js from dist/ | Entry configured as array instead of named object | Changed to named entry object: entry: { background: './scripts/background.js' } |
| 10 | btoa(code) throws on Unicode characters | btoa() only handles Latin-1. Code comments often contain Unicode | Used btoa(unescape(encodeURIComponent(code))) |
| 11 | Service worker terminates before upload completes | MV3 service workers idle-terminate after ~30s | Used event.waitUntil() pattern and kept all operations under 25s |
| 12 | README images not updating on GitHub after push | GitHub Camo CDN aggressively caches image URLs | Appended ?v=5 cache-buster to all image URLs in README |
npx webpack --mode productiondist/ + manifest.json + popup/ + welcome/ + assets/ (exclude node_modules/ and Python scripts)chrome://extensions in Developer Mode and test all flows before submitting| Field | Value | Tip |
|---|---|---|
| Extension Name | TUFHub - TakeUForward to GitHub Sync | Include primary keyword in name |
| Short Description | Auto-sync your TakeUForward accepted solutions to GitHub. Zero setup, zero servers. | 132 char max - pack keywords |
| Category | Developer Tools | Most accurate for coding tool extensions |
| Privacy Policy | https://Arora-Sir.github.io/tufhub-privacy | REQUIRED - 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 |
| Asset | Size | Purpose | Key Design Elements |
|---|---|---|---|
| Icon | 128x128 PNG | Extension icon in all contexts | TUF Orange circle, GitHub Octocat center |
| Small Promo Tile | 440x280 PNG | Web Store listing thumbnail | Fire Orange left glow, Royal Blue right glow |
| Marquee Promo Tile | 1400x560 PNG | Web Store featured banner | Both glows, hero text, screenshot mockup |
| Screenshots x5 | 1280x800 PNG | In-store gallery screenshots | macOS dark browser header, glow halos around real screenshot |
#0b1120 - Deep Navy matching TUF platform dark mode#f97316 alpha 90/255 for radial ellipse fill#2563eb alpha 80/255 for radial ellipse fill#1e293b background, #334155 URL bar#ff5f57, Yellow #ffbd2e, Green #28c840#22c55e green dot for "Active & Syncing" labelfrom 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'))
| Decision | Why |
|---|---|
| 1-row language support table | GitHub renders multi-column tables better in single rows; avoids ugly wrapping on narrow screens |
?v=5 on all image URLs | GitHub Camo CDN caches images aggressively. Cache-buster forces re-fetch after screenshots are updated. |
<details><summary> accordions | Keeps 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
| **C++** (`.cpp`) | **Java** (`.java`) | **Python** (`.py`) | **JavaScript** (`.js`) | **C#** (`.cs`) | **Go** (`.go`) | **SQL** (`.sql`) |
| :---: | :---: | :---: | :---: | :---: | :---: | :---: |
Key technical questions raised during the session and the architectural decisions they produced:
"world": "MAIN" instead of a standard content script?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.*.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.chrome.storage.local instead of chrome.storage.sync?.tufhub-toast and not .social-share?.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.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.A complete engineering checklist derived from the TUFHub build session. Use this as a template for any production Chrome Extension:
host_permissions - list ONLY the domains the extension actually communicates with.manifest_version: 3 - MV2 is deprecated and will stop working.chrome.storage.local for all user data. Never store sensitive data in chrome.storage.sync (100KB limit).chrome.runtime.onInstalled to open onboarding page only on first install (check details.reason === 'install').true from chrome.runtime.onMessage listener if the response is async (keeps message channel open).chrome.action.setBadgeText for visual feedback on sync success/error state without requiring popup open.console.log - logs are visible in chrome://extensions devtools.response.clone().json() - never consume the original stream.try/catch with silent catch - never throw errors that could break the host page.const _originalFetch = window.fetch before any page scripts modify it.document.dispatchEvent(new CustomEvent(...)) - the ONLY safe communication channel.window.fetch AND XMLHttpRequest - some platforms use XHR even today.launchWebAuthFlow (primary) and tab URL listener fallback (for Brave and strict privacy browsers).sha field included in body.btoa(unescape(encodeURIComponent(content))) for Base64 encoding any content that may contain Unicode.id="tufhub-toast" not id="toast").z-index: 2147483647 (max int32) for injected overlays to ensure they appear above host page elements.<style> tags, not external CSS files (content script CSS can cause FOUC).role="alert" to notification elements for screen reader accessibility.style.cssText) for injected elements - extension CSS files are not guaranteed to load in all contexts.document.getElementById('tufhub-toast')?.remove().DOMContentLoaded not load event - popup has no external resources to wait for.splitChunks: false - extension scripts must be self-contained bundles.manifest.json at root, all scripts in dist/, and all referenced assets.chrome://extensions in Developer Mode before submitting.eval(, innerHTML =, document.write( - all will cause rejection.