-
Notifications
You must be signed in to change notification settings - Fork 0
Add standalone ChatGPT Sites mirror proxy (sites-proxy/) #16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| # trackdub-dev-proxy | ||
|
|
||
| A tiny Cloudflare Worker that mirrors **trackdub.com** under a separate | ||
| hostname (e.g. `trackdub.dev`) so external platforms — such as OpenAI | ||
| **ChatGPT Sites** — can serve a copy of the marketing site without touching | ||
| the canonical deployment. | ||
|
|
||
| - Forwards path, query, method, and body to the upstream origin. | ||
| - Rewrites same-site redirects to stay on the mirror; passes external | ||
| redirects (GitHub, etc.) through untouched. | ||
| - Sets `x-robots-tag: noindex, nofollow` and | ||
| `x-trackdub-mirror: chatgpt-sites` on every response so search engines | ||
| keep `trackdub.com` canonical. | ||
|
|
||
| ## Layout | ||
|
|
||
| | File | Purpose | | ||
| | ---------------- | -------------------------------------------------------- | | ||
| | `index.js` | The Worker (default export is the `fetch` handler) | | ||
| | `proxy.test.mjs` | Unit tests (node built-ins only, no dependencies) | | ||
| | `build.mjs` | Copies the worker into `dist/server/index.js` for deploy | | ||
| | `wrangler.jsonc` | Standalone deploy config for this worker | | ||
|
|
||
| This lives **entirely inside `sites-proxy/`** — it does not modify the app's | ||
| `package.json`, `wrangler.jsonc`, or any source. The two deployments are | ||
| fully independent. | ||
|
|
||
| ## Test | ||
|
|
||
| ```sh | ||
| node --test sites-proxy/proxy.test.mjs | ||
| ``` | ||
|
|
||
| ## Build & deploy | ||
|
|
||
| ```sh | ||
| node sites-proxy/build.mjs # emits dist/server/index.js | ||
| npx wrangler deploy --config sites-proxy/wrangler.jsonc | ||
| npx wrangler dev --config sites-proxy/wrangler.jsonc # local preview | ||
| ``` | ||
|
|
||
| `dist/` is already covered by the repo's `.gitignore`. | ||
|
|
||
| ## Configuration | ||
|
|
||
| The upstream origin defaults to `https://trackdub.com`. To point the mirror | ||
| at a different origin (e.g. a staging deployment), set the `UPSTREAM_ORIGIN` | ||
| variable in `sites-proxy/wrangler.jsonc` under `vars`. | ||
|
|
||
| ## Notes | ||
|
|
||
| - Redirect rewriting only rewrites hosts that match the upstream origin | ||
| (bare and `www.` variants); everything else is preserved. | ||
| - The worker reads only `UPSTREAM_ORIGIN` from `env` — the Sites | ||
| `(request, env, ctx)` signature is otherwise ignored. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| import { cp, mkdir, rm } from "node:fs/promises"; | ||
|
|
||
| // Emit into sites-proxy/dist so the path stays relative to this directory | ||
| // (wrangler resolves "main" relative to the config file). | ||
| const outDir = new URL("./dist/server/", import.meta.url); | ||
|
|
||
| await rm(outDir, { recursive: true, force: true }); | ||
| await mkdir(outDir, { recursive: true }); | ||
| await cp(new URL("./index.js", import.meta.url), new URL("./index.js", outDir)); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| const DEFAULT_UPSTREAM_ORIGIN = "https://trackdub.com"; | ||
|
|
||
| function upstreamOriginSet(upstreamOrigin) { | ||
| const base = new URL(upstreamOrigin); | ||
| const hostnames = [ | ||
| base.hostname, | ||
| base.hostname.startsWith("www.") ? base.hostname.slice(4) : `www.${base.hostname}`, | ||
| ]; | ||
| return new Set( | ||
| hostnames.map((hostname) => { | ||
| const candidate = new URL(upstreamOrigin); | ||
| candidate.hostname = hostname; | ||
| return candidate.origin; | ||
| }), | ||
| ); | ||
| } | ||
|
|
||
| function rewriteLocation(location, upstreamUrl, downstreamUrl, origins) { | ||
| const target = new URL(location, upstreamUrl); | ||
| // Compare origin (scheme + host + port), not hostname alone, so a redirect like | ||
| // https://trackdub.com:8443/path is left alone instead of being rewritten to the | ||
| // mirror's default port. | ||
| if (!origins.has(target.origin)) return location; | ||
|
|
||
| target.protocol = downstreamUrl.protocol; | ||
| // Assign hostname and port separately. Setting `host` alone can retain an | ||
| // upstream non-default port (e.g. :8443) on the mirror URL. | ||
| target.hostname = downstreamUrl.hostname; | ||
| target.port = downstreamUrl.port; | ||
| return target.toString(); | ||
| } | ||
|
|
||
| export async function proxyRequest( | ||
| request, | ||
| fetchImpl = fetch, | ||
| upstreamOrigin = DEFAULT_UPSTREAM_ORIGIN, | ||
| ) { | ||
| const downstreamUrl = new URL(request.url); | ||
| const upstreamUrl = new URL(downstreamUrl.pathname + downstreamUrl.search, upstreamOrigin); | ||
| const upstreamRequest = new Request(upstreamUrl, request); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: The worker forwards every incoming header (including Prompt for AI agents |
||
| const upstreamResponse = await fetchImpl(upstreamRequest, { redirect: "manual" }); | ||
| const headers = new Headers(upstreamResponse.headers); | ||
| const location = headers.get("location"); | ||
|
|
||
| if (location) { | ||
| headers.set( | ||
| "location", | ||
| rewriteLocation(location, upstreamUrl, downstreamUrl, upstreamOriginSet(upstreamOrigin)), | ||
| ); | ||
| } | ||
|
|
||
| // Keep the mirror out of search indexes so trackdub.com remains canonical. | ||
| headers.set("x-robots-tag", "noindex, nofollow"); | ||
| headers.set("x-trackdub-mirror", "chatgpt-sites"); | ||
|
|
||
| return new Response(upstreamResponse.body, { | ||
| status: upstreamResponse.status, | ||
| statusText: upstreamResponse.statusText, | ||
| headers, | ||
| }); | ||
| } | ||
|
|
||
| export default { | ||
| fetch(request, env) { | ||
| // Sites passes (request, env, ctx); only UPSTREAM_ORIGIN is read from env. | ||
| const upstreamOrigin = env?.UPSTREAM_ORIGIN || DEFAULT_UPSTREAM_ORIGIN; | ||
| return proxyRequest(request, undefined, upstreamOrigin); | ||
| }, | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,137 @@ | ||
| import assert from "node:assert/strict"; | ||
| import test from "node:test"; | ||
|
|
||
| import worker, { proxyRequest } from "./index.js"; | ||
|
|
||
| test("forwards the path and query to trackdub.com", async () => { | ||
| let upstreamRequest; | ||
| const response = await proxyRequest( | ||
| new Request("https://trackdub.dev/pricing?currency=usd"), | ||
| async (request) => { | ||
| upstreamRequest = request; | ||
| return new Response("ok", { headers: { "content-type": "text/plain" } }); | ||
| }, | ||
| ); | ||
|
|
||
| assert.equal(upstreamRequest.url, "https://trackdub.com/pricing?currency=usd"); | ||
| assert.equal(await response.text(), "ok"); | ||
| assert.equal(response.headers.get("x-trackdub-mirror"), "chatgpt-sites"); | ||
| assert.equal(response.headers.get("x-robots-tag"), "noindex, nofollow"); | ||
| }); | ||
|
|
||
| test("forwards request methods and bodies", async () => { | ||
| let upstreamRequest; | ||
| const response = await proxyRequest( | ||
| new Request("https://trackdub.dev/api/waitlist", { | ||
| method: "POST", | ||
| body: JSON.stringify({ email: "person@example.com" }), | ||
| headers: { "content-type": "application/json" }, | ||
| }), | ||
| async (request) => { | ||
| upstreamRequest = request; | ||
| return new Response(null, { status: 204 }); | ||
| }, | ||
| ); | ||
|
|
||
| assert.equal(upstreamRequest.method, "POST"); | ||
| assert.equal(await upstreamRequest.text(), JSON.stringify({ email: "person@example.com" })); | ||
| assert.equal(response.status, 204); | ||
| }); | ||
|
|
||
| test("keeps same-site redirects on trackdub.dev", async () => { | ||
| const response = await proxyRequest( | ||
| new Request("https://trackdub.dev/old"), | ||
| async () => | ||
| new Response(null, { | ||
| status: 302, | ||
| headers: { location: "https://trackdub.com/new?from=old" }, | ||
| }), | ||
| ); | ||
|
|
||
| assert.equal(response.headers.get("location"), "https://trackdub.dev/new?from=old"); | ||
| }); | ||
|
|
||
| test("does not rewrite same-hostname redirects on a different port", async () => { | ||
| const response = await proxyRequest( | ||
| new Request("https://trackdub.dev/old"), | ||
| async () => | ||
| new Response(null, { | ||
| status: 302, | ||
| headers: { location: "https://trackdub.com:8443/internal" }, | ||
| }), | ||
| ); | ||
|
|
||
| assert.equal(response.headers.get("location"), "https://trackdub.com:8443/internal"); | ||
| }); | ||
|
|
||
| test("rewrites www upstream redirects that share the configured origin port", async () => { | ||
| const response = await proxyRequest( | ||
| new Request("https://trackdub.dev/old"), | ||
| async () => | ||
| new Response(null, { | ||
| status: 302, | ||
| headers: { location: "https://www.trackdub.com/new" }, | ||
| }), | ||
| ); | ||
|
|
||
| assert.equal(response.headers.get("location"), "https://trackdub.dev/new"); | ||
| }); | ||
|
|
||
| test("clears an upstream explicit port when rewriting to the mirror", async () => { | ||
| const response = await proxyRequest( | ||
| new Request("https://trackdub.dev/old"), | ||
| async () => | ||
| new Response(null, { | ||
| status: 302, | ||
| headers: { location: "https://staging.trackdub.com:9443/new" }, | ||
| }), | ||
| "https://staging.trackdub.com:9443", | ||
| ); | ||
|
|
||
| assert.equal(response.headers.get("location"), "https://trackdub.dev/new"); | ||
| }); | ||
|
|
||
| test("preserves redirects to external sites", async () => { | ||
| const response = await proxyRequest( | ||
| new Request("https://trackdub.dev/docs"), | ||
| async () => | ||
| new Response(null, { | ||
| status: 302, | ||
| headers: { location: "https://github.com/trackdubllc" }, | ||
| }), | ||
| ); | ||
|
|
||
| assert.equal(response.headers.get("location"), "https://github.com/trackdubllc"); | ||
| }); | ||
|
|
||
| test("honors a custom UPSTREAM_ORIGIN via env", async () => { | ||
| const originalFetch = globalThis.fetch; | ||
| let upstreamUrl; | ||
| globalThis.fetch = async (request) => { | ||
| upstreamUrl = request.url; | ||
| return new Response(null, { status: 200 }); | ||
| }; | ||
|
|
||
| try { | ||
| const response = await worker.fetch(new Request("https://mirror.example/faq"), { | ||
| UPSTREAM_ORIGIN: "https://staging.trackdub.com", | ||
| }); | ||
| assert.equal(upstreamUrl, "https://staging.trackdub.com/faq"); | ||
| assert.equal(response.status, 200); | ||
| assert.equal(response.headers.get("x-trackdub-mirror"), "chatgpt-sites"); | ||
| } finally { | ||
| globalThis.fetch = originalFetch; | ||
| } | ||
| }); | ||
|
|
||
| test("ignores unrelated Sites env arguments passed to the worker entrypoint", async () => { | ||
| const originalFetch = globalThis.fetch; | ||
| globalThis.fetch = async () => new Response("proxied"); | ||
|
|
||
| try { | ||
| const response = await worker.fetch(new Request("https://trackdub.dev/"), { WAITLIST_DB: {} }); | ||
| assert.equal(await response.text(), "proxied"); | ||
| } finally { | ||
| globalThis.fetch = originalFetch; | ||
| } | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| { | ||
| "$schema": "../node_modules/wrangler/config-schema.json", | ||
| "name": "trackdub-dev-proxy", | ||
| "account_id": "21cac5947e11018d571c18792118b8b0", | ||
| "main": "dist/server/index.js", | ||
| "compatibility_date": "2025-07-25", | ||
| "observability": { | ||
| "logs": { | ||
| "enabled": true, | ||
| "invocation_logs": true, | ||
| }, | ||
| "traces": { | ||
| "enabled": true, | ||
| }, | ||
| }, | ||
| "vars": { | ||
| // Optional: override the upstream origin. Defaults to https://trackdub.com. | ||
| // "UPSTREAM_ORIGIN": "https://trackdub.com", | ||
| }, | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SUGGESTION: Em dashes in user-facing prose
Lines 4, 5, and 54 use em dashes (
—) in user-facing prose. Replace with standard dashes or rephrase to comply with the style guide.Reply with
@kilocode-bot fix itto have Kilo Code address this issue.