Setup Instructions
The Failed to fetch error is fixed! It was caused by Google redirecting download URLs. The new <all_urls> permission allows the extension to follow these redirects securely.
- Create a folder named
hayson-viewer. - Copy all 5 files from the right panel into this folder.
-
Crucial Step: For security, Chrome V3 blocks external scripts. You must copy the JSZip code into the
jszip.jsfile! - Go to
chrome://extensions/, enable Developer Mode, and Load Unpacked.
Popup UI Preview
Hayson Viewer
Navigate to an extension page on the Chrome Web Store to view its source.
hayson-viewer / manifest.json
Copied to clipboard!
{
"manifest_version": 3,
"name": "Hayson Viewer",
"version": "1.0.0",
"description": "View and extract the source code of any Chrome Extension directly from the Chrome Web Store.",
"permissions": [
"activeTab",
"scripting"
],
"host_permissions": [
"<all_urls>"
],
"action": {
"default_popup": "popup.html",
"default_title": "Open Hayson Viewer"
}
}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hayson Viewer</title>
<style>
body {
width: 260px;
margin: 0;
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
text-align: center;
background-color: #ffffff;
color: #0f172a;
}
.icon {
font-size: 32px;
margin-bottom: 12px;
}
h2 {
margin: 0 0 4px 0;
font-size: 18px;
font-weight: 800;
letter-spacing: -0.5px;
}
p {
color: #64748b;
font-size: 12px;
margin-bottom: 20px;
line-height: 1.4;
}
button {
width: 100%;
padding: 10px;
font-size: 14px;
font-weight: 700;
cursor: pointer;
border-radius: 8px;
border: none;
background-color: #2563eb;
color: white;
transition: background 0.2s;
}
button:hover {
background-color: #1d4ed8;
}
#status {
margin-top: 12px;
font-size: 11px;
color: #ef4444; /* red for errors */
font-weight: 600;
}
</style>
</head>
<body>
<div class="icon">🛠️</div>
<h2>Hayson Viewer</h2>
<p>Navigate to an extension page on the Web Store to extract its code.</p>
<button id="extract-btn">Extract Source Code</button>
<div id="status"></div>
<script src="popup.js"></script>
</body>
</html>
document.addEventListener('DOMContentLoaded', () => {
const extractBtn = document.getElementById('extract-btn');
const statusDiv = document.getElementById('status');
extractBtn.addEventListener('click', async () => {
statusDiv.style.color = '#64748b';
statusDiv.innerText = "Checking active tab...";
try {
// Get the currently active tab
let [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tab || !tab.url) {
statusDiv.style.color = '#ef4444';
statusDiv.innerText = "Cannot read tab URL.";
return;
}
// Check if we are on the Chrome Web Store
if (tab.url.includes('chromewebstore.google.com/detail/') || tab.url.includes('chrome.google.com/webstore/detail/')) {
// Chrome Extension IDs are exactly 32 lowercase letters (a-p)
// This Regex searches the URL string to find that exact 32-character pattern
const match = tab.url.match(/([a-p]{32})(?:\?|\/|$)/);
if (match && match[1]) {
const extensionId = match[1];
statusDiv.style.color = '#10b981'; // Green
statusDiv.innerText = "ID Found! Opening Viewer...";
// Open our custom viewer HTML page in a new client-side tab, passing the ID in the URL parameters
const viewerUrl = chrome.runtime.getURL(`viewer.html?id=${extensionId}`);
chrome.tabs.create({ url: viewerUrl });
} else {
statusDiv.style.color = '#ef4444';
statusDiv.innerText = "Could not parse Extension ID from URL.";
}
} else {
statusDiv.style.color = '#ef4444';
statusDiv.innerText = "Please navigate to a Chrome Web Store extension page.";
}
} catch (error) {
statusDiv.style.color = '#ef4444';
statusDiv.innerText = "Error accessing tab: " + error.message;
}
});
});
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hayson Code Viewer</title>
<style>
:root {
--bg: #1e1e1e;
--sidebar-bg: #252526;
--border: #333333;
--text: #cccccc;
--text-light: #ffffff;
--highlight: #094771;
--accent: #007acc;
}
body {
margin: 0;
padding: 0;
display: flex;
height: 100vh;
background-color: var(--bg);
color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, sans-serif;
overflow: hidden;
}
/* Sidebar File Tree */
#sidebar {
width: 280px;
background-color: var(--sidebar-bg);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
flex-shrink: 0;
}
.sidebar-header {
padding: 12px 15px;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 1px;
font-weight: bold;
color: var(--text-light);
border-bottom: 1px solid var(--border);
background-color: #2d2d2d;
}
#file-list {
list-style: none;
padding: 10px 0;
margin: 0;
overflow-y: auto;
flex-grow: 1;
}
#file-list li {
padding: 6px 15px 6px 25px;
font-size: 13px;
cursor: pointer;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
user-select: none;
}
#file-list li:hover {
background-color: #2a2d2e;
color: var(--text-light);
}
#file-list li.active {
background-color: #37373d;
color: var(--text-light);
border-left: 3px solid var(--accent);
padding-left: 22px;
}
/* Main Content Area */
#main-content {
flex-grow: 1;
display: flex;
flex-direction: column;
min-width: 0;
}
#top-bar {
height: 40px;
background-color: #1e1e1e;
display: flex;
align-items: center;
padding: 0 15px;
border-bottom: 1px solid var(--border);
font-size: 13px;
color: #9cdcfe;
font-family: monospace;
}
#editor-container {
flex-grow: 1;
overflow: auto;
position: relative;
background-color: #1e1e1e;
}
/* Loading Overlay */
#loading-overlay {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background-color: rgba(30, 30, 30, 0.9);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 10;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid rgba(255, 255, 255, 0.1);
border-left-color: var(--accent);
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 20px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
#loading-text {
font-size: 14px;
color: var(--text-light);
text-align: center;
max-width: 80%;
line-height: 1.5;
white-space: pre-wrap;
}
/* Content renderers */
pre { margin: 0; padding: 20px; }
code { font-family: 'Consolas', monospace; font-size: 14px; line-height: 1.5; color: #d4d4d4; }
.image-viewer {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
padding: 20px;
box-sizing: border-box;
background: repeating-conic-gradient(#333 0% 25%, #222 0% 50%) 50% / 20px 20px;
}
.image-viewer img {
max-width: 100%;
max-height: 100%;
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
}
</style>
</head>
<body>
<!-- Sidebar -->
<div id="sidebar">
<div class="sidebar-header">Hayson Explorer</div>
<ul id="file-list"></ul>
</div>
<!-- Main Editor -->
<div id="main-content">
<div id="top-bar">Select a file to view code</div>
<div id="editor-container">
<div id="loading-overlay">
<div class="spinner" id="spinner-icon"></div>
<div id="loading-text">Initializing Viewer...</div>
</div>
</div>
</div>
<!-- REQUIRED LOCAL LIBRARIES -->
<script src="jszip.js"></script>
<script src="viewer.js"></script>
</body>
</html>
document.addEventListener('DOMContentLoaded', async () => {
const urlParams = new URLSearchParams(window.location.search);
const extensionId = urlParams.get('id');
const fileListEl = document.getElementById('file-list');
const editorContainer = document.getElementById('editor-container');
const topBar = document.getElementById('top-bar');
const loadingOverlay = document.getElementById('loading-overlay');
const loadingText = document.getElementById('loading-text');
const spinner = document.getElementById('spinner-icon');
function showError(msg) {
if(spinner) spinner.style.display = 'none';
if(loadingText) {
loadingText.style.color = '#ff6b6b';
loadingText.innerText = msg;
}
console.error(msg);
}
try {
if (!extensionId) {
throw new Error("Critical Error: No extension ID provided in the URL.");
}
loadingText.innerText = "Checking local libraries...";
// 1. Check if JSZip loaded properly
if (typeof JSZip === 'undefined') {
throw new Error("ERROR: JSZip library is missing!\n\nYou must copy the JSZip code into your 'jszip.js' file. Manifest V3 blocks remote scripts, so this file cannot be empty.");
}
loadingText.innerText = `Connecting to Google Servers...\nTarget ID: ${extensionId}`;
// 2. Fetch the CRX file
const crxUrl = `https://clients2.google.com/service/update2/crx?response=redirect&os=win&arch=x86-64&os_arch=x86-64&nacl_arch=x86-64&prod=chromecrx&prodchannel=&prodversion=114.0.0.0&acceptformat=crx2,crx3&x=id%3D${extensionId}%26uc`;
const response = await fetch(crxUrl, { redirect: 'follow' });
if (!response.ok) {
throw new Error(`Google Server returned HTTP ${response.status}. The extension might be restricted or region-locked.`);
}
loadingText.innerText = `Downloading Extension Data...`;
const arrayBuffer = await response.arrayBuffer();
loadingText.innerText = `Processing CRX Binary Headers...`;
// 3. Extract ZIP archive from CRX blob
const zipData = extractZipFromCrx(arrayBuffer);
loadingText.innerText = `Unzipping files in memory...`;
// 4. Use JSZip to parse
const zip = await new JSZip().loadAsync(zipData);
if(loadingOverlay) loadingOverlay.style.display = 'none'; // Hide loading screen
// 5. Build the File Tree UI
renderFileTree(zip);
} catch (error) {
showError(`Execution Failed.\n\nDetails: ${error.message}`);
}
/* HELPER FUNCTIONS */
function extractZipFromCrx(arrayBuffer) {
const bytes = new Uint8Array(arrayBuffer);
// Look for "PK\x03\x04" (50 4B 03 04)
for (let i = 0; i < bytes.length - 3; i++) {
if (bytes[i] === 0x50 && bytes[i+1] === 0x4B && bytes[i+2] === 0x03 && bytes[i+3] === 0x04) {
return arrayBuffer.slice(i);
}
}
throw new Error("Could not find valid ZIP headers. The download might be corrupted.");
}
function renderFileTree(zip) {
const fileNames = Object.keys(zip.files).sort();
fileNames.forEach(filename => {
const file = zip.files[filename];
if (file.dir) return;
const li = document.createElement('li');
li.textContent = filename;
li.title = filename;
li.onclick = (e) => {
document.querySelectorAll('#file-list li').forEach(el => el.classList.remove('active'));
e.target.classList.add('active');
displayFileContent(zip, filename);
};
fileListEl.appendChild(li);
});
}
async function displayFileContent(zip, filename) {
topBar.innerText = `📄 ${filename}`;
editorContainer.innerHTML = '';
const isImage = filename.match(/\.(png|jpg|jpeg|gif|webp|ico)$/i);
const isSvg = filename.match(/\.(svg)$/i);
try {
if (isImage || isSvg) {
const base64Data = await zip.file(filename).async("base64");
let mimeType = 'image/png';
if (isSvg) mimeType = 'image/svg+xml';
else if (filename.endsWith('.jpg')) mimeType = 'image/jpeg';
else if (filename.endsWith('.gif')) mimeType = 'image/gif';
editorContainer.innerHTML = `
<div class="image-viewer">
<img src="data:${mimeType};base64,${base64Data}" alt="${filename}">
</div>
`;
} else {
const textData = await zip.file(filename).async("string");
const safeText = textData.replace(/[&<>'"]/g, tag => ({
'&': '&', '<': '<', '>': '>', "'": ''', '"': '"'
}[tag]));
editorContainer.innerHTML = `<pre><code>${safeText}</code></pre>`;
}
} catch (err) {
editorContainer.innerHTML = `<pre style="color:#ff6b6b; padding: 20px;">Error reading file: ${err.message}</pre>`;
}
}
});
/*
* ====================================================================
* CRITICAL STEP FOR CHROME EXTENSION MANIFEST V3
* ====================================================================
*
* Chrome extensions (V3) strictly forbid downloading Javascript
* from external links (CDNs) for security reasons.
*
* To make this extension work, you MUST provide the library locally.
*
* INSTRUCTIONS:
* 1. Go to this URL in your browser:
* https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js
*
* 2. Select ALL the code on that page (Ctrl+A / Cmd+A)
* 3. Copy it (Ctrl+C / Cmd+C)
* 4. Paste it entirely into this 'jszip.js' file, replacing these comments.
* 5. Save the file.
*
* Now your extension can unzip files completely offline and securely!
* ====================================================================
*/
// Paste the jszip.min.js code here...