Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
b85c938
fix: update @appwrite.io/console dependency
lohanidamodar Feb 4, 2026
09ef6f3
feat: implement fingerprint generation for console access tracking
lohanidamodar Feb 4, 2026
6f403ba
Merge main into feat-billing and resolve conflict
lohanidamodar Feb 4, 2026
ad097ec
cached finterprint data
lohanidamodar Feb 4, 2026
266ee4a
fix: restrict console access tracking to cloud environments only
lohanidamodar Feb 4, 2026
100a967
feat: add paused project modal and update console access tracking logic
lohanidamodar Feb 5, 2026
a080abe
feat: update active project query to include paused status
lohanidamodar Feb 8, 2026
83bd6af
feat: add paused project status indication and modal integration
lohanidamodar Feb 8, 2026
4148c69
fix: update paused project modal message for clarity
lohanidamodar Feb 8, 2026
cb13e06
feat: update paused project modal integration to use layout data
lohanidamodar Feb 8, 2026
7b0511a
Merge remote-tracking branch 'origin/main' into feat-billing
lohanidamodar Feb 13, 2026
520c491
fix: ensure fingerprint header is removed after console access creation
lohanidamodar Feb 13, 2026
e04dfa3
fixes and sdk update
lohanidamodar Feb 13, 2026
0cc3ebb
Merge remote-tracking branch 'origin/main' into feat-billing
lohanidamodar Feb 17, 2026
ff73a2c
fix: update @appwrite.io/console dependency version in package.json a…
lohanidamodar Feb 17, 2026
e3fa833
feat: add ProjectResume enum for tracking project resume events
lohanidamodar Feb 17, 2026
878975a
fix: format code for better readability in layout and pausedProjectMo…
lohanidamodar Feb 17, 2026
73f69c5
fix status check
lohanidamodar Feb 17, 2026
d35a403
remove unused dependency
lohanidamodar Feb 17, 2026
96bd28b
Merge remote-tracking branch 'origin/main' into feat-billing
lohanidamodar Feb 18, 2026
e4f32ec
update publish keys
lohanidamodar Feb 18, 2026
52eecc9
add PUBLIC_CONSOLE_FINGERPRINT_KEY to Dockerfile and update fingerpri…
lohanidamodar Feb 18, 2026
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
3 changes: 3 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ jobs:
"PUBLIC_CONSOLE_MOCK_AI_SUGGESTIONS=false"
"PUBLIC_GROWTH_ENDPOINT=${{ secrets.PUBLIC_GROWTH_ENDPOINT }}"
"PUBLIC_STRIPE_KEY=${{ secrets.PUBLIC_STRIPE_KEY }}"
"PUBLIC_CONSOLE_FINGERPRINT_KEY=${{ secrets.PUBLIC_CONSOLE_FINGERPRINT_KEY }}"
"SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}"
"SENTRY_RELEASE=${{ github.event.release.tag_name }}"
publish-cloud-stage:
Expand Down Expand Up @@ -87,6 +88,7 @@ jobs:
"PUBLIC_CONSOLE_MOCK_AI_SUGGESTIONS=false"
"PUBLIC_GROWTH_ENDPOINT=${{ secrets.PUBLIC_GROWTH_ENDPOINT }}"
"PUBLIC_STRIPE_KEY=${{ secrets.PUBLIC_STRIPE_KEY_STAGE }}"
"PUBLIC_CONSOLE_FINGERPRINT_KEY=${{ secrets.PUBLIC_CONSOLE_FINGERPRINT_KEY_STAGE }}"
publish-self-hosted:
runs-on: ubuntu-latest
steps:
Expand Down Expand Up @@ -166,4 +168,5 @@ jobs:
"PUBLIC_CONSOLE_MOCK_AI_SUGGESTIONS=false"
"PUBLIC_CONSOLE_FEATURE_FLAGS="
"PUBLIC_STRIPE_KEY=${{ secrets.PUBLIC_STRIPE_KEY_STAGE }}"
"PUBLIC_CONSOLE_FINGERPRINT_KEY=${{ secrets.PUBLIC_CONSOLE_FINGERPRINT_KEY_STAGE }}"
"PUBLIC_GROWTH_ENDPOINT=${{ secrets.PUBLIC_GROWTH_ENDPOINT }}"
2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
ARG PUBLIC_APPWRITE_ENDPOINT
ARG PUBLIC_GROWTH_ENDPOINT
ARG PUBLIC_STRIPE_KEY
ARG PUBLIC_CONSOLE_FINGERPRINT_KEY
ARG SENTRY_AUTH_TOKEN

Check warning on line 26 in Dockerfile

View workflow job for this annotation

