mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-11-04 10:40:20 +00:00
fix: preserve vault fields and align cipher compatibility
Preserve linked and unknown encrypted fields in Web edits, reject stale and concurrent saves, and support official bulk delete and restore methods while retaining legacy routes. Add 20 offline compatibility regressions and update undici to 8.10.2. Verified 51 tests, TypeScript checks, locale validation, Web build and Worker deployment dry run. The unpatched braces advisory remains visible.
This commit is contained in:
1 parent
5f71cb388c
commit
293f5ce196
26 files changed
+456
-25
No files matched your search
+17
-12
@@ -174,7 +174,7 @@ function isStaleCipherUpdate(existingUpdatedAt: string, clientRevisionDate: stri
|
||||
const existingTs = Date.parse(existingUpdatedAt);
|
||||
const clientTs = Date.parse(clientRevisionDate);
|
||||
if (Number.isNaN(existingTs) || Number.isNaN(clientTs)) return false;
|
||||
return existingTs - clientTs > 1000;
|
||||
return existingTs > clientTs;
|
||||
}
|
||||
|
||||
function syncCipherComputedAliases(cipher: Cipher): Cipher {
|
||||
@@ -522,6 +522,7 @@ function normalizeCipherSecureNoteForCompatibility(secureNote: any): CipherSecur
|
||||
if (!secureNote || typeof secureNote !== 'object') return null;
|
||||
const type = Number(secureNote?.type ?? secureNote?.Type ?? 0);
|
||||
return {
|
||||
...secureNote,
|
||||
type: Number.isFinite(type) ? type : 0,
|
||||
};
|
||||
}
|
||||
@@ -655,10 +656,6 @@ function readIncomingAttachmentMetadata(source: any): IncomingAttachmentMetadata
|
||||
return [...merged.values()];
|
||||
}
|
||||
|
||||
function hasIncomingAttachmentMetadata(source: any): boolean {
|
||||
return readIncomingAttachmentMetadata(source).length > 0;
|
||||
}
|
||||
|
||||
async function syncIncomingAttachmentMetadata(
|
||||
storage: StorageService,
|
||||
cipherId: string,
|
||||
@@ -1048,7 +1045,6 @@ export async function handleUpdateCipher(request: Request, env: Env, userId: str
|
||||
const incomingPassport = readCipherProp<CipherPassport | null>(cipherData, ['passport', 'Passport']);
|
||||
const incomingPasswordHistory = readCipherProp<PasswordHistory[] | null>(cipherData, ['passwordHistory', 'PasswordHistory']);
|
||||
const incomingRevisionDate = readCipherRevisionDate(cipherData);
|
||||
const hasAttachmentMigrationMetadata = hasIncomingAttachmentMetadata(cipherData);
|
||||
const preserveRevisionDate =
|
||||
shouldPreserveRepairableCipherUris(request)
|
||||
&& (body.preserveRevisionDate === true || cipherData.preserveRevisionDate === true);
|
||||
@@ -1057,7 +1053,7 @@ export async function handleUpdateCipher(request: Request, env: Env, userId: str
|
||||
return errorResponse('Cipher key encryption is not supported by this server. Resync the client and try again.', 400);
|
||||
}
|
||||
|
||||
if (!hasAttachmentMigrationMetadata && isStaleCipherUpdate(existingCipher.updatedAt, incomingRevisionDate)) {
|
||||
if (isStaleCipherUpdate(existingCipher.updatedAt, incomingRevisionDate)) {
|
||||
return errorResponse('The client copy of this cipher is out of date. Resync the client and try again.', 400);
|
||||
}
|
||||
|
||||
@@ -1065,7 +1061,13 @@ export async function handleUpdateCipher(request: Request, env: Env, userId: str
|
||||
|
||||
// Opaque passthrough: merge existing stored data with ALL incoming client fields.
|
||||
// Unknown/future fields from the client are preserved; server-controlled fields are protected.
|
||||
const { preserveRevisionDate: _preserveRevisionDate, PreserveRevisionDate: _pascalPreserveRevisionDate, ...cipherDataWithoutFlags } = cipherData;
|
||||
const {
|
||||
preserveRevisionDate: _preserveRevisionDate,
|
||||
PreserveRevisionDate: _pascalPreserveRevisionDate,
|
||||
lastKnownRevisionDate: _lastKnownRevisionDate,
|
||||
LastKnownRevisionDate: _pascalLastKnownRevisionDate,
|
||||
...cipherDataWithoutFlags
|
||||
} = cipherData;
|
||||
const cipher: Cipher = {
|
||||
...existingCipher, // start with all existing stored data (including unknowns)
|
||||
...cipherDataWithoutFlags, // overlay all client data (including new/unknown fields)
|
||||
@@ -1076,7 +1078,7 @@ export async function handleUpdateCipher(request: Request, env: Env, userId: str
|
||||
favorite: cipherData.favorite ?? existingCipher.favorite,
|
||||
reprompt: cipherData.reprompt ?? existingCipher.reprompt,
|
||||
createdAt: existingCipher.createdAt,
|
||||
updatedAt: preserveRevisionDate ? existingCipher.updatedAt : new Date().toISOString(),
|
||||
updatedAt: preserveRevisionDate ? existingCipher.updatedAt : new Date(Math.max(Date.now(), Date.parse(existingCipher.updatedAt) + 1)).toISOString(),
|
||||
archivedAt: readCipherArchivedAt(cipherData, existingCipher.archivedAt ?? null),
|
||||
deletedAt: existingCipher.deletedAt,
|
||||
};
|
||||
@@ -1115,8 +1117,11 @@ export async function handleUpdateCipher(request: Request, env: Env, userId: str
|
||||
if (!folderOk) return errorResponse('Folder not found', 404);
|
||||
}
|
||||
|
||||
if (!(await storage.updateCipherIfUnchanged(cipher, existingCipher.updatedAt))) {
|
||||
return errorResponse('The client copy of this cipher is out of date. Resync the client and try again.', 400);
|
||||
}
|
||||
// Rejected updates must not modify attachment metadata either.
|
||||
await syncIncomingAttachmentMetadata(storage, cipher.id, cipherData);
|
||||
await storage.saveCipher(cipher);
|
||||
const revisionDate = await storage.updateRevisionDate(userId);
|
||||
notifyVaultSyncForRequest(request, env, userId, revisionDate);
|
||||
notifyCipherUpdateForRequest(request, env, cipher, revisionDate);
|
||||
@@ -1451,7 +1456,7 @@ export async function handleBulkDeleteCiphers(request: Request, env: Env, userId
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
|
||||
// POST /api/ciphers/restore - Bulk restore
|
||||
// PUT /api/ciphers/restore (POST retained for older NodeWarden clients)
|
||||
export async function handleBulkRestoreCiphers(request: Request, env: Env, userId: string): Promise<Response> {
|
||||
const storage = new StorageService(env.DB);
|
||||
|
||||
@@ -1472,7 +1477,7 @@ export async function handleBulkRestoreCiphers(request: Request, env: Env, userI
|
||||
notifyUserCiphersSync(env, userId, revisionDate, readActingDeviceIdentifier(request));
|
||||
}
|
||||
|
||||
return new Response(null, { status: 204 });
|
||||
return buildCipherListResponse(request, storage, userId, body.ids);
|
||||
}
|
||||
|
||||
// POST /api/ciphers/delete-permanent - Bulk permanent delete
|
||||
|
||||
Reference in new issue
Block a user