diff --git a/src/lib/helpers/fingerprint.ts b/src/lib/helpers/fingerprint.ts index a4d53b13de..25de4e4525 100644 --- a/src/lib/helpers/fingerprint.ts +++ b/src/lib/helpers/fingerprint.ts @@ -3,6 +3,29 @@ import { env } from '$env/dynamic/public'; const SECRET = env.PUBLIC_CONSOLE_FINGERPRINT_KEY ?? ''; const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour +/** Cached server timestamp and the local time it was fetched at, for interpolation. */ +let serverTimeCache: { serverSecs: number; fetchedAtMs: number } | null = null; + +/** + * Cache the server's clock so fingerprint timestamps always align with the + * backend's clock, regardless of local clock drift. + * + * @param serverTimeSecs - the server's unix timestamp in seconds + * (e.g. parsed from a response Date header) + */ +export function syncServerTime(serverTimeSecs: number): void { + if (serverTimeCache) return; + serverTimeCache = { serverSecs: serverTimeSecs, fetchedAtMs: Date.now() }; +} + +function getServerTimestamp(): number { + if (!serverTimeCache) { + return Math.floor(Date.now() / 1000); + } + const elapsedSecs = Math.floor((Date.now() - serverTimeCache.fetchedAtMs) / 1000); + return serverTimeCache.serverSecs + elapsedSecs; +} + async function sha256(message: string): Promise { if (!crypto?.subtle) { console.warn('crypto.subtle unavailable, fingerprinting disabled'); @@ -204,7 +227,7 @@ export async function generateFingerprintToken(): Promise { const signals: BrowserSignals = { ...staticSignals, - timestamp: Math.floor(Date.now() / 1000) + timestamp: getServerTimestamp() }; const payload = JSON.stringify(signals); diff --git a/src/routes/(console)/+layout.ts b/src/routes/(console)/+layout.ts index 5edbc55456..b221432942 100644 --- a/src/routes/(console)/+layout.ts +++ b/src/routes/(console)/+layout.ts @@ -6,6 +6,7 @@ import { Platform, Query } from '@appwrite.io/console'; import { makePlansMap } from '$lib/helpers/billing'; import { plansInfo as plansInfoStore } from '$lib/stores/billing'; import { normalizeConsoleVariables } from '$lib/helpers/domains'; +import { syncServerTime } from '$lib/helpers/fingerprint'; export const load: LayoutLoad = async ({ depends, parent }) => { const { organizations, plansInfo } = await parent(); @@ -28,7 +29,14 @@ export const load: LayoutLoad = async ({ depends, parent }) => { plansArrayPromise, fetch(`${endpoint}/health/version`, { headers: { 'X-Appwrite-Project': project as string } - }).then((response) => response.json() as { version?: string }), + }).then((response) => { + const dateHeader = response.headers.get('Date'); + const parsed = dateHeader ? new Date(dateHeader).getTime() : NaN; + if (Number.isFinite(parsed)) { + syncServerTime(Math.floor(parsed / 1000)); + } + return response.json() as { version?: string }; + }), sdk.forConsole.console.variables() ]);