Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
89 changes: 59 additions & 30 deletions generate-index.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,18 +196,6 @@ a:focus-visible { outline: 2px solid var(--accent-light); outline-offset: 2px; b
.site-nav { display: flex; gap: 16px; align-items: center; }
.site-nav a { color: var(--text-muted); font-size: 14px; font-weight: 500; white-space: nowrap; }
.site-nav a:hover { color: var(--text); text-decoration: none; }
.request-btn {
color: var(--accent-light) !important;
border: 1px solid var(--accent);
border-radius: var(--radius);
padding: 6px 12px;
transition: background 0.2s, color 0.2s;
}
.request-btn:hover {
background: var(--accent);
color: #fff !important;
text-decoration: none !important;
}
.hero {
padding: 64px 0 48px;
text-align: center;
Expand Down Expand Up @@ -395,14 +383,21 @@ a:focus-visible { outline: 2px solid var(--accent-light); outline-offset: 2px; b
pointer-events: auto;
}
.submit-btn.active:hover { background: var(--accent-light); }
.submit-preview {
margin-top: 8px;
.submit-btn.loading {
opacity: 0.6;
pointer-events: none;
}
.submit-feedback {
margin-top: 10px;
font-size: 13px;
color: var(--green);
font-family: var(--mono);
display: none;
}
.submit-preview.visible { display: block; }
.submit-feedback.visible { display: block; }
.submit-feedback.preview { color: var(--text-muted); }
.submit-feedback.success { color: var(--green); }
.submit-feedback.success a { color: var(--green); text-decoration: underline; }
.submit-feedback.error { color: var(--red); }
@media (max-width: 768px) {
.container { padding: 0 16px; }
.hero { padding: 40px 0 32px; }
Expand Down Expand Up @@ -432,7 +427,6 @@ a:focus-visible { outline: 2px solid var(--accent-light); outline-offset: 2px; b
<a href="https://supermodeltools.com">Website</a>
<a href="https://github.com/supermodeltools">GitHub</a>
<a href="https://x.com/supermodeltools">X</a>
<a href="https://github.com/supermodeltools/supermodeltools.github.io/issues/new?template=request-repo.yml" class="request-btn">+ Request a Repo</a>
</nav>
</div>
</header>
Expand Down Expand Up @@ -464,7 +458,7 @@ a:focus-visible { outline: 2px solid var(--accent-light); outline-offset: 2px; b
<input type="text" class="submit-input" id="submit-url" placeholder="https://github.com/owner/repo" autocomplete="off" spellcheck="false">
<button class="submit-btn" id="submit-btn" type="button">Request</button>
</div>
<div class="submit-preview" id="submit-preview"></div>
<div class="submit-feedback" id="submit-feedback"></div>
</div>
</div>

Expand Down Expand Up @@ -510,10 +504,10 @@ a:focus-visible { outline: 2px solid var(--accent-light); outline-offset: 2px; b
var noResults = document.getElementById('no-results');
var submitInput = document.getElementById('submit-url');
var submitBtn = document.getElementById('submit-btn');
var submitPreview = document.getElementById('submit-preview');
var feedback = document.getElementById('submit-feedback');
var noResultsRequest = document.getElementById('no-results-request');

var issueBase = 'https://github.com/supermodeltools/supermodeltools.github.io/issues/new?template=request-repo.yml';
var API_URL = '/api/request';

// --- Search ---
searchInput.addEventListener('input', function() {
Expand Down Expand Up @@ -546,36 +540,71 @@ a:focus-visible { outline: 2px solid var(--accent-light); outline-offset: 2px; b
return null;
}

function showFeedback(msg, type) {
feedback.className = 'submit-feedback visible ' + type;
feedback.innerHTML = msg;
}
Comment on lines +555 to +558
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid innerHTML for feedback messages.

Line [554] uses innerHTML for text/status content. If backend error strings ever contain HTML, this becomes an XSS sink. Use plain text rendering here.

Suggested fix
 function showFeedback(msg, type) {
   feedback.className = 'submit-feedback visible ' + type;
-  feedback.innerHTML = msg;
+  feedback.textContent = msg;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function showFeedback(msg, type) {
feedback.className = 'submit-feedback visible ' + type;
feedback.innerHTML = msg;
}
function showFeedback(msg, type) {
feedback.className = 'submit-feedback visible ' + type;
feedback.textContent = msg;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@generate-index.go` around lines 552 - 555, The showFeedback function
currently assigns feedback.innerHTML = msg which can introduce an XSS sink if
msg contains HTML; change it to render plain text instead (e.g., use
feedback.textContent = msg or create a text node and append it) and keep the
className assignment as-is; update the showFeedback function reference so any
callers still pass a plain string and remove reliance on innerHTML for
displaying backend error strings.


submitInput.addEventListener('input', function() {
var parsed = parseRepo(this.value);
if (parsed) {
var name = parsed.split('/')[1];
submitPreview.textContent = '\u2192 Docs will be at repos.supermodeltools.com/' + name + '/';
submitPreview.classList.add('visible');
showFeedback('\u2192 repos.supermodeltools.com/' + name + '/', 'preview');
submitBtn.classList.add('active');
} else {
submitPreview.classList.remove('visible');
feedback.className = 'submit-feedback';
submitBtn.classList.remove('active');
}
});

function submitRequest() {
async function submitRequest() {
var parsed = parseRepo(submitInput.value);
if (!parsed) return;

var repoUrl = 'https://github.com/' + parsed;
var name = parsed.split('/')[1];
var url = issueBase
+ '&repo_url=' + encodeURIComponent(repoUrl)
+ '&title=' + encodeURIComponent('[Repo Request] ' + name);
window.open(url, '_blank');

// Loading state
submitBtn.classList.add('loading');
submitBtn.textContent = 'Submitting...';
showFeedback('Setting up ' + name + '...', 'preview');

try {
var resp = await fetch(API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: repoUrl }),
});
var data = await resp.json();

if (!resp.ok || !data.success) {
showFeedback(data.error || 'Something went wrong. Please try again.', 'error');
submitBtn.classList.remove('loading');
submitBtn.textContent = 'Request';
return;
}

// Success — show the link, clear the input
showFeedback(
'\u2713 Submitted! Docs will be generated at <a href="' + data.docs_url + '">' +
'repos.supermodeltools.com/' + name + '/</a>', 'success'
);
submitInput.value = '';
submitBtn.classList.remove('active', 'loading');
submitBtn.textContent = 'Request';
} catch (e) {
showFeedback('Network error. Please try again.', 'error');
submitBtn.classList.remove('loading');
submitBtn.textContent = 'Request';
}
}

submitBtn.addEventListener('click', submitRequest);
submitInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter') submitRequest();
if (e.key === 'Enter' && submitBtn.classList.contains('active')) submitRequest();
});
Comment on lines +572 to 611
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Block re-entrancy to prevent duplicate repo requests.

Right now, Line [607] can still fire submitRequest() while the button is already loading, and submitRequest() itself has no guard. Since this endpoint creates GitHub issues (non-idempotent), duplicate submits are a real risk.

Suggested fix
 async function submitRequest() {
+  if (submitBtn.classList.contains('loading')) return;
   var parsed = parseRepo(submitInput.value);
   if (!parsed) return;
@@
 submitInput.addEventListener('keydown', function(e) {
-  if (e.key === 'Enter' && submitBtn.classList.contains('active')) submitRequest();
+  if (e.key === 'Enter'
+      && submitBtn.classList.contains('active')
+      && !submitBtn.classList.contains('loading')) {
+    submitRequest();
+  }
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function submitRequest() {
var parsed = parseRepo(submitInput.value);
if (!parsed) return;
var repoUrl = 'https://github.com/' + parsed;
var name = parsed.split('/')[1];
var url = issueBase
+ '&repo_url=' + encodeURIComponent(repoUrl)
+ '&title=' + encodeURIComponent('[Repo Request] ' + name);
window.open(url, '_blank');
// Loading state
submitBtn.classList.add('loading');
submitBtn.textContent = 'Generating...';
showFeedback('Setting up ' + name + '...', 'preview');
try {
var resp = await fetch(API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: repoUrl }),
});
var data = await resp.json();
if (!resp.ok || !data.success) {
showFeedback(data.error || 'Something went wrong. Please try again.', 'error');
submitBtn.classList.remove('loading');
submitBtn.textContent = 'Generate';
return;
}
// Redirect to the docs page — 404.html shows loading until docs are ready
window.location.href = data.docs_url;
} catch (e) {
showFeedback('Network error. Please try again.', 'error');
submitBtn.classList.remove('loading');
submitBtn.textContent = 'Generate';
}
}
submitBtn.addEventListener('click', submitRequest);
submitInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter') submitRequest();
if (e.key === 'Enter' && submitBtn.classList.contains('active')) submitRequest();
});
async function submitRequest() {
if (submitBtn.classList.contains('loading')) return;
var parsed = parseRepo(submitInput.value);
if (!parsed) return;
var repoUrl = 'https://github.com/' + parsed;
var name = parsed.split('/')[1];
// Loading state
submitBtn.classList.add('loading');
submitBtn.textContent = 'Generating...';
showFeedback('Setting up ' + name + '...', 'preview');
try {
var resp = await fetch(API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: repoUrl }),
});
var data = await resp.json();
if (!resp.ok || !data.success) {
showFeedback(data.error || 'Something went wrong. Please try again.', 'error');
submitBtn.classList.remove('loading');
submitBtn.textContent = 'Generate';
return;
}
// Redirect to the docs page — 404.html shows loading until docs are ready
window.location.href = data.docs_url;
} catch (e) {
showFeedback('Network error. Please try again.', 'error');
submitBtn.classList.remove('loading');
submitBtn.textContent = 'Generate';
}
}
submitBtn.addEventListener('click', submitRequest);
submitInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter'
&& submitBtn.classList.contains('active')
&& !submitBtn.classList.contains('loading')) {
submitRequest();
}
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@generate-index.go` around lines 569 - 608, submitRequest can be re-entered
while a request is in flight, causing duplicate non-idempotent submissions; add
a simple guard (e.g., an isSubmitting boolean) near the top of submitRequest and
return early if true, set isSubmitting = true when starting and set it back to
false in all exit paths (success, non-ok/data.error, and catch), and also
prevent the Enter-key handler from invoking submitRequest when isSubmitting is
true (or check submitBtn.classList.contains('loading') there); reference
submitRequest, submitBtn, submitInput, parseRepo, and API_URL when locating
where to add the guard and to ensure the loading state/reset is handled
consistently.


// "No results" request link: pre-fill with search query as a guess
// "No results" link: scroll up and focus the submit input
noResultsRequest.addEventListener('click', function() {
var q = searchInput.value.trim();
submitInput.value = q;
Expand Down
93 changes: 93 additions & 0 deletions worker/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/**
* Cloudflare Worker — repo request proxy
*
* Receives a repo URL from the homepage, creates a GitHub issue
* (which triggers the auto-add-repo workflow), and returns immediately.
*
* Environment secrets (set via wrangler secret put):
* GITHUB_TOKEN — fine-grained PAT with issues:write on supermodeltools.github.io
*
* Deploy:
* cd worker && npx wrangler deploy
*
* Route (add in Cloudflare dashboard or wrangler.toml):
* repos.supermodeltools.com/api/* → this worker
*/

const CORS_HEADERS = {
'Access-Control-Allow-Origin': 'https://repos.supermodeltools.com',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
};

const REPO_RE = /github\.com\/([a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+)/;

export default {
async fetch(request, env) {
if (request.method === 'OPTIONS') {
return new Response(null, { status: 204, headers: CORS_HEADERS });
}

if (request.method !== 'POST') {
return jsonResponse({ error: 'Method not allowed' }, 405);
}

let body;
try {
body = await request.json();
} catch {
return jsonResponse({ error: 'Invalid JSON' }, 400);
}

const url = (body.url || '').trim().replace(/\/+$/, '').replace(/\.git$/, '');
const match = url.match(REPO_RE);
if (!match) {
return jsonResponse({ error: 'Invalid GitHub repository URL' }, 400);
}

const upstream = match[1];
const name = upstream.split('/')[1];
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Heads up: potential name collision with repo-only naming.

You're extracting just the repo name (e.g., cool-lib) and ignoring the owner. If two users request different repos with the same name:

  • github.com/alice/cool-lib → name = cool-lib
  • github.com/bob/cool-lib → name = cool-lib

Both would get docs_url: repos.supermodeltools.com/cool-lib/. The second request might overwrite the first or cause confusion.

This might be intentional (maybe you want a "first come, first served" approach), but just wanted to flag it in case it wasn't considered.

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

In `@worker/index.js` around lines 48 - 49, The code extracts only the repo
segment into name (const upstream = match[1]; const name =
upstream.split('/')[1];) which causes collisions for identical repo names across
different owners; change the naming to include the owner as well (e.g., split
upstream into owner and repo via upstream.split('/') and set name to a combined,
normalized identifier like `${owner}-${repo}` or otherwise include owner in the
docs_url path) so repos are unique per owner/repo pair and avoid overwrites.


// Create the GitHub issue — this triggers auto-add-repo.yml
const ghResponse = await fetch(
'https://api.github.com/repos/supermodeltools/supermodeltools.github.io/issues',
{
method: 'POST',
headers: {
Authorization: `Bearer ${env.GITHUB_TOKEN}`,
Accept: 'application/vnd.github+json',
'User-Agent': 'supermodel-request-bot',
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: `[Repo Request] ${name}`,
body: `### Repository URL\n\nhttps://github.com/${upstream}`,
labels: ['repo-request'],
}),
}
);

if (!ghResponse.ok) {
const err = await ghResponse.text();
console.error('GitHub API error:', ghResponse.status, err);
return jsonResponse({ error: 'Failed to submit request. Please try again.' }, 502);
}

const issue = await ghResponse.json();

return jsonResponse({
success: true,
name,
upstream,
docs_url: `https://repos.supermodeltools.com/${name}/`,
issue_url: issue.html_url,
});
},
};

function jsonResponse(data, status = 200) {
return new Response(JSON.stringify(data), {
status,
headers: { 'Content-Type': 'application/json', ...CORS_HEADERS },
});
}
7 changes: 7 additions & 0 deletions worker/wrangler.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
name = "repo-request"
main = "index.js"
compatibility_date = "2024-01-01"

# Route: intercept /api/* on the custom domain
# Requires the domain to be proxied through Cloudflare
# routes = [{ pattern = "repos.supermodeltools.com/api/*", zone_name = "supermodeltools.com" }]
Loading