GitHub Actions / publish-cloud

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ARG "SENTRY_AUTH_TOKEN") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/

Check warning on line 26 in Dockerfile

View workflow job for this annotation

GitHub Actions / publish-self-hosted

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ARG "SENTRY_AUTH_TOKEN") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/

Check warning on line 26 in Dockerfile

View workflow job for this annotation

GitHub Actions / publish-cloud-stage

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ARG "SENTRY_AUTH_TOKEN") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/

Check warning on line 26 in Dockerfile

View workflow job for this annotation

GitHub Actions / publish-cloud-no-regions

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ARG "SENTRY_AUTH_TOKEN") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
ARG SENTRY_RELEASE

ENV PUBLIC_APPWRITE_ENDPOINT=$PUBLIC_APPWRITE_ENDPOINT
Expand All @@ -33,7 +34,8 @@
ENV PUBLIC_CONSOLE_EMAIL_VERIFICATION=$PUBLIC_CONSOLE_EMAIL_VERIFICATION
ENV PUBLIC_CONSOLE_MOCK_AI_SUGGESTIONS=$PUBLIC_CONSOLE_MOCK_AI_SUGGESTIONS
ENV PUBLIC_STRIPE_KEY=$PUBLIC_STRIPE_KEY
ENV PUBLIC_CONSOLE_FINGERPRINT_KEY=$PUBLIC_CONSOLE_FINGERPRINT_KEY
ENV SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN

Check warning on line 38 in Dockerfile

View workflow job for this annotation

GitHub Actions / publish-cloud

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "SENTRY_AUTH_TOKEN") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/

Check warning on line 38 in Dockerfile

View workflow job for this annotation

GitHub Actions / publish-self-hosted

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "SENTRY_AUTH_TOKEN") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/

Check warning on line 38 in Dockerfile

View workflow job for this annotation

GitHub Actions / publish-cloud-stage

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "SENTRY_AUTH_TOKEN") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/

Check warning on line 38 in Dockerfile

View workflow job for this annotation

GitHub Actions / publish-cloud-no-regions

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "SENTRY_AUTH_TOKEN") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
ENV SENTRY_RELEASE=$SENTRY_RELEASE
ENV NODE_OPTIONS=--max_old_space_size=8192

Expand Down
6 changes: 2 additions & 4 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 1 addition & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
},
"dependencies": {
"@ai-sdk/svelte": "^1.1.24",
"@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@c6f60aa",
"@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@e64d5ed",
"@appwrite.io/pink-icons": "0.25.0",
"@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@df765cc",
"@appwrite.io/pink-legacy": "^1.0.3",
Expand All @@ -38,8 +38,6 @@
"dayjs": "^1.11.13",
"deep-equal": "^2.2.3",
"echarts": "^5.6.0",
"https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@a4067bf": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@a4067bf",
"https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@a4067bf": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@a4067bf",
"ignore": "^6.0.2",
"nanoid": "^5.1.5",
"nanotar": "^0.1.1",
Expand Down
1 change: 1 addition & 0 deletions src/lib/actions/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ export enum Submit {
ProjectUpdateLabels = 'submit_project_update_labels',
ProjectService = 'submit_project_service',
ProjectUpdateSMTP = 'submit_project_update_smtp',
ProjectResume = 'submit_project_resume',
MemberCreate = 'submit_member_create',
MemberDelete = 'submit_member_delete',
MembershipUpdate = 'submit_membership_update',
Expand Down
216 changes: 216 additions & 0 deletions src/lib/helpers/fingerprint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import { env } from '$env/dynamic/public';

const SECRET = env.PUBLIC_CONSOLE_FINGERPRINT_KEY ?? '';
Comment on lines +1 to +3
Copy link
Contributor

@coderabbitai coderabbitai bot Feb 4, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: HMAC signing secret is exposed to the client.

The PUBLIC_CONSOLE_FINGERPRINT_KEY uses SvelteKit's PUBLIC_ prefix, which means this value is bundled into client-side JavaScript and visible to anyone inspecting the browser. This completely undermines the integrity guarantee of HMAC signing—users can forge valid fingerprint tokens.

Additionally, the empty string fallback silently produces signatures with a null key when the env var is missing.

If server-side verification is intended, the signing must occur on the server, or use a different authentication mechanism. If this is intentional client-side obfuscation only, consider documenting that limitation.

🤖 Prompt for AI Agents
In `@src/lib/helpers/fingerprint.ts` around lines 1 - 3, The code exposes the HMAC
signing secret by reading PUBLIC_CONSOLE_FINGERPRINT_KEY into the client-visible
SECRET constant in fingerprint.ts; move signing/verification to server-only code
and stop using any env var with the PUBLIC_ prefix. Replace the client-side
import of env and SECRET usage by implementing server endpoints or server-only
utilities that read a non-public env var (e.g., CONSOLE_FINGERPRINT_KEY) and
perform HMAC sign/verify there, and remove the empty-string fallback so the
server throws or fails fast if the secret is missing to avoid null-key
signatures; update any callers of the client-side SECRET to call the new server
endpoints/functions instead.

Copy link
Member

Choose a reason for hiding this comment

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

@lohanidamodar this is a public key from public/private pair, right?

Copy link
Contributor

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour

async function sha256(message: string): Promise<string> {
const data = new TextEncoder().encode(message);
const hash = await crypto.subtle.digest('SHA-256', data);
return Array.from(new Uint8Array(hash))
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
}

async function hmacSha256(message: string, secret: string): Promise<string> {
const key = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const sig = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(message));
return Array.from(new Uint8Array(sig))
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
}

function getCanvasFingerprint(): string {
try {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx) return '';

canvas.width = 200;
canvas.height = 50;

ctx.textBaseline = 'top';
ctx.font = '14px Arial';
ctx.fillStyle = '#f60';
ctx.fillRect(125, 1, 62, 20);
ctx.fillStyle = '#069';
ctx.fillText('Appwrite Console', 2, 15);
ctx.fillStyle = 'rgba(102, 204, 0, 0.7)';
ctx.fillText('Appwrite Console', 4, 17);

return canvas.toDataURL();
} catch {
return '';
}
}

