feat: vanity-import responder for go.moleculesai.app (initial)

Cloudflare Worker that handles ?go-get=1 requests and emits go-import
meta tags routing go.moleculesai.app/<area>/* to the canonical Gitea
repo for that area. See molecule-ai/internal#71 for design + module
map.

Files:
- worker.js: stateless responder (~170 lines, vendor-portable)
- wrangler.toml: route binding + zone config
- README.md: deploy + smoke-test instructions

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
claude-ceo-assistant 2026-05-07 23:55:22 +00:00
commit 56306dd237
3 changed files with 248 additions and 0 deletions

65
README.md Normal file
View File

@ -0,0 +1,65 @@
# go.moleculesai.app — Go-import vanity responder
Tiny Cloudflare Worker that handles `?go-get=1` requests for `go.moleculesai.app/*` and emits the `<meta name="go-import">` tag pointing at our actual SCM (Gitea today).
## Why
Source code never names the SCM host. Imports look like `go.moleculesai.app/core/platform/handlers`; this responder tells `go get` where the source actually lives. When the SCM changes, this is the one config that updates — every Go import statement stays put.
Origin: molecule-ai/internal#71. Pre-2026-05-06, our Go modules were under `github.com/Molecule-AI/...`; the suspension of that org made every import a dead link. The migration moved imports to a vanity host we control instead of relocating the lock-in to Gitea.
## Module map
| Vanity prefix | Gitea repo |
|---|---|
| `go.moleculesai.app/core/...` | `molecule-ai/molecule-core` |
| `go.moleculesai.app/controlplane/...` | `molecule-ai/molecule-controlplane` |
| `go.moleculesai.app/cli/...` | `molecule-ai/molecule-cli` |
| `go.moleculesai.app/plugin/gh-identity/...` | `molecule-ai/molecule-ai-plugin-gh-identity` |
Adding a new repo: append one entry to `MODULE_MAP` in `worker.js` and `wrangler deploy`. No code change anywhere else.
## Deploy
```bash
# Once: install wrangler
npm install -g wrangler
# Authenticate (set CLOUDFLARE_API_TOKEN env var, or `wrangler login`)
export CLOUDFLARE_API_TOKEN=<token-with-Workers-Scripts-Edit-+-Zone-DNS-Edit>
# Deploy
wrangler deploy
```
The token needs:
- `Account.Workers Scripts.Edit` (push the worker)
- `Zone.DNS.Edit` on the `moleculesai.app` zone (so wrangler can bind the route)
Or use the Worker route in the Cloudflare dashboard manually — doesn't require token DNS scope.
## Smoke test
```bash
curl -s 'https://go.moleculesai.app/core/platform?go-get=1' | grep go-import
# expected:
# <meta name="go-import" content="go.moleculesai.app/core git https://git.moleculesai.app/molecule-ai/molecule-core">
```
End-to-end:
```bash
go install go.moleculesai.app/cli/cmd/molecule@latest
# Should resolve via the responder, fetch from Gitea, build a `molecule` binary on PATH.
```
## What this responder deliberately does NOT do
- Per-version/per-tag rewrite. We don't encode semver in the path; standard Go module versioning in `go.mod` does that.
- Authentication. Vanity-URL discovery is public read; `go get` itself authenticates against the actual SCM (`git.moleculesai.app`) for private repos.
- Rate limiting. Cloudflare's edge handles abuse; the worker doesn't store state.
## Related
- Issue: molecule-ai/internal#71
- Migration PRs: plugin-gh-identity#3, cli#2, controlplane#32, core#82

169
worker.js Normal file
View File

@ -0,0 +1,169 @@
// go-import responder for go.moleculesai.app
//
// Issue: molecule-ai/internal#71 — Migrate Go module paths off
// github.com/Molecule-AI to a vanity import host we control.
//
// What this does
// ───────────────
// When the Go toolchain runs `go get go.moleculesai.app/<area>/<sub>`,
// it issues a GET against `https://go.moleculesai.app/<area>/<sub>?go-get=1`
// and looks for a `<meta name="go-import">` tag. We respond with the tag
// pointing at our actual SCM (Gitea), letting the toolchain clone from
// there. The toolchain ALSO honours `<meta name="go-source">` for source
// browsing in pkg.go.dev / godoc.
//
// Why the indirection (and not just `git.moleculesai.app/...`)
// ───────────────────────────────────────────────────────────
// `git.moleculesai.app` is the SCM host we use today. If we ever migrate
// SCMs again (Forgejo? Codeberg? self-hosted Gerrit?), every import in
// every Go file would need to change. With the vanity layer, source
// code never names the SCM host — only this responder does. SCM moves
// edit one file (this one), not 500+ source lines.
//
// Map (locked at issue #71 resolution, 2026-05-07)
// ────────────────────────────────────────────────
// go.moleculesai.app/core/... → molecule-core
// go.moleculesai.app/controlplane/... → molecule-controlplane
// go.moleculesai.app/cli/... → molecule-cli
// go.moleculesai.app/plugin/gh-identity/... → molecule-ai-plugin-gh-identity
//
// Adding a new repo: append one entry to MODULE_MAP. No code change.
//
// Runtime
// ───────
// Targets the standard service-worker `fetch` handler — runs unchanged
// on Cloudflare Workers AND Vercel Edge Functions AND any other
// Web-API-compatible runtime. No platform-specific bindings.
// MODULE_MAP — ordered longest-prefix-first so /plugin/gh-identity wins
// before /plugin (if /plugin ever maps to something else).
const MODULE_MAP = [
{ prefix: "/plugin/gh-identity", repo: "molecule-ai-plugin-gh-identity" },
{ prefix: "/controlplane", repo: "molecule-controlplane" },
{ prefix: "/cli", repo: "molecule-cli" },
{ prefix: "/core", repo: "molecule-core" },
];
const SCM_HOST = "git.moleculesai.app";
const SCM_OWNER = "molecule-ai";
const VANITY_HOST = "go.moleculesai.app";
// Path-validation: reject anything go-get wouldn't legitimately ask for.
// The go toolchain sends paths with [a-z0-9./-] only; everything else is
// a probe or attack. Keep this tight — broaden only on a real consumer
// breakage, never on speculation.
const SAFE_PATH_RE = /^\/[a-z0-9._\-/]*$/i;
function findEntry(pathname) {
for (const entry of MODULE_MAP) {
if (pathname === entry.prefix || pathname.startsWith(entry.prefix + "/")) {
return entry;
}
}
return null;
}
function buildHTML(vanityRoot, repo) {
// vanityRoot is e.g. "go.moleculesai.app/core" — the "import root" the
// toolchain registers. Subpaths under it ride the same go-import tag.
const goImport = `${vanityRoot} git https://${SCM_HOST}/${SCM_OWNER}/${repo}`;
const goSource =
`${vanityRoot} ` +
`https://${SCM_HOST}/${SCM_OWNER}/${repo} ` +
`https://${SCM_HOST}/${SCM_OWNER}/${repo}/src/branch/main{/dir} ` +
`https://${SCM_HOST}/${SCM_OWNER}/${repo}/src/branch/main{/dir}/{file}#L{line}`;
return `<!doctype html>
<html><head>
<meta name="go-import" content="${goImport}">
<meta name="go-source" content="${goSource}">
<meta http-equiv="refresh" content="0; url=https://${SCM_HOST}/${SCM_OWNER}/${repo}">
<title>${vanityRoot}</title>
</head><body>
<p>Vanity import path for <code>${vanityRoot}</code>.</p>
<p>Source: <a href="https://${SCM_HOST}/${SCM_OWNER}/${repo}">https://${SCM_HOST}/${SCM_OWNER}/${repo}</a></p>
</body></html>`;
}
function rootResponse() {
// GET / — human-friendly index. go-get never lands here (it always
// hits a path like /core/...), but humans hitting the bare host
// shouldn't get a 404.
const lines = MODULE_MAP.map(
(e) => `<li><code>${VANITY_HOST}${e.prefix}</code> → ${SCM_HOST}/${SCM_OWNER}/${e.repo}</li>`
).join("\n");
return new Response(
`<!doctype html>
<html><head><title>${VANITY_HOST}</title></head><body>
<h1>${VANITY_HOST}</h1>
<p>Vanity import host for Molecules AI Go modules. The indirection
decouples Go module paths from the underlying SCM host so an SCM
migration touches one config (this responder) instead of every
import statement. See molecule-ai/internal#71.</p>
<h2>Module map</h2>
<ul>
${lines}
</ul>
<p>Source for this responder: <a href="https://${SCM_HOST}/${SCM_OWNER}/molecule-ai-vanity-import-responder">/molecule-ai-vanity-import-responder</a></p>
</body></html>`,
{ headers: { "content-type": "text/html; charset=utf-8", "cache-control": "public, max-age=300" } }
);
}
async function handleRequest(request) {
const url = new URL(request.url);
// Only GET / HEAD. Anything else is a probe.
if (request.method !== "GET" && request.method !== "HEAD") {
return new Response("method not allowed", { status: 405 });
}
// Path sanitation — see SAFE_PATH_RE rationale above.
if (!SAFE_PATH_RE.test(url.pathname) || url.pathname.length > 256 || url.pathname.includes("..")) {
return new Response("bad path", { status: 400 });
}
// Bare root — human index.
if (url.pathname === "/" || url.pathname === "") {
return rootResponse();
}
// Strip trailing slashes for prefix matching.
const cleanPath = url.pathname.replace(/\/+$/, "") || "/";
const entry = findEntry(cleanPath);
if (!entry) {
return new Response(`unknown vanity path: ${cleanPath}`, {
status: 404,
headers: { "content-type": "text/plain; charset=utf-8" },
});
}
// The "import root" registered with the go toolchain is the prefix —
// not the full requested path. /core/platform/handlers shares the
// same go-import root as /core/platform/db.
const vanityRoot = `${VANITY_HOST}${entry.prefix}`;
const html = buildHTML(vanityRoot, entry.repo);
return new Response(html, {
status: 200,
headers: {
"content-type": "text/html; charset=utf-8",
// Cache aggressively — the map only changes when we add/remove
// repos, which is rare. CDN respects this; clients honour it.
"cache-control": "public, max-age=3600, s-maxage=86400",
// Tell pkg.go.dev / proxy.golang.org to vary on go-get like the
// upstream Go vanity-redirector does.
"x-content-type-options": "nosniff",
},
});
}
// Cloudflare Workers entry point.
addEventListener("fetch", (event) => {
event.respondWith(handleRequest(event.request));
});
// Vercel Edge Function / generic export.
export default {
async fetch(request) {
return handleRequest(request);
},
};

14
wrangler.toml Normal file
View File

@ -0,0 +1,14 @@
name = "go-import-responder"
main = "worker.js"
compatibility_date = "2025-09-01"
# Bind the worker to go.moleculesai.app at the zone root + every path
# under it. The route pattern uses a wildcard so /core/platform,
# /controlplane, /cli, /plugin/gh-identity all hit the same worker.
routes = [
{ pattern = "go.moleculesai.app/*", zone_name = "moleculesai.app" },
]
# No KV / R2 / Durable Objects bindings — the responder is fully
# stateless and serves from an in-memory MODULE_MAP. Keeps cold-start
# < 5ms and removes a cache layer that could go stale.