Align web vault updates with resource sync

This commit is contained in:
shuaiplus
2026-06-21 18:16:44 +08:00
parent 42b765b113
commit 045b23fc47
5 changed files with 274 additions and 93 deletions
+1
View File
@@ -102,6 +102,7 @@ async function processSendFileUpload(
const storage = new StorageService(env.DB); const storage = new StorageService(env.DB);
const revisionDate = await storage.updateRevisionDate(send.userId); const revisionDate = await storage.updateRevisionDate(send.userId);
notifyVaultSyncForRequest(request, env, send.userId, revisionDate); notifyVaultSyncForRequest(request, env, send.userId, revisionDate);
notifySendUpdateForRequest(request, env, send.id, send.userId, revisionDate);
return new Response(null, { status: 201 }); return new Response(null, { status: 201 });
} }
+87 -31
View File
@@ -20,6 +20,7 @@ import {
saveProfileSnapshot, saveProfileSnapshot,
revokeCurrentSession, revokeCurrentSession,
getTotpStatus, getTotpStatus,
getVaultRevisionDate,
saveSession, saveSession,
stripProfileSecrets, stripProfileSecrets,
} from '@/lib/api/auth'; } from '@/lib/api/auth';
@@ -33,7 +34,7 @@ import { clearAuditLogs, getAuditLogSettings, listAdminInvites, listAdminUsers,
import { getDomainRules, saveDomainRules } from '@/lib/api/domains'; import { getDomainRules, saveDomainRules } from '@/lib/api/domains';
import { getSendById, getSends } from '@/lib/api/send'; import { getSendById, getSends } from '@/lib/api/send';
import { getCipherById, getFolderById, repairCipherKeyMismatches, repairCipherUriChecksums } from '@/lib/api/vault'; import { getCipherById, getFolderById, repairCipherKeyMismatches, repairCipherUriChecksums } from '@/lib/api/vault';
import { getCachedVaultCoreSnapshot, invalidateVaultCoreSyncSnapshot, loadVaultCoreSyncSnapshot } from '@/lib/api/vault-sync'; import { getCachedVaultCoreSnapshot, invalidateVaultCoreSyncSnapshot, loadVaultCoreSyncSnapshot, saveVaultCoreSyncSnapshot } from '@/lib/api/vault-sync';
import { silentlyRepairBackupSettingsIfNeeded } from '@/lib/backup-settings-repair'; import { silentlyRepairBackupSettingsIfNeeded } from '@/lib/backup-settings-repair';
import { import {
parseSignalRTextFrames, parseSignalRTextFrames,
@@ -1361,7 +1362,15 @@ export default function App() {
return items.filter((item) => String(item.id || '').trim() !== normalizedId); return items.filter((item) => String(item.id || '').trim() !== normalizedId);
} }
function patchVaultCoreSnapshot(updater: (snapshot: VaultCoreSnapshot) => VaultCoreSnapshot): void { function revisionStampFromIso(value: unknown): number | null {
const stamp = new Date(String(value || '').trim()).getTime();
return Number.isFinite(stamp) && stamp > 0 ? stamp : null;
}
function patchVaultCoreSnapshot(
updater: (snapshot: VaultCoreSnapshot) => VaultCoreSnapshot,
options?: { revisionStamp?: number | null }
): void {
if (!vaultCacheKey) return; if (!vaultCacheKey) return;
let nextSnapshot: VaultCoreSnapshot | null = null; let nextSnapshot: VaultCoreSnapshot | null = null;
queryClient.setQueryData(['vault-core', vaultCacheKey], (previous?: VaultCoreSnapshot) => { queryClient.setQueryData(['vault-core', vaultCacheKey], (previous?: VaultCoreSnapshot) => {
@@ -1369,34 +1378,50 @@ export default function App() {
nextSnapshot = updater(base); nextSnapshot = updater(base);
return nextSnapshot; return nextSnapshot;
}); });
if (nextSnapshot) setCachedVaultCore(nextSnapshot); if (nextSnapshot) {
setCachedVaultCore(nextSnapshot);
void saveVaultCoreSyncSnapshot(vaultCacheKey, nextSnapshot, options?.revisionStamp ?? null);
}
} }
function upsertEncryptedCipher(cipher: Cipher): void { async function refreshVaultCoreRevisionStamp(): Promise<void> {
if (!vaultCacheKey || !session?.accessToken) return;
try {
const revisionStamp = await getVaultRevisionDate(authedFetch);
const currentSnapshot = normalizeVaultCoreSnapshot(
queryClient.getQueryData<VaultCoreSnapshot>(['vault-core', vaultCacheKey]) || cachedVaultCore
);
await saveVaultCoreSyncSnapshot(vaultCacheKey, currentSnapshot, revisionStamp);
} catch {
// A stale revision stamp only affects the next cache validation; the local resource patch remains valid.
}
}
function upsertEncryptedCipher(cipher: Cipher, revisionStamp?: number | null): void {
patchVaultCoreSnapshot((snapshot) => ({ patchVaultCoreSnapshot((snapshot) => ({
...snapshot, ...snapshot,
ciphers: upsertById(snapshot.ciphers, cipher), ciphers: upsertById(snapshot.ciphers, cipher),
})); }), { revisionStamp: revisionStamp ?? revisionStampFromIso(cipher.revisionDate) });
} }
function deleteCipherLocally(cipherId: string): void { function deleteCipherLocally(cipherId: string, revisionStamp?: number | null): void {
const id = String(cipherId || '').trim(); const id = String(cipherId || '').trim();
if (!id) return; if (!id) return;
patchVaultCoreSnapshot((snapshot) => ({ patchVaultCoreSnapshot((snapshot) => ({
...snapshot, ...snapshot,
ciphers: removeById(snapshot.ciphers, id), ciphers: removeById(snapshot.ciphers, id),
})); }), { revisionStamp });
setDecryptedCiphers((current) => removeById(current, id)); setDecryptedCiphers((current) => removeById(current, id));
} }
function upsertEncryptedFolder(folder: VaultFolder): void { function upsertEncryptedFolder(folder: VaultFolder, revisionStamp?: number | null): void {
patchVaultCoreSnapshot((snapshot) => ({ patchVaultCoreSnapshot((snapshot) => ({
...snapshot, ...snapshot,
folders: upsertById(snapshot.folders, folder), folders: upsertById(snapshot.folders, folder),
})); }), { revisionStamp: revisionStamp ?? revisionStampFromIso(folder.revisionDate) });
} }
function deleteFolderLocally(folderId: string): void { function deleteFolderLocally(folderId: string, revisionStamp?: number | null): void {
const id = String(folderId || '').trim(); const id = String(folderId || '').trim();
if (!id) return; if (!id) return;
patchVaultCoreSnapshot((snapshot) => ({ patchVaultCoreSnapshot((snapshot) => ({
@@ -1405,38 +1430,38 @@ export default function App() {
ciphers: snapshot.ciphers.map((cipher) => ( ciphers: snapshot.ciphers.map((cipher) => (
String(cipher.folderId || '').trim() === id ? { ...cipher, folderId: null } : cipher String(cipher.folderId || '').trim() === id ? { ...cipher, folderId: null } : cipher
)), )),
})); }), { revisionStamp });
setDecryptedFolders((current) => removeById(current, id)); setDecryptedFolders((current) => removeById(current, id));
setDecryptedCiphers((current) => current.map((cipher) => ( setDecryptedCiphers((current) => current.map((cipher) => (
String(cipher.folderId || '').trim() === id ? { ...cipher, folderId: null } : cipher String(cipher.folderId || '').trim() === id ? { ...cipher, folderId: null } : cipher
))); )));
} }
function upsertEncryptedSend(send: Send): void { function upsertEncryptedSend(send: Send, revisionStamp?: number | null): void {
patchVaultCoreSnapshot((snapshot) => ({ patchVaultCoreSnapshot((snapshot) => ({
...snapshot, ...snapshot,
sends: upsertById(snapshot.sends, send), sends: upsertById(snapshot.sends, send),
})); }), { revisionStamp: revisionStamp ?? revisionStampFromIso(send.revisionDate) });
queryClient.setQueryData(sendsQueryKey, (previous?: Send[]) => upsertById(Array.isArray(previous) ? previous : [], send)); queryClient.setQueryData(sendsQueryKey, (previous?: Send[]) => upsertById(Array.isArray(previous) ? previous : [], send));
} }
function deleteSendLocally(sendId: string): void { function deleteSendLocally(sendId: string, revisionStamp?: number | null): void {
const id = String(sendId || '').trim(); const id = String(sendId || '').trim();
if (!id) return; if (!id) return;
patchVaultCoreSnapshot((snapshot) => ({ patchVaultCoreSnapshot((snapshot) => ({
...snapshot, ...snapshot,
sends: removeById(snapshot.sends, id), sends: removeById(snapshot.sends, id),
})); }), { revisionStamp });
queryClient.setQueryData(sendsQueryKey, (previous?: Send[]) => removeById(Array.isArray(previous) ? previous : [], id)); queryClient.setQueryData(sendsQueryKey, (previous?: Send[]) => removeById(Array.isArray(previous) ? previous : [], id));
setDecryptedSends((current) => removeById(current, id)); setDecryptedSends((current) => removeById(current, id));
} }
async function upsertCipherFromNotification(cipherId: string): Promise<void> { async function upsertCipherFromNotification(cipherId: string, revisionStamp?: number | null): Promise<void> {
const id = String(cipherId || '').trim(); const id = String(cipherId || '').trim();
if (!id || !session?.symEncKey || !session?.symMacKey) return; if (!id || !session?.symEncKey || !session?.symMacKey) return;
try { try {
const encrypted = await getCipherById(authedFetch, id); const encrypted = await getCipherById(authedFetch, id);
upsertEncryptedCipher(encrypted); upsertEncryptedCipher(encrypted, revisionStamp);
const result = await decryptVaultCore({ const result = await decryptVaultCore({
folders: [], folders: [],
ciphers: [encrypted], ciphers: [encrypted],
@@ -1454,12 +1479,12 @@ export default function App() {
} }
} }
async function upsertFolderFromNotification(folderId: string): Promise<void> { async function upsertFolderFromNotification(folderId: string, revisionStamp?: number | null): Promise<void> {
const id = String(folderId || '').trim(); const id = String(folderId || '').trim();
if (!id || !session?.symEncKey || !session?.symMacKey) return; if (!id || !session?.symEncKey || !session?.symMacKey) return;
try { try {
const encrypted = await getFolderById(authedFetch, id); const encrypted = await getFolderById(authedFetch, id);
upsertEncryptedFolder(encrypted); upsertEncryptedFolder(encrypted, revisionStamp);
const result = await decryptVaultCore({ const result = await decryptVaultCore({
folders: [encrypted], folders: [encrypted],
ciphers: [], ciphers: [],
@@ -1477,12 +1502,12 @@ export default function App() {
} }
} }
async function upsertSendFromNotification(sendId: string): Promise<void> { async function upsertSendFromNotification(sendId: string, revisionStamp?: number | null): Promise<void> {
const id = String(sendId || '').trim(); const id = String(sendId || '').trim();
if (!id || !session?.symEncKey || !session?.symMacKey) return; if (!id || !session?.symEncKey || !session?.symMacKey) return;
try { try {
const encrypted = await getSendById(authedFetch, id); const encrypted = await getSendById(authedFetch, id);
upsertEncryptedSend(encrypted); upsertEncryptedSend(encrypted, revisionStamp);
const sends = await decryptSends({ const sends = await decryptSends({
sends: [encrypted], sends: [encrypted],
symEncKeyB64: session.symEncKey, symEncKeyB64: session.symEncKey,
@@ -1576,11 +1601,18 @@ export default function App() {
const frames = parseSignalRTextFrames(event.data); const frames = parseSignalRTextFrames(event.data);
for (const frame of frames) { for (const frame of frames) {
if (frame.type !== 1 || frame.target !== 'ReceiveMessage') continue; if (frame.type !== 1 || frame.target !== 'ReceiveMessage') continue;
const updateType = Number(frame.arguments?.[0]?.Type || 0); const message = frame.arguments?.[0] as Record<string, unknown> | undefined;
const contextId = String(frame.arguments?.[0]?.ContextId || '').trim(); const updateType = Number(message?.Type || 0);
const payload = frame.arguments?.[0]?.Payload; const contextId = String(message?.ContextId || '').trim();
const payload = message?.Payload;
const payloadRecord = payload && typeof payload === 'object' ? payload as Record<string, unknown> : null; const payloadRecord = payload && typeof payload === 'object' ? payload as Record<string, unknown> : null;
const resourceId = String(payloadRecord?.Id || payloadRecord?.id || '').trim(); const resourceId = String(payloadRecord?.Id || payloadRecord?.id || '').trim();
const revisionStamp = revisionStampFromIso(
payloadRecord?.RevisionDate
|| payloadRecord?.revisionDate
|| message?.Date
|| message?.date
);
if (updateType === SIGNALR_UPDATE_TYPE_LOG_OUT) { if (updateType === SIGNALR_UPDATE_TYPE_LOG_OUT) {
logoutNow(); logoutNow();
return; return;
@@ -1590,7 +1622,6 @@ export default function App() {
continue; continue;
} }
if (updateType === SIGNALR_UPDATE_TYPE_BACKUP_RESTORE_PROGRESS) { if (updateType === SIGNALR_UPDATE_TYPE_BACKUP_RESTORE_PROGRESS) {
const payload = frame.arguments?.[0]?.Payload;
if (isBackupProgressDetail(payload)) dispatchBackupProgress(payload); if (isBackupProgressDetail(payload)) dispatchBackupProgress(payload);
continue; continue;
} }
@@ -1606,27 +1637,27 @@ export default function App() {
continue; continue;
} }
if ((updateType === SIGNALR_UPDATE_TYPE_SYNC_CIPHER_CREATE || updateType === SIGNALR_UPDATE_TYPE_SYNC_CIPHER_UPDATE) && resourceId) { if ((updateType === SIGNALR_UPDATE_TYPE_SYNC_CIPHER_CREATE || updateType === SIGNALR_UPDATE_TYPE_SYNC_CIPHER_UPDATE) && resourceId) {
void upsertCipherFromNotification(resourceId); void upsertCipherFromNotification(resourceId, revisionStamp);
continue; continue;
} }
if (updateType === SIGNALR_UPDATE_TYPE_SYNC_CIPHER_DELETE && resourceId) { if (updateType === SIGNALR_UPDATE_TYPE_SYNC_CIPHER_DELETE && resourceId) {
deleteCipherLocally(resourceId); deleteCipherLocally(resourceId, revisionStamp);
continue; continue;
} }
if ((updateType === SIGNALR_UPDATE_TYPE_SYNC_FOLDER_CREATE || updateType === SIGNALR_UPDATE_TYPE_SYNC_FOLDER_UPDATE) && resourceId) { if ((updateType === SIGNALR_UPDATE_TYPE_SYNC_FOLDER_CREATE || updateType === SIGNALR_UPDATE_TYPE_SYNC_FOLDER_UPDATE) && resourceId) {
void upsertFolderFromNotification(resourceId); void upsertFolderFromNotification(resourceId, revisionStamp);
continue; continue;
} }
if (updateType === SIGNALR_UPDATE_TYPE_SYNC_FOLDER_DELETE && resourceId) { if (updateType === SIGNALR_UPDATE_TYPE_SYNC_FOLDER_DELETE && resourceId) {
deleteFolderLocally(resourceId); deleteFolderLocally(resourceId, revisionStamp);
continue; continue;
} }
if ((updateType === SIGNALR_UPDATE_TYPE_SYNC_SEND_CREATE || updateType === SIGNALR_UPDATE_TYPE_SYNC_SEND_UPDATE) && resourceId) { if ((updateType === SIGNALR_UPDATE_TYPE_SYNC_SEND_CREATE || updateType === SIGNALR_UPDATE_TYPE_SYNC_SEND_UPDATE) && resourceId) {
void upsertSendFromNotification(resourceId); void upsertSendFromNotification(resourceId, revisionStamp);
continue; continue;
} }
if (updateType === SIGNALR_UPDATE_TYPE_SYNC_SEND_DELETE && resourceId) { if (updateType === SIGNALR_UPDATE_TYPE_SYNC_SEND_DELETE && resourceId) {
deleteSendLocally(resourceId); deleteSendLocally(resourceId, revisionStamp);
continue; continue;
} }
if (updateType === SIGNALR_UPDATE_TYPE_SYNC_VAULT) continue; if (updateType === SIGNALR_UPDATE_TYPE_SYNC_VAULT) continue;
@@ -1688,8 +1719,33 @@ export default function App() {
}, },
refetchSends: refetchSendsFromVaultCore, refetchSends: refetchSendsFromVaultCore,
onNotify: pushToast, onNotify: pushToast,
patchEncryptedCiphers: (updater) => {
patchVaultCoreSnapshot((snapshot) => ({
...snapshot,
ciphers: updater(snapshot.ciphers),
}));
},
patchEncryptedFolders: (updater) => {
patchVaultCoreSnapshot((snapshot) => ({
...snapshot,
folders: updater(snapshot.folders),
}));
},
patchEncryptedSends: (updater) => {
let nextSends: Send[] = [];
patchVaultCoreSnapshot((snapshot) => {
nextSends = updater(snapshot.sends);
return {
...snapshot,
sends: nextSends,
};
});
queryClient.setQueryData(sendsQueryKey, nextSends);
},
patchDecryptedCiphers: setDecryptedCiphers, patchDecryptedCiphers: setDecryptedCiphers,
patchDecryptedFolders: setDecryptedFolders, patchDecryptedFolders: setDecryptedFolders,
patchDecryptedSends: setDecryptedSends,
refreshVaultRevisionStamp: refreshVaultCoreRevisionStamp,
}); });
const accountSecurityActions = useAccountSecurityActions({ const accountSecurityActions = useAccountSecurityActions({
authedFetch, authedFetch,
+143 -45
View File
@@ -41,6 +41,7 @@ import {
downloadCipherAttachmentDecrypted, downloadCipherAttachmentDecrypted,
encryptFolderImportName, encryptFolderImportName,
getAttachmentDownloadInfo, getAttachmentDownloadInfo,
getCipherById,
importCiphers, importCiphers,
permanentDeleteCipher, permanentDeleteCipher,
type CiphersImportPayload, type CiphersImportPayload,
@@ -69,8 +70,13 @@ interface UseVaultSendActionsOptions {
refetchFolders: () => Promise<{ data?: VaultFolder[] | undefined } | unknown>; refetchFolders: () => Promise<{ data?: VaultFolder[] | undefined } | unknown>;
refetchSends: () => Promise<unknown>; refetchSends: () => Promise<unknown>;
onNotify: Notify; onNotify: Notify;
patchEncryptedCiphers: (updater: (prev: Cipher[]) => Cipher[]) => void;
patchEncryptedFolders: (updater: (prev: VaultFolder[]) => VaultFolder[]) => void;
patchEncryptedSends: (updater: (prev: Send[]) => Send[]) => void;
patchDecryptedCiphers: (updater: (prev: Cipher[]) => Cipher[]) => void; patchDecryptedCiphers: (updater: (prev: Cipher[]) => Cipher[]) => void;
patchDecryptedFolders: (updater: (prev: VaultFolder[]) => VaultFolder[]) => void; patchDecryptedFolders: (updater: (prev: VaultFolder[]) => VaultFolder[]) => void;
patchDecryptedSends: (updater: (prev: Send[]) => Send[]) => void;
refreshVaultRevisionStamp: () => Promise<void>;
} }
function extractImportIdMaps(cipherMap: ImportedCipherMapEntry[] | null) { function extractImportIdMaps(cipherMap: ImportedCipherMapEntry[] | null) {
@@ -288,8 +294,13 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
refetchFolders, refetchFolders,
refetchSends, refetchSends,
onNotify, onNotify,
patchEncryptedCiphers,
patchEncryptedFolders,
patchEncryptedSends,
patchDecryptedCiphers, patchDecryptedCiphers,
patchDecryptedFolders, patchDecryptedFolders,
patchDecryptedSends,
refreshVaultRevisionStamp,
} = options; } = options;
const [downloadingAttachmentKey, setDownloadingAttachmentKey] = useState(''); const [downloadingAttachmentKey, setDownloadingAttachmentKey] = useState('');
const [attachmentDownloadPercent, setAttachmentDownloadPercent] = useState<number | null>(null); const [attachmentDownloadPercent, setAttachmentDownloadPercent] = useState<number | null>(null);
@@ -308,21 +319,20 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
throw new Error(t('txt_offline_vault_readonly')); throw new Error(t('txt_offline_vault_readonly'));
}; };
const syncVaultCoreInBackground = (options?: { includeFolders?: boolean }) => {
const tasks: Promise<unknown>[] = [Promise.resolve(refetchCiphers())];
if (options?.includeFolders) {
tasks.push(Promise.resolve(refetchFolders()));
}
void Promise.all(tasks).catch((err) => {
console.warn('Background vault sync failed:', err);
});
};
async function decryptAndPatch(encrypted: Cipher) { async function decryptAndPatch(encrypted: Cipher) {
if (!session?.symEncKey || !session?.symMacKey) { if (!session?.symEncKey || !session?.symMacKey) {
await refetchCiphers(); await refetchCiphers();
return; return;
} }
patchEncryptedCiphers((prev) => {
const idx = prev.findIndex((c) => c.id === encrypted.id);
if (idx >= 0) {
const next = [...prev];
next[idx] = encrypted;
return next;
}
return [encrypted, ...prev];
});
const encKey = base64ToBytes(session.symEncKey); const encKey = base64ToBytes(session.symEncKey);
const macKey = base64ToBytes(session.symMacKey); const macKey = base64ToBytes(session.symMacKey);
const decrypted = await decryptSingleCipher(encrypted, encKey, macKey); const decrypted = await decryptSingleCipher(encrypted, encKey, macKey);
@@ -342,6 +352,7 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
await refetchCiphers(); await refetchCiphers();
return; return;
} }
patchEncryptedCiphers((prev) => [encrypted, ...prev.filter((cipher) => cipher.id !== optimisticId && cipher.id !== encrypted.id)]);
const encKey = base64ToBytes(session.symEncKey); const encKey = base64ToBytes(session.symEncKey);
const macKey = base64ToBytes(session.symMacKey); const macKey = base64ToBytes(session.symMacKey);
const decrypted = await decryptSingleCipher(encrypted, encKey, macKey); const decrypted = await decryptSingleCipher(encrypted, encKey, macKey);
@@ -352,12 +363,36 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
} }
function removeCipherFromState(id: string) { function removeCipherFromState(id: string) {
patchEncryptedCiphers((prev) => prev.filter((c) => c.id !== id));
patchDecryptedCiphers((prev) => prev.filter((c) => c.id !== id)); patchDecryptedCiphers((prev) => prev.filter((c) => c.id !== id));
} }
function patchCipherBatch(ids: string[], updater: (cipher: Cipher) => Cipher | null) { function patchCipherBatch(
ids: string[],
updater: (cipher: Cipher) => Cipher | null,
options?: { patchEncrypted?: boolean; patchDecrypted?: boolean }
) {
const idSet = new Set(ids.map((id) => String(id || '').trim()).filter(Boolean)); const idSet = new Set(ids.map((id) => String(id || '').trim()).filter(Boolean));
if (!idSet.size) return; if (!idSet.size) return;
const shouldPatchEncrypted = options?.patchEncrypted !== false;
const shouldPatchDecrypted = options?.patchDecrypted !== false;
if (shouldPatchEncrypted) {
patchEncryptedCiphers((prev) => {
let changed = false;
const next: Cipher[] = [];
for (const cipher of prev) {
if (!idSet.has(cipher.id)) {
next.push(cipher);
continue;
}
const updated = updater(cipher);
changed = true;
if (updated) next.push(updated);
}
return changed ? next : prev;
});
}
if (shouldPatchDecrypted) {
patchDecryptedCiphers((prev) => { patchDecryptedCiphers((prev) => {
let changed = false; let changed = false;
const next: Cipher[] = []; const next: Cipher[] = [];
@@ -373,10 +408,25 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
return changed ? next : prev; return changed ? next : prev;
}); });
} }
}
function patchFolderBatch(ids: string[], updater: (folder: VaultFolder) => VaultFolder | null) { function patchFolderBatch(ids: string[], updater: (folder: VaultFolder) => VaultFolder | null) {
const idSet = new Set(ids.map((id) => String(id || '').trim()).filter(Boolean)); const idSet = new Set(ids.map((id) => String(id || '').trim()).filter(Boolean));
if (!idSet.size) return; if (!idSet.size) return;
patchEncryptedFolders((prev) => {
let changed = false;
const next: VaultFolder[] = [];
for (const folder of prev) {
if (!idSet.has(folder.id)) {
next.push(folder);
continue;
}
const updated = updater(folder);
changed = true;
if (updated) next.push(updated);
}
return changed ? next : prev;
});
patchDecryptedFolders((prev) => { patchDecryptedFolders((prev) => {
let changed = false; let changed = false;
const next: VaultFolder[] = []; const next: VaultFolder[] = [];
@@ -393,6 +443,31 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
}); });
} }
function upsertEncryptedFolder(folder: VaultFolder) {
patchEncryptedFolders((prev) => {
const index = prev.findIndex((item) => item.id === folder.id);
if (index < 0) return [folder, ...prev];
const next = [...prev];
next[index] = folder;
return next;
});
}
function upsertSend(send: Send) {
patchEncryptedSends((prev) => {
const index = prev.findIndex((item) => item.id === send.id);
if (index < 0) return [send, ...prev];
const next = [...prev];
next[index] = send;
return next;
});
}
function removeSend(id: string) {
patchEncryptedSends((prev) => prev.filter((send) => send.id !== id));
patchDecryptedSends((prev) => prev.filter((send) => send.id !== id));
}
const uploadImportedAttachments = async ( const uploadImportedAttachments = async (
attachments: ImportAttachmentFile[], attachments: ImportAttachmentFile[],
idMaps: { byIndex: Map<number, string>; bySourceId: Map<string, string> } idMaps: { byIndex: Map<number, string>; bySourceId: Map<string, string> }
@@ -468,8 +543,9 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
setAttachmentUploadPercent(0); setAttachmentUploadPercent(0);
await uploadCipherAttachment(authedFetch, session, created.id, file, undefined, setAttachmentUploadPercent); await uploadCipherAttachment(authedFetch, session, created.id, file, undefined, setAttachmentUploadPercent);
} }
await decryptAndReplaceOptimistic(optimistic.id, created); const finalCipher = attachments.length ? await getCipherById(authedFetch, created.id) : created;
syncVaultCoreInBackground({ includeFolders: !!draft.folderId || attachments.length > 0 }); await decryptAndReplaceOptimistic(optimistic.id, finalCipher);
void refreshVaultRevisionStamp();
onNotify('success', t('txt_item_created')); onNotify('success', t('txt_item_created'));
} catch (error) { } catch (error) {
patchDecryptedCiphers((prev) => prev.filter((cipher) => cipher.id !== optimistic.id)); patchDecryptedCiphers((prev) => prev.filter((cipher) => cipher.id !== optimistic.id));
@@ -511,7 +587,7 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
.filter((attachment) => !removedSet.has(String(attachment?.id || '').trim())) .filter((attachment) => !removedSet.has(String(attachment?.id || '').trim()))
.map((attachment) => ({ ...attachment })); .map((attachment) => ({ ...attachment }));
} }
patchCipherBatch([cipher.id], () => optimistic); patchCipherBatch([cipher.id], () => optimistic, { patchEncrypted: false });
try { try {
const updated = await updateCipher(authedFetch, session, cipher, draft); const updated = await updateCipher(authedFetch, session, cipher, draft);
for (const attachmentId of removeAttachmentIds) { for (const attachmentId of removeAttachmentIds) {
@@ -524,16 +600,14 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
setAttachmentUploadPercent(0); setAttachmentUploadPercent(0);
await uploadCipherAttachment(authedFetch, session, cipher.id, file, cipher, setAttachmentUploadPercent); await uploadCipherAttachment(authedFetch, session, cipher.id, file, cipher, setAttachmentUploadPercent);
} }
await decryptAndPatch(updated); const finalCipher = addFiles.length || removeAttachmentIds.length
syncVaultCoreInBackground({ ? await getCipherById(authedFetch, cipher.id)
includeFolders: : updated;
draft.folderId !== (cipher.folderId || '') await decryptAndPatch(finalCipher);
|| addFiles.length > 0 void refreshVaultRevisionStamp();
|| removeAttachmentIds.length > 0,
});
onNotify('success', t('txt_item_updated')); onNotify('success', t('txt_item_updated'));
} catch (error) { } catch (error) {
patchCipherBatch([cipher.id], () => previousCipher); patchCipherBatch([cipher.id], () => previousCipher, { patchEncrypted: false });
onNotify('error', error instanceof Error ? error.message : t('txt_update_item_failed')); onNotify('error', error instanceof Error ? error.message : t('txt_update_item_failed'));
throw error; throw error;
} finally { } finally {
@@ -572,7 +646,7 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
try { try {
await permanentDeleteCipher(authedFetch, cipher.id); await permanentDeleteCipher(authedFetch, cipher.id);
patchCipherBatch([cipher.id], () => null); patchCipherBatch([cipher.id], () => null);
syncVaultCoreInBackground({ includeFolders: true }); void refreshVaultRevisionStamp();
onNotify('success', t('txt_item_deleted_permanently')); onNotify('success', t('txt_item_deleted_permanently'));
} catch (error) { } catch (error) {
onNotify('error', error instanceof Error ? error.message : t('txt_permanent_delete_item_failed')); onNotify('error', error instanceof Error ? error.message : t('txt_permanent_delete_item_failed'));
@@ -585,10 +659,10 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
try { try {
const deleted = await deleteCipher(authedFetch, cipher.id); const deleted = await deleteCipher(authedFetch, cipher.id);
await decryptAndPatch(deleted); await decryptAndPatch(deleted);
syncVaultCoreInBackground({ includeFolders: true }); void refreshVaultRevisionStamp();
onNotify('success', t('txt_item_deleted')); onNotify('success', t('txt_item_deleted'));
} catch (error) { } catch (error) {
patchCipherBatch([cipher.id], () => previousCipher); patchCipherBatch([cipher.id], () => previousCipher, { patchEncrypted: false });
onNotify('error', error instanceof Error ? error.message : t('txt_delete_item_failed')); onNotify('error', error instanceof Error ? error.message : t('txt_delete_item_failed'));
throw error; throw error;
} }
@@ -607,10 +681,10 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
try { try {
const archived = await archiveCipher(authedFetch, cipher.id); const archived = await archiveCipher(authedFetch, cipher.id);
await decryptAndPatch(archived); await decryptAndPatch(archived);
syncVaultCoreInBackground({ includeFolders: true }); void refreshVaultRevisionStamp();
onNotify('success', t('txt_item_archived')); onNotify('success', t('txt_item_archived'));
} catch (error) { } catch (error) {
patchCipherBatch([cipher.id], () => previousCipher); patchCipherBatch([cipher.id], () => previousCipher, { patchEncrypted: false });
onNotify('error', error instanceof Error ? error.message : t('txt_archive_item_failed')); onNotify('error', error instanceof Error ? error.message : t('txt_archive_item_failed'));
throw error; throw error;
} }
@@ -629,10 +703,10 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
try { try {
const unarchived = await unarchiveCipher(authedFetch, cipher.id); const unarchived = await unarchiveCipher(authedFetch, cipher.id);
await decryptAndPatch(unarchived); await decryptAndPatch(unarchived);
syncVaultCoreInBackground({ includeFolders: true }); void refreshVaultRevisionStamp();
onNotify('success', t('txt_item_unarchived')); onNotify('success', t('txt_item_unarchived'));
} catch (error) { } catch (error) {
patchCipherBatch([cipher.id], () => previousCipher); patchCipherBatch([cipher.id], () => previousCipher, { patchEncrypted: false });
onNotify('error', error instanceof Error ? error.message : t('txt_unarchive_item_failed')); onNotify('error', error instanceof Error ? error.message : t('txt_unarchive_item_failed'));
throw error; throw error;
} }
@@ -649,7 +723,7 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
await bulkDeleteCiphers(authedFetch, ids); await bulkDeleteCiphers(authedFetch, ids);
const deletedDate = new Date().toISOString(); const deletedDate = new Date().toISOString();
patchCipherBatch(ids, (cipher) => ({ ...cipher, deletedDate, archivedDate: null })); patchCipherBatch(ids, (cipher) => ({ ...cipher, deletedDate, archivedDate: null }));
syncVaultCoreInBackground({ includeFolders: true }); void refreshVaultRevisionStamp();
onNotify('success', t('txt_deleted_selected_items')); onNotify('success', t('txt_deleted_selected_items'));
} catch (error) { } catch (error) {
onNotify('error', error instanceof Error ? error.message : t('txt_bulk_delete_failed')); onNotify('error', error instanceof Error ? error.message : t('txt_bulk_delete_failed'));
@@ -668,7 +742,7 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
await bulkArchiveCiphers(authedFetch, ids); await bulkArchiveCiphers(authedFetch, ids);
const archivedDate = new Date().toISOString(); const archivedDate = new Date().toISOString();
patchCipherBatch(ids, (cipher) => ({ ...cipher, archivedDate, deletedDate: null })); patchCipherBatch(ids, (cipher) => ({ ...cipher, archivedDate, deletedDate: null }));
syncVaultCoreInBackground({ includeFolders: true }); void refreshVaultRevisionStamp();
onNotify('success', t('txt_archived_selected_items')); onNotify('success', t('txt_archived_selected_items'));
} catch (error) { } catch (error) {
onNotify('error', error instanceof Error ? error.message : t('txt_bulk_archive_failed')); onNotify('error', error instanceof Error ? error.message : t('txt_bulk_archive_failed'));
@@ -686,7 +760,7 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
try { try {
await bulkUnarchiveCiphers(authedFetch, ids); await bulkUnarchiveCiphers(authedFetch, ids);
patchCipherBatch(ids, (cipher) => ({ ...cipher, archivedDate: null })); patchCipherBatch(ids, (cipher) => ({ ...cipher, archivedDate: null }));
syncVaultCoreInBackground({ includeFolders: true }); void refreshVaultRevisionStamp();
onNotify('success', t('txt_unarchived_selected_items')); onNotify('success', t('txt_unarchived_selected_items'));
} catch (error) { } catch (error) {
onNotify('error', error instanceof Error ? error.message : t('txt_bulk_unarchive_failed')); onNotify('error', error instanceof Error ? error.message : t('txt_bulk_unarchive_failed'));
@@ -704,7 +778,7 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
try { try {
await bulkMoveCiphers(authedFetch, ids, folderId); await bulkMoveCiphers(authedFetch, ids, folderId);
patchCipherBatch(ids, (cipher) => ({ ...cipher, folderId })); patchCipherBatch(ids, (cipher) => ({ ...cipher, folderId }));
syncVaultCoreInBackground({ includeFolders: true }); void refreshVaultRevisionStamp();
onNotify('success', t('txt_moved_selected_items')); onNotify('success', t('txt_moved_selected_items'));
} catch (error) { } catch (error) {
onNotify('error', error instanceof Error ? error.message : t('txt_bulk_move_failed')); onNotify('error', error instanceof Error ? error.message : t('txt_bulk_move_failed'));
@@ -727,15 +801,18 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
try { try {
if (!session) throw new Error(t('txt_vault_key_unavailable')); if (!session) throw new Error(t('txt_vault_key_unavailable'));
const created = await createFolder(authedFetch, session, folderName); const created = await createFolder(authedFetch, session, folderName);
upsertEncryptedFolder(created);
patchDecryptedFolders((prev) => [ patchDecryptedFolders((prev) => [
{ {
id: created.id, id: created.id,
name: created.name || folderName, name: created.name || folderName,
decName: folderName, decName: folderName,
revisionDate: created.revisionDate,
creationDate: created.creationDate,
}, },
...prev, ...prev,
]); ]);
syncVaultCoreInBackground({ includeFolders: true }); void refreshVaultRevisionStamp();
onNotify('success', t('txt_folder_created')); onNotify('success', t('txt_folder_created'));
} catch (error) { } catch (error) {
onNotify('error', error instanceof Error ? error.message : t('txt_create_folder_failed')); onNotify('error', error instanceof Error ? error.message : t('txt_create_folder_failed'));
@@ -758,8 +835,9 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
try { try {
await deleteFolder(authedFetch, id); await deleteFolder(authedFetch, id);
patchFolderBatch([id], () => null); patchFolderBatch([id], () => null);
patchEncryptedCiphers((prev) => prev.map((cipher) => (cipher.folderId === id ? { ...cipher, folderId: null } : cipher)));
patchDecryptedCiphers((prev) => prev.map((cipher) => (cipher.folderId === id ? { ...cipher, folderId: null } : cipher))); patchDecryptedCiphers((prev) => prev.map((cipher) => (cipher.folderId === id ? { ...cipher, folderId: null } : cipher)));
syncVaultCoreInBackground({ includeFolders: true }); void refreshVaultRevisionStamp();
onNotify('success', t('txt_folder_deleted')); onNotify('success', t('txt_folder_deleted'));
} catch (error) { } catch (error) {
onNotify('error', error instanceof Error ? error.message : t('txt_delete_folder_failed')); onNotify('error', error instanceof Error ? error.message : t('txt_delete_folder_failed'));
@@ -786,9 +864,14 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
} }
try { try {
if (!session) throw new Error(t('txt_vault_key_unavailable')); if (!session) throw new Error(t('txt_vault_key_unavailable'));
await updateFolder(authedFetch, session, id, nextName); const updated = await updateFolder(authedFetch, session, id, nextName);
patchFolderBatch([id], (folder) => ({ ...folder, decName: nextName })); upsertEncryptedFolder(updated);
syncVaultCoreInBackground({ includeFolders: true }); patchDecryptedFolders((prev) => prev.map((folder) => (
folder.id === id
? { ...folder, name: updated.name || folder.name, decName: nextName, revisionDate: updated.revisionDate }
: folder
)));
void refreshVaultRevisionStamp();
onNotify('success', t('txt_folder_updated')); onNotify('success', t('txt_folder_updated'));
} catch (error) { } catch (error) {
onNotify('error', error instanceof Error ? error.message : t('txt_update_folder_failed')); onNotify('error', error instanceof Error ? error.message : t('txt_update_folder_failed'));
@@ -806,7 +889,7 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
try { try {
await bulkRestoreCiphers(authedFetch, ids); await bulkRestoreCiphers(authedFetch, ids);
patchCipherBatch(ids, (cipher) => ({ ...cipher, deletedDate: null })); patchCipherBatch(ids, (cipher) => ({ ...cipher, deletedDate: null }));
syncVaultCoreInBackground({ includeFolders: true }); void refreshVaultRevisionStamp();
onNotify('success', t('txt_restored_selected_items')); onNotify('success', t('txt_restored_selected_items'));
} catch (error) { } catch (error) {
onNotify('error', error instanceof Error ? error.message : t('txt_bulk_restore_failed')); onNotify('error', error instanceof Error ? error.message : t('txt_bulk_restore_failed'));
@@ -824,7 +907,7 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
try { try {
await bulkPermanentDeleteCiphers(authedFetch, ids); await bulkPermanentDeleteCiphers(authedFetch, ids);
patchCipherBatch(ids, () => null); patchCipherBatch(ids, () => null);
syncVaultCoreInBackground({ includeFolders: true }); void refreshVaultRevisionStamp();
onNotify('success', t('txt_deleted_selected_items_permanently')); onNotify('success', t('txt_deleted_selected_items_permanently'));
} catch (error) { } catch (error) {
onNotify('error', error instanceof Error ? error.message : t('txt_bulk_permanent_delete_failed')); onNotify('error', error instanceof Error ? error.message : t('txt_bulk_permanent_delete_failed'));
@@ -844,9 +927,11 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
try { try {
await bulkDeleteFolders(authedFetch, ids); await bulkDeleteFolders(authedFetch, ids);
const removedIds = new Set(ids); const removedIds = new Set(ids);
patchEncryptedFolders((prev) => prev.filter((folder) => !removedIds.has(folder.id)));
patchEncryptedCiphers((prev) => prev.map((cipher) => (cipher.folderId && removedIds.has(cipher.folderId) ? { ...cipher, folderId: null } : cipher)));
patchDecryptedFolders((prev) => prev.filter((folder) => !removedIds.has(folder.id))); patchDecryptedFolders((prev) => prev.filter((folder) => !removedIds.has(folder.id)));
patchDecryptedCiphers((prev) => prev.map((cipher) => (cipher.folderId && removedIds.has(cipher.folderId) ? { ...cipher, folderId: null } : cipher))); patchDecryptedCiphers((prev) => prev.map((cipher) => (cipher.folderId && removedIds.has(cipher.folderId) ? { ...cipher, folderId: null } : cipher)));
syncVaultCoreInBackground({ includeFolders: true }); void refreshVaultRevisionStamp();
onNotify('success', t('txt_folders_deleted')); onNotify('success', t('txt_folders_deleted'));
} catch (error) { } catch (error) {
onNotify('error', error instanceof Error ? error.message : t('txt_delete_all_folders_failed')); onNotify('error', error instanceof Error ? error.message : t('txt_delete_all_folders_failed'));
@@ -874,7 +959,8 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
setSendUploadPercent(0); setSendUploadPercent(0);
} }
const created = await createSend(authedFetch, session, draft, fileName ? setSendUploadPercent : undefined); const created = await createSend(authedFetch, session, draft, fileName ? setSendUploadPercent : undefined);
await refetchSends(); upsertSend(created);
void refreshVaultRevisionStamp();
if (autoCopyLink && created.key && session.symEncKey && session.symMacKey) { if (autoCopyLink && created.key && session.symEncKey && session.symMacKey) {
const keyPart = await buildSendShareKey(created.key, session.symEncKey, session.symMacKey); const keyPart = await buildSendShareKey(created.key, session.symEncKey, session.symMacKey);
const shareUrl = buildPublicSendUrl(window.location.origin, created.accessId, keyPart); const shareUrl = buildPublicSendUrl(window.location.origin, created.accessId, keyPart);
@@ -900,7 +986,8 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
} }
try { try {
const updated = await updateSend(authedFetch, session, send, draft); const updated = await updateSend(authedFetch, session, send, draft);
await refetchSends(); upsertSend(updated);
void refreshVaultRevisionStamp();
if (autoCopyLink && updated.key && session.symEncKey && session.symMacKey) { if (autoCopyLink && updated.key && session.symEncKey && session.symMacKey) {
const keyPart = await buildSendShareKey(updated.key, session.symEncKey, session.symMacKey); const keyPart = await buildSendShareKey(updated.key, session.symEncKey, session.symMacKey);
const shareUrl = buildPublicSendUrl(window.location.origin, updated.accessId, keyPart); const shareUrl = buildPublicSendUrl(window.location.origin, updated.accessId, keyPart);
@@ -922,7 +1009,8 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
} }
try { try {
await deleteSend(authedFetch, send.id); await deleteSend(authedFetch, send.id);
await refetchSends(); removeSend(send.id);
void refreshVaultRevisionStamp();
onNotify('success', t('txt_send_deleted')); onNotify('success', t('txt_send_deleted'));
} catch (error) { } catch (error) {
onNotify('error', error instanceof Error ? error.message : t('txt_delete_send_failed')); onNotify('error', error instanceof Error ? error.message : t('txt_delete_send_failed'));
@@ -939,7 +1027,10 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
} }
try { try {
await bulkDeleteSends(authedFetch, ids); await bulkDeleteSends(authedFetch, ids);
await refetchSends(); const idSet = new Set(ids.map((id) => String(id || '').trim()).filter(Boolean));
patchEncryptedSends((prev) => prev.filter((send) => !idSet.has(send.id)));
patchDecryptedSends((prev) => prev.filter((send) => !idSet.has(send.id)));
void refreshVaultRevisionStamp();
onNotify('success', t('txt_deleted_selected_sends')); onNotify('success', t('txt_deleted_selected_sends'));
} catch (error) { } catch (error) {
onNotify('error', error instanceof Error ? error.message : t('txt_bulk_delete_sends_failed')); onNotify('error', error instanceof Error ? error.message : t('txt_bulk_delete_sends_failed'));
@@ -1299,10 +1390,17 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
encryptedFolders, encryptedFolders,
importAuthedFetch, importAuthedFetch,
onNotify, onNotify,
patchDecryptedCiphers,
patchDecryptedFolders,
patchDecryptedSends,
patchEncryptedCiphers,
patchEncryptedFolders,
patchEncryptedSends,
profile, profile,
refetchCiphers, refetchCiphers,
refetchFolders, refetchFolders,
refetchSends, refetchSends,
refreshVaultRevisionStamp,
session, session,
sendUploadPercent, sendUploadPercent,
uploadingAttachmentName, uploadingAttachmentName,
+23
View File
@@ -51,6 +51,29 @@ export async function invalidateVaultCoreSyncSnapshot(cacheKey: string): Promise
await clearCachedVaultCoreSnapshot(normalizedKey); await clearCachedVaultCoreSnapshot(normalizedKey);
} }
export async function saveVaultCoreSyncSnapshot(
cacheKey: string,
snapshot: VaultCoreSnapshot,
revisionStamp?: number | null
): Promise<void> {
const normalizedKey = String(cacheKey || '').trim();
if (!normalizedKey) return;
const normalizedSnapshot = normalizeCachedSnapshot(snapshot);
const currentMemory = memoryVaultCoreCache.get(normalizedKey);
let nextRevisionStamp = Number(revisionStamp);
if (!Number.isFinite(nextRevisionStamp) || nextRevisionStamp <= 0) {
const cached = await loadCachedVaultCoreSnapshot(normalizedKey);
nextRevisionStamp = currentMemory?.revisionStamp || cached?.revisionStamp || Date.now();
}
memoryVaultCoreCache.set(normalizedKey, {
revisionStamp: nextRevisionStamp,
snapshot: normalizedSnapshot,
});
await saveCachedVaultCoreSnapshot(normalizedKey, nextRevisionStamp, normalizedSnapshot);
}
export async function loadVaultCoreSyncSnapshot(authedFetch: AuthedFetch, cacheKey: string): Promise<VaultCoreSnapshot> { export async function loadVaultCoreSyncSnapshot(authedFetch: AuthedFetch, cacheKey: string): Promise<VaultCoreSnapshot> {
const normalizedKey = String(cacheKey || '').trim(); const normalizedKey = String(cacheKey || '').trim();
if (!normalizedKey) return { ciphers: [], folders: [], sends: [] }; if (!normalizedKey) return { ciphers: [], folders: [], sends: [] };
+7 -4
View File
@@ -43,7 +43,7 @@ export async function createFolder(
authedFetch: AuthedFetch, authedFetch: AuthedFetch,
session: SessionState, session: SessionState,
name: string name: string
): Promise<{ id: string; name?: string | null }> { ): Promise<Folder> {
if (!session.symEncKey || !session.symMacKey) throw new Error('Vault key unavailable'); if (!session.symEncKey || !session.symMacKey) throw new Error('Vault key unavailable');
const enc = base64ToBytes(session.symEncKey); const enc = base64ToBytes(session.symEncKey);
const mac = base64ToBytes(session.symMacKey); const mac = base64ToBytes(session.symMacKey);
@@ -54,9 +54,9 @@ export async function createFolder(
body: JSON.stringify({ name: encryptedName }), body: JSON.stringify({ name: encryptedName }),
}); });
if (!resp.ok) throw new Error('Create folder failed'); if (!resp.ok) throw new Error('Create folder failed');
const body = await parseJson<{ id?: string; name?: string | null }>(resp); const body = await parseJson<Folder>(resp);
if (!body?.id) throw new Error('Create folder failed'); if (!body?.id) throw new Error('Create folder failed');
return { id: body.id, name: body.name ?? null }; return body;
} }
export async function encryptFolderImportName(session: SessionState, name: string): Promise<string> { export async function encryptFolderImportName(session: SessionState, name: string): Promise<string> {
@@ -92,7 +92,7 @@ export async function updateFolder(
session: SessionState, session: SessionState,
folderId: string, folderId: string,
name: string name: string
): Promise<void> { ): Promise<Folder> {
const id = String(folderId || '').trim(); const id = String(folderId || '').trim();
if (!id) throw new Error('Folder id is required'); if (!id) throw new Error('Folder id is required');
if (!session.symEncKey || !session.symMacKey) throw new Error('Vault key unavailable'); if (!session.symEncKey || !session.symMacKey) throw new Error('Vault key unavailable');
@@ -105,6 +105,9 @@ export async function updateFolder(
body: JSON.stringify({ name: encryptedName }), body: JSON.stringify({ name: encryptedName }),
}); });
if (!resp.ok) throw new Error('Update folder failed'); if (!resp.ok) throw new Error('Update folder failed');
const body = await parseJson<Folder>(resp);
if (!body?.id) throw new Error('Update folder failed');
return body;
} }
export async function getCiphers(authedFetch: AuthedFetch, cacheKey: string): Promise<Cipher[]> { export async function getCiphers(authedFetch: AuthedFetch, cacheKey: string): Promise<Cipher[]> {