function getWebGLFingerprint(): string {
try {
const canvas = document.createElement('canvas');
const gl =
canvas.getContext('webgl') ||
(canvas.getContext('experimental-webgl') as WebGLRenderingContext | null);
if (!gl) return '';

const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
if (!debugInfo) return 'webgl-no-debug';

const vendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL) || '';
const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) || '';

return `${vendor}~${renderer}`;
} catch {
return '';
}
}

async function getAudioFingerprint(): Promise<string> {
try {
const OfflineCtx =
window.OfflineAudioContext ||
(window as unknown as { webkitOfflineAudioContext: typeof OfflineAudioContext })
.webkitOfflineAudioContext;
if (!OfflineCtx) return '';

const sampleRate = 44100;
const length = 4096;
const context = new OfflineCtx(1, length, sampleRate);

const oscillator = context.createOscillator();
oscillator.type = 'triangle';
oscillator.frequency.value = 10000;

const compressor = context.createDynamicsCompressor();
compressor.threshold.value = -50;
compressor.knee.value = 40;
compressor.ratio.value = 12;
compressor.attack.value = 0;
compressor.release.value = 0.25;

oscillator.connect(compressor);
compressor.connect(context.destination);

oscillator.start(0);

const buffer = await context.startRendering();
const samples = buffer.getChannelData(0);

let sum = 0;
for (let i = 0; i < samples.length; i++) {
sum += Math.abs(samples[i]);
}

return sum.toString();
} catch {
return '';
}
}

interface StaticSignals {
userAgent: string;
language: string;
languages: string[];
platform: string;
hardwareConcurrency: number;
deviceMemory: number | undefined;
maxTouchPoints: number;
screenWidth: number;
screenHeight: number;
screenColorDepth: number;
screenPixelDepth: number;
devicePixelRatio: number;
timezoneOffset: number;
timezone: string;
canvas: string;
webgl: string;
audio: string;
}

interface BrowserSignals extends StaticSignals {
timestamp: number;
}

interface SignalsCache {
signals: StaticSignals;
collectedAt: number;
}

let cache: SignalsCache | null = null;
let cachePromise: Promise<StaticSignals> | null = null;

async function collectStaticSignals(): Promise<StaticSignals> {
const [canvasRaw, webgl, audio] = await Promise.all([
Promise.resolve(getCanvasFingerprint()),
Promise.resolve(getWebGLFingerprint()),
getAudioFingerprint()
]);

const canvas = canvasRaw ? await sha256(canvasRaw) : '';

return {
userAgent: navigator.userAgent,
language: navigator.language,
languages: [...(navigator.languages || [])],
platform: navigator.platform,
hardwareConcurrency: navigator.hardwareConcurrency || 0,
deviceMemory: (navigator as Navigator & { deviceMemory?: number }).deviceMemory,
maxTouchPoints: navigator.maxTouchPoints || 0,
screenWidth: screen.width,
screenHeight: screen.height,
screenColorDepth: screen.colorDepth,
screenPixelDepth: screen.pixelDepth,
devicePixelRatio: window.devicePixelRatio || 1,
timezoneOffset: new Date().getTimezoneOffset(),
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
canvas,
webgl,
audio
};
}
Comment on lines +146 to +174
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Privacy/Compliance: Browser fingerprinting collects device-identifying data.

