Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
6 changes: 0 additions & 6 deletions .cursor/rules/file-format-preference.mdc

This file was deleted.

5 changes: 4 additions & 1 deletion src/app/actions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use server";

import { validateName } from "@/lib/formValidation";
import { normaliseReferrer } from "@/utils/referrer";
import { createClient } from "@/utils/supabase/server";
import { getBaseUrl } from "@/utils/url";
import {
Expand Down Expand Up @@ -28,7 +29,9 @@ export const signUpAction = async (formData: FormData, request?: Request) => {
const origin = headersList.get("origin");

// Get attribution data
const referrer = formData.get("initial_referrer")?.toString();
const referrer = normaliseReferrer(
formData.get("initial_referrer")?.toString()
);

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

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

normaliseReferrer is applied to formData.get("initial_referrer"), which is user-controllable. Because it can decode percent-encoded payloads, it can introduce control characters (e.g. %0d%0a) that then get logged and persisted to Supabase user metadata. Consider sanitizing the normalized value (strip control chars / enforce a max length) and/or validating it as an http(s) URL before storing/logging.

Copilot uses AI. Check for mistakes.
const utmSource = formData.get("utm_source")?.toString();
const utmMedium = formData.get("utm_medium")?.toString();
const utmCampaign = formData.get("utm_campaign")?.toString();
Expand Down
18 changes: 7 additions & 11 deletions src/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,13 @@ export async function proxy(request: NextRequest) {
externalReferrer &&
request.method === "GET"
) {
response.cookies.set(
INITIAL_REFERRER_COOKIE,
encodeURIComponent(externalReferrer),
{
httpOnly: false,
maxAge: INITIAL_REFERRER_MAX_AGE,
path: "/",
sameSite: "lax",
secure: request.nextUrl.protocol === "https:",
}
);
response.cookies.set(INITIAL_REFERRER_COOKIE, externalReferrer, {
httpOnly: false,
maxAge: INITIAL_REFERRER_MAX_AGE,
path: "/",
sameSite: "lax",
secure: request.nextUrl.protocol === "https:",
});
Comment on lines +39 to +45

Copilot AI Apr 18, 2026

Copy link

Choose a reason for hiding this comment

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

Cookie values can’t safely contain certain separator characters (notably ;, ,, and whitespace). Since this now stores the raw externalReferrer string, a referrer path containing one of those characters could lead to a truncated/invalid cookie in browsers. Consider ensuring the value is cookie-safe here (e.g. rely on a cookie serializer that encodes, or explicitly encode/sanitize before calling cookies.set).

Copilot uses AI. Check for mistakes.
}

return response;
Expand Down
20 changes: 13 additions & 7 deletions src/utils/attributionUtils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"use client";

import { normaliseReferrer } from "@/utils/referrer";

Comment thread
dnywh marked this conversation as resolved.
const UTM_STORAGE_KEY = "attribution_params";
const INITIAL_REFERRER_KEY = "initial_referrer";
const INITIAL_REFERRER_COOKIE = "initial_referrer";
Expand Down Expand Up @@ -46,11 +48,7 @@ const getCookie = (name: string): string | null => {

if (!value) return null;

try {
return decodeURIComponent(value);
} catch {
return value;
}
return normaliseReferrer(value) ?? null;

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

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

getCookie now returns normaliseReferrer(value) directly. Since normaliseReferrer can legally return an empty string (e.g. input contains only control characters), this function can return "" instead of null, which then blocks fallbacks that use nullish coalescing (??). Consider treating empty/whitespace-only results as null (e.g. trim + return null when empty) and drop the redundant ?? null (the overload for string returns string).

Suggested change
return normaliseReferrer(value) ?? null;
const normalisedValue = normaliseReferrer(value);
if (!normalisedValue.trim()) return null;
return normalisedValue;

Copilot uses AI. Check for mistakes.
};

const getExternalDocumentReferrer = (): string | null => {
Expand All @@ -72,7 +70,7 @@ const getExternalDocumentReferrer = (): string | null => {
};

const storeInitialReferrer = (referrer: string) => {
localStorage.setItem(INITIAL_REFERRER_KEY, referrer);
localStorage.setItem(INITIAL_REFERRER_KEY, normaliseReferrer(referrer));
};

export function captureAttributionParams() {
Expand Down Expand Up @@ -125,10 +123,18 @@ export function getStoredAttributionParams(): StoredAttributionParams {
const stored = localStorage.getItem(UTM_STORAGE_KEY);
const storedInitialReferrer = localStorage.getItem(INITIAL_REFERRER_KEY);
const cookieReferrer = getCookie(INITIAL_REFERRER_COOKIE);
const initialReferrer = storedInitialReferrer ?? cookieReferrer;
const initialReferrer = storedInitialReferrer
? normaliseReferrer(storedInitialReferrer)
: cookieReferrer;

if (!storedInitialReferrer && initialReferrer) {
storeInitialReferrer(initialReferrer);
} else if (
storedInitialReferrer &&

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

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

storedInitialReferrer comes from localStorage.getItem(...) (type string | null), but the new conditional uses truthiness. This changes behavior vs the previous ?? logic: an empty string will now be treated as missing and fall back to the cookie. Use an explicit null check (e.g., storedInitialReferrer !== null) so empty-string values don’t change control flow unintentionally.

Suggested change
const initialReferrer = storedInitialReferrer
? normaliseReferrer(storedInitialReferrer)
: cookieReferrer;
if (!storedInitialReferrer && initialReferrer) {
storeInitialReferrer(initialReferrer);
} else if (
storedInitialReferrer &&
const initialReferrer =
storedInitialReferrer !== null
? normaliseReferrer(storedInitialReferrer)
: cookieReferrer;
if (storedInitialReferrer === null && initialReferrer) {
storeInitialReferrer(initialReferrer);
} else if (
storedInitialReferrer !== null &&

Copilot uses AI. Check for mistakes.
initialReferrer &&
initialReferrer !== storedInitialReferrer
) {
storeInitialReferrer(initialReferrer);
}

return {
Expand Down
24 changes: 24 additions & 0 deletions src/utils/referrer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const encodedReferrerPrefix = /^https?%(?:25)*3a%(?:25)*2f%(?:25)*2f/i;

export function normaliseReferrer(referrer: string): string;
export function normaliseReferrer(referrer: undefined): undefined;
export function normaliseReferrer(
referrer: string | undefined
): string | undefined;
export function normaliseReferrer(referrer: string | undefined) {
Comment on lines +5 to +10

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

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

normaliseReferrer uses British spelling, but the rest of the codebase appears to consistently use normalize* (e.g. normalizeNextPath, normalizeAssetPath). Consider renaming to normalizeReferrer (and updating imports/usages) to keep naming consistent and improve discoverability.

Copilot uses AI. Check for mistakes.
if (referrer === undefined) return undefined;

let current = referrer;

for (let i = 0; i < 3 && encodedReferrerPrefix.test(current); i += 1) {
try {
const decoded = decodeURIComponent(current);
if (decoded === current) break;
current = decoded;
} catch {
break;
}
}

return current;
}
Loading