Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/build-index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ jobs:

- name: Generate site
run: go run generate-index.go
env:
ISSUES_TOKEN: ${{ secrets.ISSUES_TOKEN }}

- uses: actions/upload-pages-artifact@v3
with:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
site/
216 changes: 206 additions & 10 deletions generate-index.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ func main() {
os.Exit(1)
}

if err := generateSkeleton(); err != nil {
fmt.Fprintf(os.Stderr, "Error generating skeleton: %v\n", err)
os.Exit(1)
}

// Copy CNAME and static root files to site directory
if cname, err := os.ReadFile("CNAME"); err == nil {
os.WriteFile("site/CNAME", cname, 0644)
Expand All @@ -86,6 +91,13 @@ func generate404() error {
`), 0644)
}

func generateSkeleton() error {
if err := os.MkdirAll("site/generating", 0755); err != nil {
return err
}
return os.WriteFile("site/generating/index.html", []byte(skeletonTemplate), 0644)
}

func generateSitemap(cfg Config) error {
var b strings.Builder
b.WriteString(`<?xml version="1.0" encoding="UTF-8"?>` + "\n")
Expand All @@ -99,6 +111,11 @@ func generateSitemap(cfg Config) error {
return os.WriteFile("site/sitemap.xml", []byte(b.String()), 0644)
}

type PageData struct {
Config
Token string
}
Comment on lines +114 to +117
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: ISSUES_TOKEN is being published to every browser.

Line 153 injects the secret into the HTML, and Lines 539/611 send it from client-side JS. On a static site this is fully exposed (view-source/devtools), so anyone can spam or abuse issue creation on your repo.

Use a server-side relay (or GitHub App flow) so the credential never leaves trusted infrastructure.

🔒 Minimal safe mitigation (disable public token exposure)
 type PageData struct {
 	Config
-	Token string
 }

@@
-	return tmpl.Execute(f, PageData{Config: cfg, Token: os.Getenv("ISSUES_TOKEN")})
+	return tmpl.Execute(f, PageData{Config: cfg})
@@
-    var GH_TOKEN = '{{.Token}}';
+    var GH_TOKEN = '';

Also applies to: 153-153, 539-541, 608-613

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@generate-index.go` around lines 114 - 117, The PageData struct currently
includes a Token field and the code injects ISSUES_TOKEN into rendered HTML
(exposing the secret); remove Token from PageData and stop passing ISSUES_TOKEN
into any client-side templates/JS (references: PageData, ISSUES_TOKEN, and the
template rendering in generate-index.go), then implement a server-side relay
endpoint that performs authenticated GitHub issue creation (or switch to a
GitHub App/OAuth flow) so client JS calls your backend route instead of using
the token directly; update client-side calls (those currently sending the token)
to call the new relay endpoint and remove any code that reads/forwards
ISSUES_TOKEN to the browser.


func generateIndex(cfg Config) error {
tmpl, err := template.New("index").Funcs(template.FuncMap{
"escape": html.EscapeString,
Expand Down Expand Up @@ -133,7 +150,7 @@ func generateIndex(cfg Config) error {
}
defer f.Close()

return tmpl.Execute(f, cfg)
return tmpl.Execute(f, PageData{Config: cfg, Token: os.Getenv("ISSUES_TOKEN")})
}

const indexTemplate = `<!DOCTYPE html>
Expand Down Expand Up @@ -519,7 +536,8 @@ a:focus-visible { outline: 2px solid var(--accent-light); outline-offset: 2px; b
var feedback = document.getElementById('submit-feedback');
var noResultsRequest = document.getElementById('no-results-request');

var API_URL = '/api/request';
var GH_TOKEN = '{{.Token}}';
var GH_REPO = 'supermodeltools/supermodeltools.github.io';

// --- Search ---
searchInput.addEventListener('input', function() {
Expand Down Expand Up @@ -573,6 +591,11 @@ a:focus-visible { outline: 2px solid var(--accent-light); outline-offset: 2px; b
var parsed = parseRepo(submitInput.value);
if (!parsed) return;

if (!GH_TOKEN) {
showFeedback('Generate is not configured yet.', 'error');
return;
}

var repoUrl = 'https://github.com/' + parsed;
var name = parsed.split('/')[1];

Expand All @@ -582,22 +605,30 @@ a:focus-visible { outline: 2px solid var(--accent-light); outline-offset: 2px; b
showFeedback('Setting up ' + name + '...', 'preview');

try {
var resp = await fetch(API_URL, {
var resp = await fetch('https://api.github.com/repos/' + GH_REPO + '/issues', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: repoUrl }),
headers: {
'Authorization': 'Bearer ' + GH_TOKEN,
'Accept': 'application/vnd.github+json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: '[Repo Request] ' + name,
body: '### Repository URL\n\n' + repoUrl,
labels: ['repo-request'],
}),
});
var data = await resp.json();

if (!resp.ok || !data.success) {
showFeedback(data.error || 'Something went wrong. Please try again.', 'error');
if (!resp.ok) {
var err = await resp.json().catch(function() { return {}; });
showFeedback(err.message || 'Something went wrong. Please try again.', 'error');
submitBtn.classList.remove('loading');
submitBtn.textContent = 'Generate';
return;
}

// Redirect to the skeleton loading page — served by the worker
window.location.href = data.generating_url;
// Redirect to the skeleton loading page
window.location.href = '/generating/?repo=' + encodeURIComponent(name);
} catch (e) {
showFeedback('Network error. Please try again.', 'error');
submitBtn.classList.remove('loading');
Expand All @@ -624,3 +655,168 @@ a:focus-visible { outline: 2px solid var(--accent-light); outline-offset: 2px; b
</html>
`

const skeletonTemplate = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Generating — Architecture Documentation</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
:root{--bg:#0f1117;--bg-card:#1a1d27;--bg-hover:#22263a;--border:#2a2e3e;--text:#e4e4e7;--text-muted:#9ca3af;--accent:#6366f1;--accent-light:#818cf8;--font:'Inter',-apple-system,BlinkMacSystemFont,sans-serif;--mono:'JetBrains Mono','Fira Code',monospace;--max-w:1200px;--radius:8px}
*{margin:0;padding:0;box-sizing:border-box}html{overflow-x:hidden}
body{font-family:var(--font);background:var(--bg);color:var(--text);line-height:1.6;-webkit-font-smoothing:antialiased;overflow-x:hidden}
a{color:var(--accent-light);text-decoration:none}a:hover{text-decoration:underline}
.container{max-width:var(--max-w);margin:0 auto;padding:0 24px}
.site-header{border-bottom:1px solid var(--border);padding:16px 0;position:sticky;top:0;background:var(--bg);z-index:100}
.site-header .container{display:flex;align-items:center;justify-content:space-between;gap:16px}
.site-brand{font-size:18px;font-weight:700;color:var(--text);display:flex;align-items:center;gap:8px;white-space:nowrap}
.site-brand:hover{text-decoration:none;color:var(--accent-light)}
.site-brand svg{width:24px;height:24px}
.site-nav{display:flex;gap:16px;align-items:center}
.site-nav a,.site-nav span{color:var(--text-muted);font-size:14px;font-weight:500;white-space:nowrap}
.nav-all-repos{color:var(--accent-light)!important;padding-right:12px;margin-right:4px;border-right:1px solid var(--border)}
.hero{padding:48px 0 40px;text-align:center}
.hero h1{font-size:28px;font-weight:700;margin-bottom:12px}
.hero-sub{color:var(--text-muted);font-size:15px;max-width:560px;margin:0 auto 24px}
.hero-actions{display:flex;gap:8px;justify-content:center;margin-bottom:16px}
.hero-btn{display:inline-flex;align-items:center;gap:6px;padding:7px 14px;font-size:13px;font-weight:500;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);color:var(--text-muted)}
.hero-stats{display:flex;justify-content:center;gap:28px;flex-wrap:wrap}
.hero-stat{text-align:center}
.hero-stat .label{font-size:12px;color:var(--text-muted)}
@keyframes shimmer{0%{background-position:-400px 0}100%{background-position:400px 0}}
.shim{background:linear-gradient(90deg,var(--bg-card) 25%,var(--bg-hover) 50%,var(--bg-card) 75%);background-size:800px 100%;animation:shimmer 1.8s ease-in-out infinite;border-radius:4px}
.shim-num{width:48px;height:28px;margin:0 auto 4px;border-radius:4px}
.chart-panel{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:24px;margin-bottom:24px}
.chart-panel h3{font-size:16px;font-weight:600;margin-bottom:16px}
.shim-chart{height:280px;border-radius:var(--radius)}
.section{margin-bottom:40px}
.section-title{font-size:20px;font-weight:700;margin-bottom:12px}
.tax-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:8px}
.tax-entry-skel{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;background:var(--bg-card);border:1px solid var(--border);border-radius:6px}
.shim-entry-name{width:60%;height:14px}
.shim-entry-count{width:28px;height:14px}
.gen-status{position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:var(--bg-card);border:1px solid var(--border);border-radius:12px;padding:14px 24px;display:flex;align-items:center;gap:14px;font-size:14px;color:var(--text);box-shadow:0 8px 32px rgba(0,0,0,0.4);z-index:200;max-width:90vw}
.gen-spinner{width:18px;height:18px;flex-shrink:0;border:2px solid var(--border);border-top-color:var(--accent-light);border-radius:50%;animation:spin .8s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
.gen-step{color:var(--text-muted)}.gen-step strong{color:var(--text)}
.site-footer{border-top:1px solid var(--border);padding:32px 0;margin-top:48px;color:var(--text-muted);font-size:13px;text-align:center}
@media(max-width:768px){.container{padding:0 16px}.hero{padding:32px 0 24px}.hero h1{font-size:22px}.hero-stats{gap:16px}.tax-grid{grid-template-columns:1fr}.gen-status{bottom:12px;padding:10px 16px;font-size:13px}}
</style>
</head>
<body>
<header class="site-header">
<div class="container">
<span class="site-brand" id="brand">
<svg viewBox="0 0 90 78" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M90 61.1124C75.9375 73.4694 59.8419 78 44.7554 78C29.669 78 11.8614 72.6122 0 61.1011V16.9458C11.6168 6 29.891 0 44.9887 0C62.77 0 78.8723 6.97959 89.9887 16.9458V61.1124H90ZM88.1881 38.9553C77.7923 22.8824 59.8983 15.7959 44.7554 15.7959C29.6126 15.7959 13.4515 21.9008 1.556 38.9444C12.5382 54.69 26.9 62.5085 44.7554 62.0944C67.6297 61.5639 77.6495 51.9184 88.1881 38.9553ZM44.7554 16.3475C32.4756 16.3475 22.3888 26.6879 22.2554 38.9388C34.3765 38.9162 44.7554 29.1429 44.7554 16.3475C44.7554 29.1429 55.1344 38.9162 67.2554 38.9388C67.1202 26.5216 57.1141 16.3475 44.7554 16.3475ZM44.7554 61.5639C44.7554 48.4898 34.3765 38.9613 22.2554 38.9388C22.3888 51.1897 32.4756 61.5639 44.7554 61.5639C57.0352 61.5639 67.122 51.1897 67.2554 38.9388C55.1344 38.9613 44.7554 48.4898 44.7554 61.5639Z" fill="currentColor"/></svg>
<span id="brand-name"></span>
</span>
<nav class="site-nav">
<a href="https://repos.supermodeltools.com/" class="nav-all-repos">&larr; All Repos</a>
<span>By Type</span><span>Domains</span><span>Languages</span><span>Tags</span>
</nav>
</div>
</header>
<main>
<div class="container">
<div class="hero">
<h1 id="hero-title"></h1>
<div class="hero-actions">
<span class="hero-btn">View on GitHub</span>
<span class="hero-btn">Star</span>
<span class="hero-btn">Fork</span>
</div>
<p class="hero-sub">Architecture documentation generated from code analysis. Explore every file, function, class, and domain.</p>
<div class="hero-stats">
<div class="hero-stat"><div class="shim shim-num"></div><div class="label">Total Entities</div></div>
<div class="hero-stat"><div class="shim shim-num"></div><div class="label">Node Types</div></div>
<div class="hero-stat"><div class="shim shim-num"></div><div class="label">Languages</div></div>
<div class="hero-stat"><div class="shim shim-num"></div><div class="label">Domains</div></div>
<div class="hero-stat"><div class="shim shim-num"></div><div class="label">Subdomains</div></div>
<div class="hero-stat"><div class="shim shim-num"></div><div class="label">Top Directories</div></div>
</div>
</div>
<div class="chart-panel"><h3>Architecture Overview</h3><div class="shim shim-chart"></div></div>
<div class="chart-panel"><h3>Codebase Composition</h3><div class="shim shim-chart" style="height:200px"></div></div>
<div class="section"><h2 class="section-title">Node Types</h2><div class="tax-grid">
<div class="tax-entry-skel"><div class="shim shim-entry-name"></div><div class="shim shim-entry-count"></div></div>
<div class="tax-entry-skel"><div class="shim shim-entry-name"></div><div class="shim shim-entry-count"></div></div>
<div class="tax-entry-skel"><div class="shim shim-entry-name"></div><div class="shim shim-entry-count"></div></div>
<div class="tax-entry-skel"><div class="shim shim-entry-name"></div><div class="shim shim-entry-count"></div></div>
<div class="tax-entry-skel"><div class="shim shim-entry-name"></div><div class="shim shim-entry-count"></div></div>
<div class="tax-entry-skel"><div class="shim shim-entry-name"></div><div class="shim shim-entry-count"></div></div>
</div></div>
<div class="section"><h2 class="section-title">Domains</h2><div class="tax-grid">
<div class="tax-entry-skel"><div class="shim shim-entry-name"></div><div class="shim shim-entry-count"></div></div>
<div class="tax-entry-skel"><div class="shim shim-entry-name"></div><div class="shim shim-entry-count"></div></div>
<div class="tax-entry-skel"><div class="shim shim-entry-name"></div><div class="shim shim-entry-count"></div></div>
<div class="tax-entry-skel"><div class="shim shim-entry-name"></div><div class="shim shim-entry-count"></div></div>
</div></div>
<div class="section"><h2 class="section-title">Languages</h2><div class="tax-grid">
<div class="tax-entry-skel"><div class="shim shim-entry-name"></div><div class="shim shim-entry-count"></div></div>
<div class="tax-entry-skel"><div class="shim shim-entry-name"></div><div class="shim shim-entry-count"></div></div>
<div class="tax-entry-skel"><div class="shim shim-entry-name"></div><div class="shim shim-entry-count"></div></div>
</div></div>
</div>
</main>
<footer class="site-footer"><div class="container"><p>Generated with <a href="https://github.com/supermodeltools/arch-docs">arch-docs</a> by <a href="https://supermodeltools.com">supermodeltools</a></p></div></footer>
<div class="gen-status" id="gen-status">
<div class="gen-spinner"></div>
<div class="gen-step" id="gen-step"><strong>Generating docs</strong> &mdash; forking repository&hellip;</div>
</div>
<script>
(function() {
var params = new URLSearchParams(window.location.search);
var name = params.get('repo') || 'repository';
var docsUrl = 'https://repos.supermodeltools.com/' + encodeURIComponent(name) + '/';

document.getElementById('brand-name').textContent = name;
document.getElementById('hero-title').textContent = name;
document.title = 'Generating ' + name + ' \u2014 Architecture Documentation';

var statusEl = document.getElementById('gen-step');
var messages = [
{ text: '<strong>Generating docs</strong> \u2014 forking repository\u2026', at: 0 },
{ text: '<strong>Generating docs</strong> \u2014 analyzing codebase\u2026', at: 8000 },
{ text: '<strong>Generating docs</strong> \u2014 building code graphs\u2026', at: 35000 },
{ text: '<strong>Generating docs</strong> \u2014 mapping architecture\u2026', at: 60000 },
{ text: '<strong>Generating docs</strong> \u2014 deploying site\u2026', at: 90000 },
{ text: '<strong>Almost there</strong> \u2014 finalizing\u2026', at: 120000 },
];
messages.forEach(function(m) {
setTimeout(function() { statusEl.innerHTML = m.text; }, m.at);
});

var pollCount = 0;
var maxPolls = 120;
function poll() {
pollCount++;
if (pollCount > maxPolls) {
statusEl.innerHTML = '<strong>Still working</strong> \u2014 this repo may be large. <a href="' + docsUrl + '">Check manually \u2192</a>';
return;
}
fetch(docsUrl, { cache: 'no-store', redirect: 'follow' })
.then(function(resp) {
if (resp.ok) {
return resp.text().then(function(html) {
if (html.indexOf('arch-docs') !== -1 && html.indexOf('gen-status') === -1) {
statusEl.innerHTML = '<strong>Ready!</strong> Loading docs\u2026';
setTimeout(function() { window.location.href = docsUrl; }, 600);
return;
}
setTimeout(poll, 5000);
});
}
setTimeout(poll, 5000);
})
.catch(function() { setTimeout(poll, 5000); });
}
setTimeout(poll, 5000);
})();
</script>
</body>
</html>
`

Loading
Loading