From ba8e2340d296f6ca090a0b40a970b9555c46e2c5 Mon Sep 17 00:00:00 2001 From: Ade Adedo Date: Sun, 9 Aug 2026 00:42:58 +0100 Subject: [PATCH] Add Fixars product path connectors --- landing-page/package-lock.json | 22 +++++------- landing-page/tests/sites-worker.test.mjs | 44 ++++++++++++++++++++++++ landing-page/worker/index.js | 33 ++++++++++++++++++ 3 files changed, 85 insertions(+), 14 deletions(-) diff --git a/landing-page/package-lock.json b/landing-page/package-lock.json index 1b8c252..1fe4420 100644 --- a/landing-page/package-lock.json +++ b/landing-page/package-lock.json @@ -12,8 +12,7 @@ "react": "19.2.0", "react-dom": "19.2.0", "vite": "6.4.3" - }, - "devDependencies": {} + } }, "node_modules/@babel/code-frame": { "version": "7.29.7", @@ -43,7 +42,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -1169,7 +1167,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.44", "caniuse-lite": "^1.0.30001806", @@ -1369,9 +1366,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "funding": [ { "type": "github", @@ -1406,7 +1403,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -1415,9 +1411,9 @@ } }, "node_modules/postcss": { - "version": "8.5.22", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.22.tgz", - "integrity": "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==", + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", "funding": [ { "type": "opencollective", @@ -1434,7 +1430,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.16", + "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -1447,7 +1443,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -1592,7 +1587,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", diff --git a/landing-page/tests/sites-worker.test.mjs b/landing-page/tests/sites-worker.test.mjs index 64f277e..8ba1bbe 100644 --- a/landing-page/tests/sites-worker.test.mjs +++ b/landing-page/tests/sites-worker.test.mjs @@ -3,6 +3,50 @@ import { access } from "node:fs/promises"; import test from "node:test"; import worker from "../worker/index.js"; +test("permanently redirects product connector paths to canonical domains", async () => { + const cases = [ + ["/skills", "https://skillscanvas.co/"], + ["/skills/passport?source=hub", "https://skillscanvas.co/passport?source=hub"], + ["/concepts/ideas/42", "https://conceptsnexus.co/ideas/42"], + ["/collab/capsules/demo", "https://collaboard.co/capsules/demo"], + ["/vest/projects/demo", "https://vestden.co/projects/demo"], + ]; + + for (const [pathname, expectedLocation] of cases) { + let assetCalls = 0; + const response = await worker.fetch(new Request(`https://fixars.ai${pathname}`), { + ASSETS: { + fetch: async () => { + assetCalls += 1; + return new Response("unexpected", { status: 500 }); + }, + }, + }); + + assert.equal(response.status, 308); + assert.equal(response.headers.get("location"), expectedLocation); + assert.equal(assetCalls, 0); + } +}); + +test("does not redirect write requests through connector aliases", async () => { + let assetCalls = 0; + const response = await worker.fetch( + new Request("https://fixars.ai/skills/passport", { method: "POST" }), + { + ASSETS: { + fetch: async () => { + assetCalls += 1; + return new Response("missing", { status: 404 }); + }, + }, + }, + ); + + assert.equal(response.status, 404); + assert.equal(assetCalls, 1); +}); + test("serves existing static assets without a fallback", async () => { const calls = []; const response = await worker.fetch(new Request("https://example.test/assets/app.js"), { diff --git a/landing-page/worker/index.js b/landing-page/worker/index.js index 5f35412..3928f55 100644 --- a/landing-page/worker/index.js +++ b/landing-page/worker/index.js @@ -1,5 +1,38 @@ +const PRODUCT_CONNECTORS = new Map([ + ["skills", "https://skillscanvas.co"], + ["concepts", "https://conceptsnexus.co"], + ["collab", "https://collaboard.co"], + ["vest", "https://vestden.co"], +]); + +function productConnectorRedirect(request) { + if (!["GET", "HEAD"].includes(request.method)) { + return null; + } + + const incomingUrl = new URL(request.url); + const [, connector, ...remainder] = incomingUrl.pathname.split("/"); + const canonicalOrigin = PRODUCT_CONNECTORS.get(connector); + + if (!canonicalOrigin) { + return null; + } + + // These server-side connector paths are aliases only. Product domains remain + // canonical deployment origins; the suffix and query string cross intact. + const destination = new URL(canonicalOrigin); + destination.pathname = remainder.length ? `/${remainder.join("/")}` : "/"; + destination.search = incomingUrl.search; + return Response.redirect(destination, 308); +} + export default { async fetch(request, env) { + const connectorRedirect = productConnectorRedirect(request); + if (connectorRedirect) { + return connectorRedirect; + } + const response = await env.ASSETS.fetch(request); const acceptsHtml = request.headers.get("accept")?.includes("text/html");