Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
55 changes: 55 additions & 0 deletions sites-proxy/README.md
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

Copy link
Copy Markdown

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 it to have Kilo Code address this issue.

**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.
9 changes: 9 additions & 0 deletions sites-proxy/build.mjs
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));
69 changes: 69 additions & 0 deletions sites-proxy/index.js
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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: The worker forwards every incoming header (including Cookie/Authorization) to the upstream and passes the upstream's Set-Cookie back unmodified to mirror visitors. Since the mirror and trackdub.com are different origins, this lets each side read/write the other's cookies through the proxy — a visitor's mirror cookies are sent to the canonical site, and any cookie the upstream sets ends up stored and re-sent under the mirror host. For a public marketing mirror this is low impact today, but it becomes a cross-origin session-leak if either endpoint ever carries auth or if ChatGPT Sites' requests include credentials. Consider stripping cookie/authorization from the upstream request and filtering set-cookie (or namespacing/overriding the Domain) on the response for this unauthenticated mirror.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At sites-proxy/index.js, line 24:

<comment>The worker forwards every incoming header (including `Cookie`/`Authorization`) to the upstream and passes the upstream's `Set-Cookie` back unmodified to mirror visitors. Since the mirror and `trackdub.com` are different origins, this lets each side read/write the other's cookies through the proxy — a visitor's mirror cookies are sent to the canonical site, and any cookie the upstream sets ends up stored and re-sent under the mirror host. For a public marketing mirror this is low impact today, but it becomes a cross-origin session-leak if either endpoint ever carries auth or if ChatGPT Sites' requests include credentials. Consider stripping `cookie`/`authorization` from the upstream request and filtering `set-cookie` (or namespacing/overriding the `Domain`) on the response for this unauthenticated mirror.</comment>

<file context>
@@ -0,0 +1,53 @@
+) {
+  const downstreamUrl = new URL(request.url);
+  const upstreamUrl = new URL(downstreamUrl.pathname + downstreamUrl.search, upstreamOrigin);
+  const upstreamRequest = new Request(upstreamUrl, request);
+  const upstreamResponse = await fetchImpl(upstreamRequest, { redirect: "manual" });
+  const headers = new Headers(upstreamResponse.headers);
</file context>

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);
},
};
137 changes: 137 additions & 0 deletions sites-proxy/proxy.test.mjs
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;
}
});
20 changes: 20 additions & 0 deletions sites-proxy/wrangler.jsonc
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",
},
}