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
13 changes: 13 additions & 0 deletions src/lib/affiliates/tracking.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,19 @@ describe("generateTrackingCode", () => {
expect(code1).toMatch(/^bob-[a-f0-9]{6}$/);
expect(code2).toMatch(/^bob-[a-f0-9]{6}$/);
});

it("sanitizes username prefixes for URL-safe tracking codes", () => {
const code = generateTrackingCode("Alice Smith/@Example", "offer-1");

expect(code).toMatch(/^alice-smith-example-[a-f0-9]{6}$/);
expect(code).not.toMatch(/[ /@]/);
});

it("falls back when the username has no URL-safe characters", () => {
const code = generateTrackingCode("!!!", "offer-1");

expect(code).toMatch(/^affiliate-[a-f0-9]{6}$/);
});
});

describe("hashIP", () => {
Expand Down
7 changes: 6 additions & 1 deletion src/lib/affiliates/tracking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,17 @@ async function getAttributionWindowStart(admin: SupabaseClient, offerId: string)
*/
export function generateTrackingCode(username: string, offerSlug: string): string {
const base = `${username}-${offerSlug}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Hash seed uses original unsanitized username

The base variable (used as the hash seed) still embeds the raw username, but the prefix returned in the tracking code is now the sanitized form. This means the human-readable portion of the code (prefix) no longer matches the entropy source (base). For usernames that sanitize to "affiliate" (the fallback), all such usernames share the same prefix but generate hashes from their distinct raw values — so codes are still unique. The disconnect is harmless today, but if base were ever used to reverse-lookup or validate the prefix this divergence would be surprising. Consider using prefix (or a normalized form) as the base instead of the raw username.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

const prefix = username
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "")
.slice(0, 48) || "affiliate";
Comment on lines +22 to +26

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Trailing dash possible after slice

The dash-trim (.replace(/^-+|-+$/g, "")) runs before .slice(0, 48), so if the slice boundary falls on a hyphen the prefix can end with -, producing a double-dash in the final tracking code (e.g. aaaa...a--abc123). A username like "a" * 47 + " extra" sanitizes to "a"*47 + "-extra" and slices to "a"*47 + "-", yielding the prefix aaaa...a-. Apply the trim a second time after slicing to prevent this.

const hash = crypto
.createHash("sha256")
.update(base + Date.now())
.digest("hex")
.slice(0, 6);
return `${username}-${hash}`;
return `${prefix}-${hash}`;
}

/**
Expand Down
Loading