From 0a39a7a981f619ac64fdeb26294c7cfd4f110a1a Mon Sep 17 00:00:00 2001 From: Joe Mou Date: Tue, 11 Aug 2026 11:39:01 -0400 Subject: Redirect to GitHub with a meta refresh A browser extension redirects github.com to cgithub with declarative net requests. Its rules match our HTTP redirects back to github.com too, so those bounce straight back here and loop forever. A meta refresh makes cgithub the initiator origin instead, which the extension's rules can exclude, and its delay leaves a page the user can stop on. Co-Authored-By: Claude Opus 5 --- README.md | 30 ++++++++++++++++++++++++++++++ package.json | 6 ++++-- src/app.test.ts | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/app.ts | 35 +++++++++++++++++++++-------------- views/redirect.eta | 14 ++++++++++++++ 5 files changed, 119 insertions(+), 16 deletions(-) create mode 100644 src/app.test.ts create mode 100644 views/redirect.eta diff --git a/README.md b/README.md index 3bcf017..a625116 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,36 @@ $ pnpm run deploy The worker name and other settings live in `wrangler.jsonc`. +## Browser extension redirects + +Pages cgithub can't render (marketing pages, code search, anything unrouted) are +redirected back to github.com. A browser extension that rewrites github.com URLs +to a cgithub instance with `declarativeNetRequest` will match those redirects +too and send the request straight back, looping forever. + +To avoid this, cgithub never answers with an HTTP redirect: it serves a small +page that navigates to github.com with a two-second ``. +Because that navigation is initiated by the cgithub origin, the extension can +let it through with `excludedInitiatorDomains`; without that, the user at least +lands on a page they can stop on, with a link, instead of an endless loop. + +A rule that redirects to a cgithub instance should therefore look like this: + +```jsonc +{ + "action": { + "type": "redirect", + "redirect": { "transform": { "host": "cgithub.example" } }, + }, + "condition": { + "urlFilter": "||github.com", + "resourceTypes": ["main_frame"], + // So cgithub's own meta refresh to github.com isn't redirected back here. + "excludedInitiatorDomains": ["cgithub.example"], + }, +} +``` + ## Disclosures AI coding assistants, in particular Claude, are used in the development process. diff --git a/package.json b/package.json index 7673612..ce25701 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "scripts": { "dev": "node scripts/build.ts && wrangler dev", "deploy": "node scripts/build.ts && wrangler deploy", - "test": "node --test --test-concurrency=4 src/scraper.test.ts" + "test": "node --test --test-concurrency=4 src/app.test.ts src/scraper.test.ts" }, "dependencies": { "css-select": "^6.0.0", @@ -18,7 +18,9 @@ "htmlparser2": "^10.1.0" }, "pnpm": { - "onlyBuiltDependencies": ["workerd"] + "onlyBuiltDependencies": [ + "workerd" + ] }, "devDependencies": { "@types/node": "^22.10.2", diff --git a/src/app.test.ts b/src/app.test.ts new file mode 100644 index 0000000..498d8a6 --- /dev/null +++ b/src/app.test.ts @@ -0,0 +1,50 @@ +import assert from "node:assert"; +import path from "node:path"; +import { describe, it } from "node:test"; +import { Eta } from "eta"; +import { createApp } from "./app.ts"; + +// The routes exercised here redirect without scraping, so no network is needed. +const eta = new Eta({ views: path.join(import.meta.dirname, "..", "views") }); +const app = createApp(eta); + +describe("redirects to GitHub", () => { + // An HTTP redirect would be caught by the extension's declarativeNetRequest + // rules and bounced straight back here. + it("should redirect unknown paths with a meta refresh", async () => { + const res = await app.request("http://cgithub.example/a/b/c?x=1"); + + assert.strictEqual(res.status, 200); + assert.strictEqual(res.headers.get("location"), null); + assert.strictEqual(res.headers.get("Referrer-Policy"), "no-referrer"); + + const body = await res.text(); + assert.match( + body, + //, + ); + assert.doesNotMatch(body, /