Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
22 changes: 21 additions & 1 deletion src/app/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,24 @@ import { headers } from "next/headers";
import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server";

const normaliseReferrer = (referrer: string | undefined) => {
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

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

Naming: the repository uses normalize* in other utils; consider renaming normaliseReferrer to normalizeReferrer here as well for consistency and discoverability.

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

let current = referrer;

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

return current;
};
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

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

This normaliseReferrer logic duplicates the helper added in src/utils/attributionUtils.ts. Consider extracting a shared helper and importing it, so the client and server normalisation rules don’t diverge over time.

Copilot uses AI. Check for mistakes.

export const signUpAction = async (formData: FormData, request?: Request) => {
const t = await getTranslations("Errors");
const email = formData.get("email")?.toString();
Expand All @@ -28,7 +46,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()
);
Copy link

Copilot AI Apr 19, 2026

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
Copy link

Copilot AI Apr 18, 2026

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
30 changes: 27 additions & 3 deletions src/utils/attributionUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,28 @@ const getCookie = (name: string): string | null => {
if (!value) return null;

try {
return decodeURIComponent(value);
return normaliseReferrer(value);
} catch {
return value;
}
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

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

getCookie wraps normaliseReferrer(value) in a try/catch, but normaliseReferrer already catches decodeURIComponent errors internally and never throws. This try/catch is effectively dead code; consider removing it (or simplify normaliseReferrer to let errors bubble if you prefer handling them here).

Copilot uses AI. Check for mistakes.
};

const normaliseReferrer = (referrer: string): string => {
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

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

Naming: the codebase already uses American spelling in similar helpers (e.g. normalizeNextPath, normalizeAssetPath). Consider renaming normaliseReferrer to normalizeReferrer for consistency and easier searchability.

Suggested change
return normaliseReferrer(value);
} catch {
return value;
}
};
const normaliseReferrer = (referrer: string): string => {
return normalizeReferrer(value);
} catch {
return value;
}
};
const normalizeReferrer = (referrer: string): string => {

Copilot uses AI. Check for mistakes.
let current = referrer;

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

return current;
};
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

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

normaliseReferrer is now implemented in both src/utils/attributionUtils.ts (client) and src/app/actions.ts (server) with the same logic. To avoid the two drifting over time, consider extracting this into a shared utility (e.g. src/utils/referrer.ts) and importing it from both places.

Copilot uses AI. Check for mistakes.

const getExternalDocumentReferrer = (): string | null => {
const referrer = document.referrer;

Expand All @@ -72,7 +88,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 +141,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 &&
Copy link

Copilot AI Apr 19, 2026

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
Loading