Files
app/src/services/tldraw/syncService.ts
T
kcar 67e47fc47f
app-ci-deploy / test-build-deploy (push) Has been cancelled
feat(tlsync): fetch short-lived token from API before multiplayer connect
- Remove VITE_TLSYNC_SECRET from browser env (no longer exposed to bundle)
- Add useTlsyncToken hook that fetches /api/tlsync/token with Supabase auth
- Extract TldrawCanvas sub-component: only renders after token is ready
- Pass API-issued short-lived token to createSyncConnectionOptions
- Add vite.config.ts blocklist to prevent secret leak (defense-in-depth)
- Remove VITE_TLSYNC_SECRET from .env.example (server-side only now)

Related: t_a69128a1 (API token endpoint), t_41a844a7 (this task)
2026-05-28 18:00:43 +01:00

120 lines
3.3 KiB
TypeScript

// External imports
import {
TLAssetStore,
uniqueId,
TLAsset,
TLBookmarkAsset,
AssetRecordType,
getHashForString,
} from '@tldraw/tldraw';
import { logger } from '../../debugConfig';
export interface SyncConnectionOptions {
userId: string;
displayName: string;
color: string;
roomId?: string;
baseUrl: string;
token?: string;
}
export function createSyncConnectionOptions(options: SyncConnectionOptions) {
const {
userId,
displayName,
roomId = 'multiplayer',
baseUrl,
token
} = options;
// Ensure we have valid user info
if (!userId || !displayName) {
logger.warn('sync-service', 'Missing user information', { userId, displayName });
}
// Create a unique room ID if not provided
const effectiveRoomId = roomId || `room-${uniqueId()}`;
const multiplayerAssets: TLAssetStore = {
async upload(_asset: unknown, file: File) {
const id = uniqueId();
const objectName = `${id}-${file.name}`;
const uploadPath = '/uploads';
const url = `${baseUrl}${uploadPath}/${encodeURIComponent(objectName)}`;
try {
const response = await fetch(url, {
method: 'PUT',
body: file,
headers: {
'Content-Type': file.type
}
});
if (!response.ok) {
const errorDetail = await response.text();
throw new Error(`Failed to upload asset: ${response.statusText} - Details: ${errorDetail}`);
}
return url;
} catch (error) {
logger.error('sync-service', 'Error during asset upload: ', error);
throw error;
}
},
resolve(asset: TLAsset) {
return asset.props.src ?? '';
}
};
logger.info('sync-service', '🔄 Creating sync connection', {
userId,
displayName,
roomId: effectiveRoomId
});
const tokenParam = token ? `?token=${encodeURIComponent(token)}` : '';
return {
uri: `${baseUrl}/connect/${effectiveRoomId}${tokenParam}`,
assets: multiplayerAssets,
roomId: effectiveRoomId
};
}
export async function handleExternalAsset(baseUrl: string, url: string): Promise<TLBookmarkAsset> {
const asset: TLBookmarkAsset = {
id: AssetRecordType.createId(getHashForString(url)),
typeName: 'asset',
type: 'bookmark',
props: {
src: url,
description: '',
image: '',
favicon: '',
title: ''
},
meta: {}
};
try {
const response = await fetch(`${baseUrl}/unfurl?url=${encodeURIComponent(url)}`);
const data = await response.json();
asset.props = {
...asset.props,
...data
};
} catch (error) {
logger.error('sync-service', 'Error unfurling URL:', error);
}
return asset;
}
export function generateSharedRoomId(path: string): string {
// Create a deterministic room ID based on the path
const sanitizedPath = path.replace(/[^a-zA-Z0-9]/g, '-');
return `shared-${sanitizedPath}`;
}