From e21c1c83bbee41029f4f38e3d9e93a87f4a2841d Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Thu, 5 Feb 2026 08:56:12 -0800 Subject: [PATCH 1/4] configure salt rotation period using env vars. Closed #3427 --- src/app/api/send/route.ts | 7 ++++--- src/lib/crypto.ts | 9 +++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/app/api/send/route.ts b/src/app/api/send/route.ts index a0becc2ac..c3aa9a00e 100644 --- a/src/app/api/send/route.ts +++ b/src/app/api/send/route.ts @@ -1,10 +1,10 @@ -import { startOfHour, startOfMonth } from 'date-fns'; +import { startOfHour } from 'date-fns'; import { isbot } from 'isbot'; import { serializeError } from 'serialize-error'; import { z } from 'zod'; import clickhouse from '@/lib/clickhouse'; import { COLLECTION_TYPE, EVENT_TYPE } from '@/lib/constants'; -import { hash, secret, uuid } from '@/lib/crypto'; +import { getSalt, hash, secret, uuid } from '@/lib/crypto'; import { getClientInfo, hasBlockedIp } from '@/lib/detect'; import { createToken, parseToken } from '@/lib/jwt'; import { fetchWebsite } from '@/lib/load'; @@ -130,7 +130,8 @@ export async function POST(request: Request) { const createdAt = timestamp ? new Date(timestamp * 1000) : new Date(); const now = Math.floor(Date.now() / 1000); - const sessionSalt = hash(startOfMonth(createdAt).toUTCString()); + const saltRotation = process.env.SALT_ROTATION || 'month'; + const sessionSalt = getSalt(saltRotation, createdAt); const visitSalt = hash(startOfHour(createdAt).toUTCString()); const sessionId = id ? uuid(sourceId, id) : uuid(sourceId, ip, userAgent, sessionSalt); diff --git a/src/lib/crypto.ts b/src/lib/crypto.ts index ee4c977fe..6dde8826d 100644 --- a/src/lib/crypto.ts +++ b/src/lib/crypto.ts @@ -1,4 +1,5 @@ import crypto from 'node:crypto'; +import { startOfDay, startOfMonth, startOfWeek } from 'date-fns'; import { v4, v5, v7 } from 'uuid'; const ALGORITHM = 'aes-256-gcm'; @@ -67,3 +68,11 @@ export function uuid(...args: any) { export function createAuthKey() { return crypto.randomBytes(16).toString('hex'); } + +export function getSalt(saltRotation: string, createdAt: Date): string { + return hash( + (saltRotation === 'day' ? startOfDay : saltRotation === 'week' ? startOfWeek : startOfMonth)( + createdAt, + ).toUTCString(), + ); +} From 9d6bb64c3d50e2a94273944946be0d8d15f33389 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Thu, 5 Feb 2026 09:13:18 -0800 Subject: [PATCH 2/4] use website_event_stats_hourly view for excludeBounceQuery --- src/lib/clickhouse.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/clickhouse.ts b/src/lib/clickhouse.ts index 3160dbfea..77357f8ba 100644 --- a/src/lib/clickhouse.ts +++ b/src/lib/clickhouse.ts @@ -138,12 +138,12 @@ function getExcludeBounceQuery(filters: Record) { return `join (select distinct session_id, visit_id - from website_event + from website_event_stats_hourly where website_id = {websiteId:UUID} and created_at between {startDate:DateTime64} and {endDate:DateTime64} and event_type = 1 group by session_id, visit_id - having count(*) > 1 + having sum(views) > 1 ) excludeBounce on excludeBounce.session_id = website_event.session_id and excludeBounce.visit_id = website_event.visit_id From 397796cfff7ddc7bd6ea36c9b829afadd041658a Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Thu, 5 Feb 2026 09:35:29 -0800 Subject: [PATCH 3/4] Fix share url. --- .../[websiteId]/settings/ShareEditForm.tsx | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/app/(main)/websites/[websiteId]/settings/ShareEditForm.tsx b/src/app/(main)/websites/[websiteId]/settings/ShareEditForm.tsx index 4b86247ac..00598fcfd 100644 --- a/src/app/(main)/websites/[websiteId]/settings/ShareEditForm.tsx +++ b/src/app/(main)/websites/[websiteId]/settings/ShareEditForm.tsx @@ -40,10 +40,7 @@ export function ShareEditForm({ const isEditing = !!shareId; const getUrl = (slug: string) => { - if (cloudMode) { - return `${process.env.cloudUrl}/share/${slug}`; - } - return `${window?.location.origin}${process.env.basePath || ''}/share/${slug}`; + return `${cloudMode ? process.env.cloudUrl : window?.location.origin}${process.env.basePath || ''}/share/${slug}`; }; useEffect(() => { @@ -74,9 +71,16 @@ export function ShareEditForm({ try { if (isEditing) { - await post(`/share/id/${shareId}`, { name: data.name, slug: share.slug, parameters }); + await post(`/share/id/${shareId}`, { + name: data.name, + slug: share.slug, + parameters, + }); } else { - await post(`/websites/${websiteId}/shares`, { name: data.name, parameters }); + await post(`/websites/${websiteId}/shares`, { + name: data.name, + parameters, + }); } touch('shares'); onSave?.(); From 7514af42362ebc97d699a971e27cd7693c67f4d8 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Thu, 5 Feb 2026 10:51:51 -0800 Subject: [PATCH 4/4] fix getURL for share --- .../(main)/websites/[websiteId]/settings/ShareEditForm.tsx | 5 ++++- src/app/(main)/websites/[websiteId]/settings/SharesTable.tsx | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/app/(main)/websites/[websiteId]/settings/ShareEditForm.tsx b/src/app/(main)/websites/[websiteId]/settings/ShareEditForm.tsx index 00598fcfd..a71a49277 100644 --- a/src/app/(main)/websites/[websiteId]/settings/ShareEditForm.tsx +++ b/src/app/(main)/websites/[websiteId]/settings/ShareEditForm.tsx @@ -40,7 +40,10 @@ export function ShareEditForm({ const isEditing = !!shareId; const getUrl = (slug: string) => { - return `${cloudMode ? process.env.cloudUrl : window?.location.origin}${process.env.basePath || ''}/share/${slug}`; + if (cloudMode) { + return `${process.env.cloudUrl}/share/${slug}`; + } + return `${window?.location.origin}${process.env.basePath || ''}/share/${slug}`; }; useEffect(() => { diff --git a/src/app/(main)/websites/[websiteId]/settings/SharesTable.tsx b/src/app/(main)/websites/[websiteId]/settings/SharesTable.tsx index e997d2476..52f8a0579 100644 --- a/src/app/(main)/websites/[websiteId]/settings/SharesTable.tsx +++ b/src/app/(main)/websites/[websiteId]/settings/SharesTable.tsx @@ -11,7 +11,10 @@ export function SharesTable(props: DataTableProps) { const { isMobile } = useMobile(); const getUrl = (slug: string) => { - return `${cloudMode ? process.env.cloudUrl : window?.location.origin}${process.env.basePath || ''}/share/${slug}`; + if (cloudMode) { + return `${process.env.cloudUrl}/share/${slug}`; + } + return `${window?.location.origin}${process.env.basePath || ''}/share/${slug}`; }; return (