@@ -27,6 +27,7 @@ import { deleteAllAttachmentsForCipher, deleteAllAttachmentsForCiphers } from '.
2727import { parsePagination , encodeContinuationToken } from '../utils/pagination' ;
2828import { readActingDeviceIdentifier } from '../utils/device' ;
2929import { auditRequestMetadata , writeAuditEvent } from '../services/audit-events' ;
30+ import { readNullableFullUpdateField } from './cipher-full-update' ;
3031
3132// CONTRACT:
3233// Cipher JSON is the highest-risk Bitwarden compatibility surface. Preserve
@@ -1100,16 +1101,10 @@ export async function handleUpdateCipher(request: Request, env: Env, userId: str
11001101 cipher . passwordHistory = incomingPasswordHistory . value ?? null ;
11011102 }
11021103
1103- // Custom fields deletion compatibility:
1104- // - Accept both camelCase "fields" and PascalCase "Fields".
1105- // - For full update (PUT/POST on this endpoint), missing fields means cleared fields.
1106- // This prevents stale custom fields from being resurrected by merge fallback.
1107- const incomingFields = getAliasedProp ( cipherData , [ 'fields' , 'Fields' ] ) ;
1108- if ( incomingFields . present ) {
1109- cipher . fields = incomingFields . value ?? null ;
1110- } else if ( request . method === 'PUT' || request . method === 'POST' ) {
1111- cipher . fields = null ;
1112- }
1104+ // Nullable fields use replacement semantics on this full-update endpoint.
1105+ // Some clients omit cleared values, so merge fallback must not resurrect them.
1106+ cipher . notes = readNullableFullUpdateField < string > ( cipherData , [ 'notes' , 'Notes' ] ) ;
1107+ cipher . fields = readNullableFullUpdateField < Cipher [ 'fields' ] > ( cipherData , [ 'fields' , 'Fields' ] ) ;
11131108 normalizeCipherForStorage ( cipher ) ;
11141109 const compatibilityError = validateCipherEncryptedFieldsForCompatibility ( cipher ) ;
11151110 if ( compatibilityError ) return errorResponse ( compatibilityError , 400 ) ;
0 commit comments