fix: parse cookie domain (#185)

* chore: update bots and referrers

* fix: handle cookie domain better
This commit is contained in:
Carl-Gerhard Lindesvärd
2025-10-04 21:22:09 +02:00
committed by GitHub
parent 0b4fcbad69
commit 280065c2c4
3 changed files with 527 additions and 8 deletions

View File

@@ -1,11 +1,4 @@
// Sorry co.uk, but you're not a top domain
const parseCookieDomain = (url: string) => {
const domain = new URL(url);
return {
domain: domain.hostname.split('.').slice(-2).join('.'),
secure: domain.protocol === 'https:',
};
};
import { parseCookieDomain } from './parse-cookie-domain';
const parsed = parseCookieDomain(process.env.NEXT_PUBLIC_DASHBOARD_URL ?? '');

View File

@@ -0,0 +1,402 @@
import { describe, expect, it } from 'vitest';
import { parseCookieDomain } from './parse-cookie-domain';
describe('parseCookieDomain', () => {
it('should return undefined domain for empty string', () => {
expect(parseCookieDomain('')).toEqual({
domain: undefined,
secure: false,
});
});
describe('localhost and IP addresses', () => {
it('should return undefined domain for localhost', () => {
expect(parseCookieDomain('http://localhost:3000')).toEqual({
domain: undefined,
secure: false,
});
});
it('should return undefined domain for localhost with https', () => {
expect(parseCookieDomain('https://localhost:3000')).toEqual({
domain: undefined,
secure: true,
});
});
it('should return undefined domain for IPv4 addresses', () => {
expect(parseCookieDomain('http://192.168.1.1')).toEqual({
domain: undefined,
secure: false,
});
});
it('should return undefined domain for IPv4 addresses with https', () => {
expect(parseCookieDomain('https://192.168.1.1')).toEqual({
domain: undefined,
secure: true,
});
});
it('should return undefined domain for IPv4 addresses with port', () => {
expect(parseCookieDomain('http://192.168.1.1:8080')).toEqual({
domain: undefined,
secure: false,
});
});
});
describe('multi-part TLDs (co.uk, com.au, etc.)', () => {
it('should handle co.uk domains correctly', () => {
expect(parseCookieDomain('https://example.co.uk')).toEqual({
domain: '.example.co.uk',
secure: true,
});
});
it('should handle subdomains of co.uk domains', () => {
expect(parseCookieDomain('https://subdomain.example.co.uk')).toEqual({
domain: '.example.co.uk',
secure: true,
});
});
it('should handle deep subdomains of co.uk domains', () => {
expect(parseCookieDomain('https://api.subdomain.example.co.uk')).toEqual({
domain: '.example.co.uk',
secure: true,
});
});
it('should handle com.au domains correctly', () => {
expect(parseCookieDomain('https://example.com.au')).toEqual({
domain: '.example.com.au',
secure: true,
});
});
it('should handle subdomains of com.au domains', () => {
expect(parseCookieDomain('https://api.example.com.au')).toEqual({
domain: '.example.com.au',
secure: true,
});
});
it('should handle co.za domains correctly', () => {
expect(parseCookieDomain('https://example.co.za')).toEqual({
domain: '.example.co.za',
secure: true,
});
});
it('should handle org.uk domains correctly', () => {
expect(parseCookieDomain('https://example.org.uk')).toEqual({
domain: '.example.org.uk',
secure: true,
});
});
it('should handle gov.uk domains correctly', () => {
expect(parseCookieDomain('https://example.gov.uk')).toEqual({
domain: '.example.gov.uk',
secure: true,
});
});
it('should handle ac.uk domains correctly', () => {
expect(parseCookieDomain('https://example.ac.uk')).toEqual({
domain: '.example.ac.uk',
secure: true,
});
});
it('should handle nhs.uk domains correctly', () => {
expect(parseCookieDomain('https://example.nhs.uk')).toEqual({
domain: '.example.nhs.uk',
secure: true,
});
});
});
describe('regular domains', () => {
it('should handle root domains correctly', () => {
expect(parseCookieDomain('https://example.com')).toEqual({
domain: '.example.com',
secure: true,
});
});
it('should handle root domains with http', () => {
expect(parseCookieDomain('http://example.com')).toEqual({
domain: '.example.com',
secure: false,
});
});
it('should handle subdomains correctly', () => {
expect(parseCookieDomain('https://api.example.com')).toEqual({
domain: '.example.com',
secure: true,
});
});
it('should handle deep subdomains correctly', () => {
expect(parseCookieDomain('https://v1.api.example.com')).toEqual({
domain: '.example.com',
secure: true,
});
});
it('should handle very deep subdomains correctly', () => {
expect(parseCookieDomain('https://staging.v1.api.example.com')).toEqual({
domain: '.example.com',
secure: true,
});
});
});
describe('PaaS platform subdomains', () => {
it('should handle zeabur.app subdomains correctly', () => {
expect(parseCookieDomain('https://xxx.zeabur.app')).toEqual({
domain: '.zeabur.app',
secure: true,
});
});
it('should handle railway.app subdomains correctly', () => {
expect(parseCookieDomain('https://xxx.railway.app')).toEqual({
domain: '.railway.app',
secure: true,
});
});
it('should handle vercel.app subdomains correctly', () => {
expect(parseCookieDomain('https://xxx.vercel.app')).toEqual({
domain: '.vercel.app',
secure: true,
});
});
it('should handle netlify.app subdomains correctly', () => {
expect(parseCookieDomain('https://xxx.netlify.app')).toEqual({
domain: '.netlify.app',
secure: true,
});
});
it('should handle render.com subdomains correctly', () => {
expect(parseCookieDomain('https://xxx.onrender.com')).toEqual({
domain: '.onrender.com',
secure: true,
});
});
});
describe('edge cases and potential breaking scenarios', () => {
it('should handle domains with ports', () => {
expect(parseCookieDomain('https://example.com:8080')).toEqual({
domain: '.example.com',
secure: true,
});
});
it('should handle domains with paths', () => {
expect(parseCookieDomain('https://example.com/path')).toEqual({
domain: '.example.com',
secure: true,
});
});
it('should handle domains with query parameters', () => {
expect(parseCookieDomain('https://example.com?param=value')).toEqual({
domain: '.example.com',
secure: true,
});
});
it('should handle domains with fragments', () => {
expect(parseCookieDomain('https://example.com#fragment')).toEqual({
domain: '.example.com',
secure: true,
});
});
it('should handle domains with all URL components', () => {
expect(
parseCookieDomain('https://example.com:8080/path?param=value#fragment'),
).toEqual({
domain: '.example.com',
secure: true,
});
});
it('should handle single-level domains', () => {
expect(parseCookieDomain('https://example')).toEqual({
domain: '.example',
secure: true,
});
});
it('should handle domains with underscores (invalid but should not crash)', () => {
expect(parseCookieDomain('https://example_test.com')).toEqual({
domain: '.example_test.com',
secure: true,
});
});
it('should handle domains with hyphens', () => {
expect(parseCookieDomain('https://example-test.com')).toEqual({
domain: '.example-test.com',
secure: true,
});
});
it('should handle domains with numbers', () => {
expect(parseCookieDomain('https://example123.com')).toEqual({
domain: '.example123.com',
secure: true,
});
});
});
describe('error cases that should break', () => {
it('should throw error for invalid URLs', () => {
expect(() => parseCookieDomain('not-a-url')).toThrow();
});
it('should throw error for URLs without protocol', () => {
expect(() => parseCookieDomain('example.com')).toThrow();
});
it('should throw error for malformed URLs', () => {
expect(() => parseCookieDomain('http://')).toThrow();
});
it('should throw error for URLs with invalid characters', () => {
expect(() =>
parseCookieDomain('http://example.com:invalid-port'),
).toThrow();
});
});
describe('specific real-world scenarios', () => {
it('should handle openpanel.dev domains correctly', () => {
expect(parseCookieDomain('https://api.openpanel.dev')).toEqual({
domain: '.openpanel.dev',
secure: true,
});
});
it('should handle dashboard.openpanel.dev domains correctly', () => {
expect(parseCookieDomain('https://dashboard.openpanel.dev')).toEqual({
domain: '.openpanel.dev',
secure: true,
});
});
it('should handle subdomains of openpanel.dev correctly', () => {
expect(
parseCookieDomain('https://staging.dashboard.openpanel.dev'),
).toEqual({
domain: '.openpanel.dev',
secure: true,
});
});
it('should handle custom domains correctly', () => {
expect(parseCookieDomain('https://myapp.com')).toEqual({
domain: '.myapp.com',
secure: true,
});
});
it('should handle subdomains of custom domains correctly', () => {
expect(parseCookieDomain('https://api.myapp.com')).toEqual({
domain: '.myapp.com',
secure: true,
});
});
});
describe('all multi-part TLDs from the list', () => {
const multiPartTLDs = [
'co.uk',
'com.au',
'co.za',
'co.nz',
'co.jp',
'co.kr',
'co.in',
'co.il',
'com.br',
'com.mx',
'com.ar',
'com.pe',
'com.cl',
'com.co',
'com.ve',
'net.au',
'org.au',
'gov.au',
'edu.au',
'net.nz',
'org.nz',
'gov.nz',
'org.uk',
'gov.uk',
'ac.uk',
'nhs.uk',
'org.za',
'gov.za',
'ac.za',
'ac.jp',
'or.jp',
'go.jp',
'or.kr',
'go.kr',
'org.in',
'gov.in',
'ac.in',
'org.il',
'gov.il',
'ac.il',
'net.br',
'org.br',
'gov.br',
'net.mx',
'org.mx',
'gov.mx',
'net.ar',
'org.ar',
'gov.ar',
'net.pe',
'org.pe',
'gov.pe',
'net.cl',
'org.cl',
'gov.cl',
'net.co',
'org.co',
'gov.co',
'net.ve',
'org.ve',
'gov.ve',
];
multiPartTLDs.forEach((tld) => {
it(`should handle ${tld} domains correctly`, () => {
expect(parseCookieDomain(`https://example.${tld}`)).toEqual({
domain: `.example.${tld}`,
secure: true,
});
});
it(`should handle subdomains of ${tld} domains correctly`, () => {
expect(parseCookieDomain(`https://api.example.${tld}`)).toEqual({
domain: `.example.${tld}`,
secure: true,
});
});
});
});
});

View File

@@ -0,0 +1,124 @@
// List of known multi-part TLDs that should be treated as single domains
const MULTI_PART_TLDS = [
'co.uk',
'com.au',
'co.za',
'co.nz',
'co.jp',
'co.kr',
'co.in',
'co.il',
'com.br',
'com.mx',
'com.ar',
'com.pe',
'com.cl',
'com.co',
'com.ve',
'net.au',
'org.au',
'gov.au',
'edu.au',
'net.nz',
'org.nz',
'gov.nz',
'org.uk',
'gov.uk',
'ac.uk',
'nhs.uk',
'org.za',
'gov.za',
'ac.za',
'ac.jp',
'or.jp',
'go.jp',
'or.kr',
'go.kr',
'org.in',
'gov.in',
'ac.in',
'org.il',
'gov.il',
'ac.il',
'net.br',
'org.br',
'gov.br',
'net.mx',
'org.mx',
'gov.mx',
'net.ar',
'org.ar',
'gov.ar',
'net.pe',
'org.pe',
'gov.pe',
'net.cl',
'org.cl',
'gov.cl',
'net.co',
'org.co',
'gov.co',
'net.ve',
'org.ve',
'gov.ve',
];
export const parseCookieDomain = (url: string) => {
if (!url) {
return {
domain: undefined,
secure: false,
};
}
const domain = new URL(url);
const hostname = domain.hostname;
// For localhost or IP addresses, don't set domain
if (hostname === 'localhost' || /^\d+\.\d+\.\d+\.\d+$/.test(hostname)) {
return {
domain: undefined,
secure: domain.protocol === 'https:',
};
}
const parts = hostname.split('.');
// Handle multi-part TLDs like co.uk, com.au, etc.
if (parts.length >= 3) {
const potentialTLD = parts.slice(-2).join('.');
if (MULTI_PART_TLDS.includes(potentialTLD)) {
// For domains like example.co.uk or subdomain.example.co.uk
// Use the last 3 parts: .example.co.uk
return {
domain: `.${parts.slice(-3).join('.')}`,
secure: domain.protocol === 'https:',
};
}
}
// For regular subdomains, use the last 2 parts
if (parts.length > 2) {
return {
domain: `.${parts.slice(-2).join('.')}`,
secure: domain.protocol === 'https:',
};
}
// For root domains, use the full domain with leading dot
return {
domain: `.${hostname}`,
secure: domain.protocol === 'https:',
};
};
const parsed = parseCookieDomain(process.env.NEXT_PUBLIC_DASHBOARD_URL ?? '');
export const COOKIE_MAX_AGE = 60 * 60 * 24 * 30;
export const COOKIE_OPTIONS = {
domain: parsed.domain,
secure: parsed.secure,
sameSite: 'lax',
httpOnly: true,
path: '/',
} as const;