security: remove VITE_TLSYNC_SECRET from frontend bundle

- Remove import.meta.env.VITE_TLSYNC_SECRET from syncService.ts
- Add optional token parameter to SyncConnectionOptions interface
  (caller passes token fetched from API, not a baked-in secret)
- Add clientEnvBlocklist in vite.config.ts to prevent sensitive VITE_
  env vars (TLSYNC_SECRET, MICROSOFT_CLIENT_SECRET*) from reaching bundle
- Remove VITE_TLSYNC_SECRET from .env.example
- Token auth for TLSync now handled server-side or via API endpoint

Ref: t_99d166c6
This commit is contained in:
kcar 2026-05-28 13:51:46 +01:00
parent 0db53bfd9c
commit 0d76868a42
3 changed files with 14 additions and 5 deletions

View File

@ -27,7 +27,6 @@ VITE_SUPABASE_ANON_KEY=your-supabase-anon-key
# TLSync (TLDraw Sync) Configuration
# =============================================================================
VITE_TLSYNC_URL=https://app.classroomcopilot.ai/tldraw
VITE_TLSYNC_SECRET=your-tlsync-secret
# =============================================================================
# WhisperLive (Transcription) Configuration

View File

@ -15,6 +15,8 @@ export interface SyncConnectionOptions {
color: string;
roomId?: string;
baseUrl: string;
/** Optional TLSync auth token (fetched from API server-side, never baked into bundle) */
token?: string;
}
export function createSyncConnectionOptions(options: SyncConnectionOptions) {
@ -22,8 +24,8 @@ export function createSyncConnectionOptions(options: SyncConnectionOptions) {
userId,
displayName,
roomId = 'multiplayer',
baseUrl
baseUrl,
token = ''
} = options;
// Ensure we have valid user info
@ -72,7 +74,8 @@ export function createSyncConnectionOptions(options: SyncConnectionOptions) {
roomId: effectiveRoomId
});
const token = import.meta.env.VITE_TLSYNC_SECRET ?? ''
// Token is provided by the caller (e.g. fetched from API server-side).
// Never embed a shared secret in the frontend bundle.
const tokenParam = token ? `?token=${encodeURIComponent(token)}` : ''
return {

View File

@ -14,9 +14,16 @@ export default defineConfig(({ mode }) => {
// Determine client-side env vars to expose
const envPrefix = 'VITE_'
// Blocklist: sensitive env vars that must NEVER reach the client bundle.
const clientEnvBlocklist = new Set([
'VITE_TLSYNC_SECRET',
'VITE_MICROSOFT_CLIENT_SECRET',
'VITE_MICROSOFT_CLIENT_SECRET_ID',
])
const clientEnv = Object.fromEntries(
Object.entries(env)
.filter(([key]) => key.startsWith(envPrefix))
.filter(([key]) => key.startsWith(envPrefix) && !clientEnvBlocklist.has(key))
.map(([key, value]) => [`import.meta.env.${key}`, JSON.stringify(value)])
)