Skip to content
Merged
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
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 { getSafeHttpReferrer } 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 = getSafeHttpReferrer(
formData.get("initial_referrer")?.toString()
);
const utmSource = formData.get("utm_source")?.toString();
const utmMedium = formData.get("utm_medium")?.toString();
const utmCampaign = formData.get("utm_campaign")?.toString();
Expand Down
4 changes: 2 additions & 2 deletions src/app/auth/callback/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createClient } from "@/utils/supabase/server";
import { appendSuccessParam, normalizeNextPath } from "@/utils/authRedirects";
import { appendSuccessParam, normaliseNextPath } from "@/utils/authRedirects";
import { NextResponse } from "next/server";

const isAuthDebugEnabled = process.env.NEXT_PUBLIC_AUTH_DEBUG === "true";
Expand All @@ -23,7 +23,7 @@ export async function GET(request: Request) {
const requestedNextPath =
requestUrl.searchParams.get("next") ??
requestUrl.searchParams.get("redirect_to");
const nextPath = normalizeNextPath(requestedNextPath, "/profile");
const nextPath = normaliseNextPath(requestedNextPath, "/profile");

if (code) {
const supabase = await createClient();
Expand Down
4 changes: 2 additions & 2 deletions src/app/auth/confirm/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
appendSuccessParam,
getDefaultNextPathByType,
isSupportedEmailAuthType,
normalizeNextPath,
normaliseNextPath,
} from "@/utils/authRedirects";
import { NextResponse } from "next/server";

Expand Down Expand Up @@ -35,7 +35,7 @@ export async function GET(request: Request) {
requestUrl.searchParams.get("next") ??
requestUrl.searchParams.get("redirect_to");
const defaultNextPath = getDefaultNextPathByType(authType);
const nextPath = normalizeNextPath(requestedNextPath, defaultNextPath);
const nextPath = normaliseNextPath(requestedNextPath, defaultNextPath);

if (!tokenHash || !isSupportedEmailAuthType(authType)) {
debugAuth("invalid-confirm-query", {
Expand Down
4 changes: 2 additions & 2 deletions src/app/auth/session/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
appendSuccessParam,
getDefaultNextPathByType,
isSupportedEmailAuthType,
normalizeNextPath,
normaliseNextPath,
} from "@/utils/authRedirects";
import { NextResponse } from "next/server";

Expand Down Expand Up @@ -54,7 +54,7 @@ export async function POST(request: Request) {
}

const defaultNextPath = getDefaultNextPathByType(type);
const nextPath = normalizeNextPath(
const nextPath = normaliseNextPath(
typeof body?.next === "string" ? body.next : null,
defaultNextPath
);
Expand Down
8 changes: 4 additions & 4 deletions src/components/AuthHashCompletion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useEffect } from "react";
import {
getDefaultNextPathByType,
isSupportedEmailAuthType,
normalizeNextPath,
normaliseNextPath,
} from "@/utils/authRedirects";

const INVALID_LINK_MESSAGE =
Expand Down Expand Up @@ -37,7 +37,7 @@ export default function AuthHashCompletion() {
queryParams.get("next") ?? queryParams.get("redirect_to");
const requestedType = queryParams.get("type") ?? hashType;
const defaultNextPath = getDefaultNextPathByType(requestedType);
const nextPath = normalizeNextPath(preferredNextPath, defaultNextPath);
const nextPath = normaliseNextPath(preferredNextPath, defaultNextPath);

const authCode = queryParams.get("code");
const hasAuthHashPayload =
Expand Down Expand Up @@ -126,7 +126,7 @@ export default function AuthHashCompletion() {
return;
}

const typedNextPath = normalizeNextPath(
const typedNextPath = normaliseNextPath(
preferredNextPath,
getDefaultNextPathByType(type)
);
Expand Down Expand Up @@ -177,7 +177,7 @@ export default function AuthHashCompletion() {
return;
}

const resolvedNextPath = normalizeNextPath(data.next, typedNextPath);
const resolvedNextPath = normaliseNextPath(data.next, typedNextPath);
debugAuth("session-finalized", {
type,
nextPath: resolvedNextPath,
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
28 changes: 20 additions & 8 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,12 @@ const getCookie = (name: string): string | null => {

if (!value) return null;

try {
return decodeURIComponent(value);
} catch {
return value;
}
const cleanedValue = normaliseReferrer(value);
const trimmedValue = cleanedValue.trim();

if (!trimmedValue) return null;

return trimmedValue;
};

const getExternalDocumentReferrer = (): string | null => {
Expand All @@ -72,7 +75,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,9 +128,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 !== null
? normaliseReferrer(storedInitialReferrer)
: cookieReferrer;

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

Expand Down
6 changes: 3 additions & 3 deletions src/utils/authRedirects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const getDefaultNextPathByType = (type: string | null | undefined) => {
return "/profile";
};

export const normalizeNextPath = (
export const normaliseNextPath = (
candidatePath: string | null | undefined,
fallbackPath: string
) => {
Expand All @@ -49,8 +49,8 @@ export const normalizeNextPath = (
};

export const appendSuccessParam = (path: string, successValue: string) => {
const normalizedPath = normalizeNextPath(path, "/profile");
const url = new URL(normalizedPath, "https://www.peels.app");
const normalisedPath = normaliseNextPath(path, "/profile");
const url = new URL(normalisedPath, "https://www.peels.app");
url.searchParams.set("success", successValue);
return `${url.pathname}${url.search}${url.hash}`;
};
47 changes: 47 additions & 0 deletions src/utils/referrer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
const encodedReferrerPrefix = /^https?%(?:25)*3a%(?:25)*2f%(?:25)*2f/i;
const controlCharacters = /[\u0000-\u001f\u007f]/g;
const MAX_REFERRER_LENGTH = 512;

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
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 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.trim();

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.replace(controlCharacters, "").slice(0, MAX_REFERRER_LENGTH);
}

export function getSafeHttpReferrer(referrer: string | undefined) {
const cleanedReferrer = normaliseReferrer(referrer)?.trim();

if (!cleanedReferrer) return undefined;

try {
const referrerUrl = new URL(cleanedReferrer);

if (!["http:", "https:"].includes(referrerUrl.protocol)) {
return undefined;
}

referrerUrl.search = "";
referrerUrl.hash = "";

return referrerUrl.toString().slice(0, MAX_REFERRER_LENGTH);
} catch {
return undefined;
}
}
6 changes: 3 additions & 3 deletions src/utils/storage.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const PRODUCTION_SUPABASE_HOST = "mfnaqdyunuafbwukbbyr.supabase.co";
const STORAGE_PUBLIC_PATH = "/storage/v1/object/public";

function normalizeAssetPath(assetPath: string) {
function normaliseAssetPath(assetPath: string) {
return assetPath.replace(/^\/+/, "");
}

Expand All @@ -22,7 +22,7 @@ export function getStoragePublicUrl(bucket: string, assetPath: string) {

if (!supabaseUrl) return null;

return `${supabaseUrl.replace(/\/$/, "")}${STORAGE_PUBLIC_PATH}/${bucket}/${normalizeAssetPath(assetPath)}`;
return `${supabaseUrl.replace(/\/$/, "")}${STORAGE_PUBLIC_PATH}/${bucket}/${normaliseAssetPath(assetPath)}`;
}

export function usesHostedStaticAssets() {
Expand All @@ -43,7 +43,7 @@ export function getStaticAssetUrl(
export function getStaticFontUrl(assetPath: string) {
return getStoragePublicUrl(
"static",
`fonts/${normalizeAssetPath(assetPath)}`
`fonts/${normaliseAssetPath(assetPath)}`
);
}

Expand Down
Loading