import { Env, SyncResponse, CipherResponse, FolderResponse, ProfileResponse } from '../types'; import { StorageService } from '../services/storage'; import { errorResponse } from '../utils/response'; import { cipherToResponse, isCipherResponseSyncCompatible } from './ciphers'; import { sendToResponse } from './sends'; import { LIMITS } from '../config/limits'; import { buildAccountKeys, buildUserDecryptionCompat, buildUserDecryptionOptions, } from '../utils/user-decryption'; import { buildDomainsResponse } from '../services/domain-rules'; function buildSyncCacheRequest(request: Request, userId: string, revisionDate: string, excludeDomains: boolean, excludeSends: boolean): Request { const url = new URL(request.url); const cacheUrl = new URL( `/__nodewarden/cache/sync/${encodeURIComponent(userId)}/${encodeURIComponent(revisionDate)}/${excludeDomains ? '1' : '0'}/${excludeSends ? '1' : '0'}`, url.origin ); return new Request(cacheUrl.toString(), { method: 'GET' }); } async function readSyncCache(cacheRequest: Request): Promise { const hit = await caches.default.match(cacheRequest); if (!hit) return null; return new Response(hit.body, hit); } async function writeSyncCache(cacheRequest: Request, response: Response): Promise { await caches.default.put(cacheRequest, response.clone()); } // GET /api/sync export async function handleSync(request: Request, env: Env, userId: string): Promise { const storage = new StorageService(env.DB); const url = new URL(request.url); const excludeDomainsParam = url.searchParams.get('excludeDomains'); const excludeDomains = excludeDomainsParam !== null && /^(1|true|yes)$/i.test(excludeDomainsParam); const excludeSendsParam = url.searchParams.get('excludeSends'); const excludeSends = excludeSendsParam !== null && /^(1|true|yes)$/i.test(excludeSendsParam); const user = await storage.getUserById(userId); if (!user) { return errorResponse('User not found', 404); } const revisionDate = await storage.getRevisionDate(userId); const cacheRequest = buildSyncCacheRequest(request, userId, revisionDate, excludeDomains, excludeSends); const cachedResponse = await readSyncCache(cacheRequest); if (cachedResponse) { return cachedResponse; } const [ciphers, folders, sends, attachmentsByCipher, domainSettings] = await Promise.all([ storage.getAllCiphers(userId), storage.getAllFolders(userId), excludeSends ? Promise.resolve([]) : storage.getAllSends(userId), storage.getAttachmentsByUserId(userId), excludeDomains ? Promise.resolve(null) : storage.getUserDomainSettings(userId), ]); const accountKeys = buildAccountKeys(user); const userDecryptionOptions = buildUserDecryptionOptions(user); const profile: ProfileResponse = { id: user.id, name: user.name, email: user.email, emailVerified: true, premium: true, premiumFromOrganization: false, usesKeyConnector: false, masterPasswordHint: user.masterPasswordHint, culture: 'en-US', twoFactorEnabled: !!user.totpSecret, key: user.key, privateKey: user.privateKey, accountKeys, securityStamp: user.securityStamp || user.id, organizations: [], providers: [], providerOrganizations: [], forcePasswordReset: false, avatarColor: null, creationDate: user.createdAt, verifyDevices: user.verifyDevices, object: 'profile', }; const cipherResponses: CipherResponse[] = []; for (const cipher of ciphers) { const response = cipherToResponse(cipher, attachmentsByCipher.get(cipher.id) || []); if (isCipherResponseSyncCompatible(response)) { cipherResponses.push(response); } } const folderResponses: FolderResponse[] = []; for (const folder of folders) { folderResponses.push({ id: folder.id, name: folder.name, revisionDate: folder.updatedAt, creationDate: folder.createdAt, object: 'folder', }); } const sendResponses = sends.map(sendToResponse); const syncResponse: SyncResponse = { profile, folders: folderResponses, collections: [], ciphers: cipherResponses, domains: excludeDomains ? null : buildDomainsResponse( domainSettings?.equivalentDomains || [], domainSettings?.customEquivalentDomains || [], domainSettings?.excludedGlobalEquivalentDomains || [], { omitExcludedGlobals: true } ), policies: [], sends: sendResponses, UserDecryption: { MasterPasswordUnlock: userDecryptionOptions.MasterPasswordUnlock, TrustedDeviceOption: null, KeyConnectorOption: null, Object: 'userDecryption', }, UserDecryptionOptions: userDecryptionOptions, userDecryption: buildUserDecryptionCompat(user) as SyncResponse['userDecryption'], object: 'sync', }; const response = new Response(JSON.stringify(syncResponse), { status: 200, headers: { 'Content-Type': 'application/json', 'Cache-Control': `private, max-age=${Math.max(1, Math.floor(LIMITS.cache.syncResponseTtlMs / 1000))}`, }, }); await writeSyncCache(cacheRequest, response); return response; }