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:
commit
56306dd237
65
README.md
Normal file
65
README.md
Normal 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
169
worker.js
Normal 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
14
wrangler.toml
Normal 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.
|
||||
Loading…
Reference in New Issue
Block a user