This function collects extensive device fingerprinting signals (hardware concurrency, device memory, screen properties, canvas/WebGL/audio fingerprints) that can uniquely identify users across sessions. Under GDPR/CCPA, browser fingerprinting may require:

  • User disclosure in privacy policy
  • Consent mechanisms depending on jurisdiction and purpose
  • Data retention policies for the collected fingerprints

Ensure legal/compliance review has approved this data collection, and that appropriate disclosures exist.

🤖 Prompt for AI Agents
In `@src/lib/helpers/fingerprint.ts` around lines 152 - 180, collectStaticSignals
is gathering high-risk fingerprinting fields (hardwareConcurrency, deviceMemory,
screen dimensions, canvas/webgl/audio) without any consent check or
minimization; update collectStaticSignals to only run when an explicit
consent/config flag (e.g., analyticsConsent or allowFingerprinting) is true, and
add a privacy-safe mode that omits or coarsens identifiable fields (remove or
bucket deviceMemory, hardwareConcurrency, screenWidth/Height, and avoid raw
audio/webgl data), keep only necessary hashed values (canvas is already hashed)
and a short audit/comment referencing that collection requires legal approval
and retention policy; ensure the gating flag is plumbed from your app config and
that StaticSignals consumers handle missing fields gracefully.


async function getCachedSignals(): Promise<StaticSignals> {
const now = Date.now();

if (cache && now - cache.collectedAt < CACHE_TTL_MS) {
return cache.signals;
}

if (cachePromise) {
return cachePromise;
}

cachePromise = collectStaticSignals();

try {
const signals = await cachePromise;
cache = { signals, collectedAt: now };
return signals;
} finally {
cachePromise = null;
}
}

export async function generateFingerprintToken(): Promise<string> {
const staticSignals = await getCachedSignals();

const signals: BrowserSignals = {
...staticSignals,
timestamp: Math.floor(Date.now() / 1000)
};

const payload = JSON.stringify(signals);
const encoded = btoa(payload);

if (!SECRET) {
return encoded;
}

const signature = await hmacSha256(encoded, SECRET);

return `${encoded}.${signature}`;
}
12 changes: 11 additions & 1 deletion src/routes/(console)/organization-[organization]/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@
import { onMount, type ComponentType } from 'svelte';
import { canWriteProjects } from '$lib/stores/roles';
import { checkPricingRefAndRedirect } from '$lib/helpers/pricingRedirect';
import { Alert, Badge, Icon, Layout, Tooltip, Typography } from '@appwrite.io/pink-svelte';
import { Alert, Badge, Icon, Layout, Tag, Tooltip, Typography } from '@appwrite.io/pink-svelte';
import {
IconAndroid,
IconApple,
IconCode,
IconExclamationCircle,
IconFlutter,
IconPlus,
IconReact,
Expand Down Expand Up @@ -210,6 +211,15 @@
{project.name}
</svelte:fragment>

<svelte:fragment slot="status">
{#if project.status === 'paused'}
<Tag size="s" style="white-space: nowrap;">
<Icon icon={IconExclamationCircle} size="s" slot="start" />
Paused
</Tag>
{/if}
</svelte:fragment>

{#each platforms.slice(0, 2) as platform}
{@const icon = getIconForPlatform(platform.icon)}
<Badge
Expand Down
2 changes: 1 addition & 1 deletion src/routes/(console)/organization-[organization]/+page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export const load: PageLoad = async ({ params, url, route, depends, parent }) =>
? [Query.or([Query.search('search', search), Query.contains('labels', search)])]
: [];
const activeQueries = isCloud
? [Query.or([Query.equal('status', 'active'), Query.isNull('status')])]
? [Query.or([Query.equal('status', ['active', 'paused']), Query.isNull('status')])]
: [];

const activeProjects = await sdk.forConsole.projects.list({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@
canWriteSites
} from '$lib/stores/roles';
import CsvImportBox from '$lib/components/csvImportBox.svelte';
import { isCloud } from '$lib/system';
import PausedProjectModal from './pausedProjectModal.svelte';
import type { LayoutData } from './$types';

export let data: LayoutData;

onMount(() => {
return realtime.forProject(page.params.region, ['project', 'console'], (response) => {
Expand Down Expand Up @@ -114,6 +119,10 @@

<slot />

{#if isCloud && data.project?.status === 'paused'}
<PausedProjectModal show={true} projectId={data.project.$id} />
{/if}

<div class="layout-level-progress-bars">
<UploadBox />
<MigrationBox />
Expand Down
Loading