Skip to content

Commit e8938c3

Browse files
lohanidamodarclaude
andcommitted
fix: use server time for fingerprint timestamps to prevent clock drift rejections
Fingerprint validation was failing with "Timestamp expired" for users whose local clock drifted beyond the server's tolerance. Now we fetch the server's time via health.getTime() once per session and use it for all fingerprint timestamps, eliminating dependency on the user's local clock. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 67531ae commit e8938c3

3 files changed

Lines changed: 43 additions & 4 deletions

File tree

src/lib/helpers/fingerprint.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,37 @@ import { env } from '$env/dynamic/public';
33
const SECRET = env.PUBLIC_CONSOLE_FINGERPRINT_KEY ?? '';
44
const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
55

6+
/** Cached server timestamp and the local time it was fetched at, for interpolation. */
7+
let serverTimeCache: { serverSecs: number; fetchedAtMs: number } | null = null;
8+
9+
/**
10+
* Fetch and cache the server's clock so fingerprint timestamps always align
11+
* with the backend's clock, regardless of local clock drift.
12+
*
13+
* @param getServerTime - callback that returns the server's unix timestamp in seconds
14+
* (e.g. `health.getTime()` → `response.localTime`)
15+
*/
16+
export async function syncServerTime(
17+
getServerTime: () => Promise<number>
18+
): Promise<void> {
19+
if (serverTimeCache) return;
20+
try {
21+
const fetchedAtMs = Date.now();
22+
const serverSecs = await getServerTime();
23+
serverTimeCache = { serverSecs, fetchedAtMs };
24+
} catch {
25+
console.warn('Failed to sync server time for fingerprint');
26+
}
27+
}
28+
29+
function getServerTimestamp(): number {
30+
if (!serverTimeCache) {
31+
return Math.floor(Date.now() / 1000);
32+
}
33+
const elapsedSecs = Math.floor((Date.now() - serverTimeCache.fetchedAtMs) / 1000);
34+
return serverTimeCache.serverSecs + elapsedSecs;
35+
}
36+
637
async function sha256(message: string): Promise<string> {
738
if (!crypto?.subtle) {
839
console.warn('crypto.subtle unavailable, fingerprinting disabled');
@@ -204,7 +235,7 @@ export async function generateFingerprintToken(): Promise<string> {
204235

205236
const signals: BrowserSignals = {
206237
...staticSignals,
207-
timestamp: Math.floor(Date.now() / 1000)
238+
timestamp: getServerTimestamp()
208239
};
209240

210241
const payload = JSON.stringify(signals);

src/routes/(console)/project-[region]-[project]/+layout.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { loadAvailableRegions } from '$routes/(console)/regions';
1111
import { type Models, Platform } from '@appwrite.io/console';
1212
import { redirect } from '@sveltejs/kit';
1313
import { resolve } from '$app/paths';
14-
import { generateFingerprintToken } from '$lib/helpers/fingerprint';
14+
import { generateFingerprintToken, syncServerTime } from '$lib/helpers/fingerprint';
1515
import { normalizeConsoleVariables } from '$lib/helpers/domains';
1616
import { browser } from '$app/environment';
1717

@@ -107,7 +107,11 @@ export const load: LayoutLoad = async ({ params, depends, parent }) => {
107107
// Track console access for cloud projects (fire-and-forget, backend has 6-day cooldown).
108108
// Skip if paused — user must explicitly resume via the paused project modal.
109109
if (isCloud && browser && project.status !== 'paused') {
110-
generateFingerprintToken()
110+
syncServerTime(async () => {
111+
const { localTime } = await sdk.forConsole.health.getTime();
112+
return localTime;
113+
})
114+
.then(() => generateFingerprintToken())
111115
.then((fingerprint) => {
112116
sdk.forConsole.client.headers['X-Appwrite-Console-Fingerprint'] = fingerprint;
113117
return sdk.forConsole.projects.updateConsoleAccess({

src/routes/(console)/project-[region]-[project]/pausedProjectModal.svelte

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import { Dependencies } from '$lib/constants';
77
import { addNotification } from '$lib/stores/notifications';
88
import { Submit, trackError } from '$lib/actions/analytics';
9-
import { generateFingerprintToken } from '$lib/helpers/fingerprint';
9+
import { generateFingerprintToken, syncServerTime } from '$lib/helpers/fingerprint';
1010
import { Alert, Layout, Modal, Typography } from '@appwrite.io/pink-svelte';
1111
import { Status } from '@appwrite.io/console';
1212
@@ -28,6 +28,10 @@
2828
error = null;
2929
3030
try {
31+
await syncServerTime(async () => {
32+
const { localTime } = await sdk.forConsole.health.getTime();
33+
return localTime;
34+
});
3135
const fingerprint = await generateFingerprintToken();
3236
sdk.forConsole.client.headers['X-Appwrite-Console-Fingerprint'] = fingerprint;
3337

0 commit comments

Comments
 (0)