@@ -229,6 +229,12 @@ def _serialize_text_objects(value: list[BaseModel]) -> str:
229229 return to_json (payload ).decode ()
230230
231231
232+ def _serialize_signature_configuration (
233+ value : _PdfSignatureConfigurationModel ,
234+ ) -> str :
235+ return value .model_dump_json (exclude_none = True )
236+
237+
232238def _allowed_mime_types (
233239 allowed_mime_types : str , * more_allowed_mime_types : str , error_msg : str | None
234240) -> Callable [[Any ], Any ]:
@@ -242,7 +248,10 @@ def allowed_mime_types_validator(
242248 _ = allowed_mime_types_validator (item )
243249 return value
244250 if value .type not in combined_allowed_mime_types :
245- msg = error_msg or f"The file type must be one of: { allowed_mime_types } "
251+ msg = (
252+ error_msg
253+ or f"The file type must be one of: { combined_allowed_mime_types } "
254+ )
246255 raise ValueError (msg )
247256 return value
248257
@@ -978,6 +987,44 @@ class PdfPresetRedactionModel(BaseModel):
978987 value : PdfRedactionPreset
979988
980989
990+ class _PdfSignaturePointModel (BaseModel ):
991+ x : float
992+ y : float
993+
994+
995+ class _PdfSignatureLocationModel (BaseModel ):
996+ bottom_left : _PdfSignaturePointModel
997+ top_right : _PdfSignaturePointModel
998+ page : str | int
999+
1000+
1001+ class _PdfSignatureDisplayModel (BaseModel ):
1002+ include_distinguished_name : bool | None = None
1003+ include_datetime : bool | None = None
1004+ contact : str | None = None
1005+ location : str | None = None
1006+ name : str | None = None
1007+ reason : str | None = None
1008+
1009+
1010+ class _PdfSignatureConfigurationModel (BaseModel ):
1011+ type : Literal ["new" , "existing" ]
1012+ name : str | None = None
1013+ logo_opacity : Annotated [float | None , Field (gt = 0 , le = 1 , default = None )] = None
1014+ location : _PdfSignatureLocationModel | None = None
1015+ display : _PdfSignatureDisplayModel | None = None
1016+
1017+ @model_validator (mode = "after" )
1018+ def _validate_location_requirements (self ) -> _PdfSignatureConfigurationModel :
1019+ if self .type == "new" and self .location is None :
1020+ msg = (
1021+ "Missing location information for a new digital signature field. "
1022+ "See documentation for required fields."
1023+ )
1024+ raise ValueError (msg )
1025+ return self
1026+
1027+
9811028_PdfRedactionVariant = Annotated [
9821029 PdfLiteralRedactionModel | PdfRegexRedactionModel | PdfPresetRedactionModel ,
9831030 Field (discriminator = "type" ),
@@ -1435,6 +1482,190 @@ class PdfExportFormDataPayload(BaseModel):
14351482 ] = None
14361483
14371484
1485+ class PdfSignPayload (BaseModel ):
1486+ """Adapt caller options into a pdfRest-ready sign request payload."""
1487+
1488+ files : Annotated [
1489+ list [PdfRestFile ],
1490+ Field (
1491+ min_length = 1 ,
1492+ max_length = 1 ,
1493+ validation_alias = AliasChoices ("file" , "files" ),
1494+ serialization_alias = "id" ,
1495+ ),
1496+ BeforeValidator (_ensure_list ),
1497+ AfterValidator (
1498+ _allowed_mime_types ("application/pdf" , error_msg = "Must be a PDF file" )
1499+ ),
1500+ PlainSerializer (_serialize_as_first_file_id ),
1501+ ]
1502+ signature_configuration : Annotated [
1503+ _PdfSignatureConfigurationModel ,
1504+ Field (serialization_alias = "signature_configuration" ),
1505+ PlainSerializer (_serialize_signature_configuration ),
1506+ ]
1507+ pfx_credential : Annotated [
1508+ list [PdfRestFile ] | None ,
1509+ Field (
1510+ default = None ,
1511+ min_length = 1 ,
1512+ max_length = 1 ,
1513+ validation_alias = AliasChoices ("pfx" , "pfx_credential" ),
1514+ serialization_alias = "pfx_credential_id" ,
1515+ ),
1516+ BeforeValidator (_ensure_list ),
1517+ AfterValidator (
1518+ _allowed_mime_types (
1519+ "application/x-pkcs12" ,
1520+ "application/pkcs12" ,
1521+ "application/octet-stream" ,
1522+ error_msg = "PFX credentials must be a .pfx or .p12 file" ,
1523+ )
1524+ ),
1525+ PlainSerializer (_serialize_as_first_file_id ),
1526+ ] = None
1527+ pfx_passphrase : Annotated [
1528+ list [PdfRestFile ] | None ,
1529+ Field (
1530+ default = None ,
1531+ min_length = 1 ,
1532+ max_length = 1 ,
1533+ validation_alias = AliasChoices ("pfx_passphrase" , "passphrase" ),
1534+ serialization_alias = "pfx_passphrase_id" ,
1535+ ),
1536+ BeforeValidator (_ensure_list ),
1537+ AfterValidator (
1538+ _allowed_mime_types (
1539+ "text/plain" ,
1540+ "application/octet-stream" ,
1541+ error_msg = "PFX passphrase must be a text file" ,
1542+ )
1543+ ),
1544+ PlainSerializer (_serialize_as_first_file_id ),
1545+ ] = None
1546+ certificate : Annotated [
1547+ list [PdfRestFile ] | None ,
1548+ Field (
1549+ default = None ,
1550+ min_length = 1 ,
1551+ max_length = 1 ,
1552+ validation_alias = AliasChoices ("certificate" , "cert" ),
1553+ serialization_alias = "certificate_id" ,
1554+ ),
1555+ BeforeValidator (_ensure_list ),
1556+ AfterValidator (
1557+ # DER cert/key uploads are frequently tagged as x509-ca-cert (or octet-stream
1558+ # in some environments), so we intentionally keep this allowlist broad.
1559+ _allowed_mime_types (
1560+ "application/pkix-cert" ,
1561+ "application/x-x509-ca-cert" ,
1562+ "application/x-pem-file" ,
1563+ "application/pem-certificate-chain" ,
1564+ "application/octet-stream" ,
1565+ error_msg = "Certificate must be a .pem or .der file" ,
1566+ )
1567+ ),
1568+ PlainSerializer (_serialize_as_first_file_id ),
1569+ ] = None
1570+ private_key : Annotated [
1571+ list [PdfRestFile ] | None ,
1572+ Field (
1573+ default = None ,
1574+ min_length = 1 ,
1575+ max_length = 1 ,
1576+ validation_alias = AliasChoices ("private_key" , "key" ),
1577+ serialization_alias = "private_key_id" ,
1578+ ),
1579+ BeforeValidator (_ensure_list ),
1580+ AfterValidator (
1581+ # Keep parity with provider/browser MIME detection for DER private keys.
1582+ _allowed_mime_types (
1583+ "application/pkix-cert" ,
1584+ "application/x-x509-ca-cert" ,
1585+ "application/pkcs8" ,
1586+ "application/x-pem-file" ,
1587+ "application/pem-certificate-chain" ,
1588+ "application/octet-stream" ,
1589+ error_msg = "Private key must be a .pem or .der file" ,
1590+ )
1591+ ),
1592+ PlainSerializer (_serialize_as_first_file_id ),
1593+ ] = None
1594+ logo : Annotated [
1595+ list [PdfRestFile ] | None ,
1596+ Field (
1597+ default = None ,
1598+ min_length = 1 ,
1599+ max_length = 1 ,
1600+ validation_alias = AliasChoices ("logo" , "logos" ),
1601+ serialization_alias = "logo_id" ,
1602+ ),
1603+ BeforeValidator (_ensure_list ),
1604+ AfterValidator (
1605+ _allowed_mime_types (
1606+ "image/jpeg" ,
1607+ "image/png" ,
1608+ "image/tiff" ,
1609+ "image/bmp" ,
1610+ error_msg = "Logo must be an image file" ,
1611+ )
1612+ ),
1613+ PlainSerializer (_serialize_as_first_file_id ),
1614+ ] = None
1615+ output : Annotated [
1616+ str | None ,
1617+ Field (serialization_alias = "output" , min_length = 1 , default = None ),
1618+ AfterValidator (_validate_output_prefix ),
1619+ ] = None
1620+
1621+ @model_validator (mode = "before" )
1622+ @classmethod
1623+ def _normalize_credentials (cls , data : Any ) -> Any :
1624+ if not isinstance (data , Mapping ):
1625+ return data
1626+
1627+ payload = cast (Mapping [object , Any ], data )
1628+ credentials = payload .get ("credentials" )
1629+ if credentials is None :
1630+ return {str (key ): value for key , value in payload .items ()}
1631+ if not isinstance (credentials , Mapping ):
1632+ msg = (
1633+ "credentials must be a mapping with either pfx/passphrase or "
1634+ "certificate/private_key."
1635+ )
1636+ raise ValueError (msg ) # noqa: TRY004
1637+
1638+ normalized : dict [str , Any ] = {str (key ): value for key , value in payload .items ()}
1639+ credential_map = cast (Mapping [object , Any ], credentials )
1640+ for raw_key , value in credential_map .items ():
1641+ key = str (raw_key )
1642+ if key not in normalized :
1643+ normalized [key ] = value
1644+ return normalized
1645+
1646+ @model_validator (mode = "after" )
1647+ def _validate_credentials (self ) -> PdfSignPayload :
1648+ has_pfx = self .pfx_credential is not None or self .pfx_passphrase is not None
1649+ has_pem = self .certificate is not None or self .private_key is not None
1650+
1651+ if has_pfx and has_pem :
1652+ msg = "Provide either PFX credentials (pfx + passphrase) or certificate/private_key, not both."
1653+ raise ValueError (msg )
1654+ if has_pfx :
1655+ if not self .pfx_credential or not self .pfx_passphrase :
1656+ msg = "Both pfx and passphrase are required when supplying PFX credentials."
1657+ raise ValueError (msg )
1658+ elif has_pem :
1659+ if not self .certificate or not self .private_key :
1660+ msg = "Both certificate and private_key are required when supplying PEM/DER credentials."
1661+ raise ValueError (msg )
1662+ else :
1663+ msg = "Either PFX credentials (pfx + passphrase) or certificate/private_key credentials are required."
1664+ raise ValueError (msg )
1665+
1666+ return self
1667+
1668+
14381669class PdfCompressPayload (BaseModel ):
14391670 """Adapt caller options into a pdfRest-ready compress request payload."""
14401671
0 commit comments