diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ProfileController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ProfileController.java index 4889cb057..15d07000b 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ProfileController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ProfileController.java @@ -11,6 +11,10 @@ import com.google.common.base.Preconditions; import io.dropwizard.auth.Auth; import io.micrometer.core.instrument.Metrics; import io.micrometer.core.instrument.Tags; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; @@ -72,7 +76,6 @@ import org.whispersystems.textsecuregcm.entities.BaseProfileResponse; import org.whispersystems.textsecuregcm.entities.BatchIdentityCheckRequest; import org.whispersystems.textsecuregcm.entities.BatchIdentityCheckResponse; import org.whispersystems.textsecuregcm.entities.CreateProfileRequest; -import org.whispersystems.textsecuregcm.entities.CredentialProfileResponse; import org.whispersystems.textsecuregcm.entities.ExpiringProfileKeyCredentialProfileResponse; import org.whispersystems.textsecuregcm.entities.ProfileAvatarUploadAttributes; import org.whispersystems.textsecuregcm.entities.VersionedProfileResponse; @@ -151,6 +154,18 @@ public class ProfileController { @PUT @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) + @Operation( + summary = "Update profile", + description = "Updates an account’s profile. Must be authenticated.") + @ApiResponse(responseCode = "200", description = "The profile was updated successfully.", + content = @Content(schema = @Schema( + implementation = ProfileAvatarUploadAttributes.class, + description = "If the request changed the avatar, the response body contains an upload form."))) + @ApiResponse(responseCode = "400", description = "Invalid create profile request.") + @ApiResponse(responseCode = "401", description = "Account authentication check failed.") + @ApiResponse(responseCode = "403", description = "The request contained a payment address, but payments are not supported in the region of the account’s phone number.") + @ApiResponse(responseCode = "412", description = "The requesting account has the profiles_v2 capability") + @ApiResponse(responseCode = "422", description = "Invalid request format") public Response setProfile(@Auth AuthenticatedDevice auth, @NotNull @Valid CreateProfileRequest request) { final Account account = accountsManager.getByAccountIdentifier(auth.accountIdentifier()) @@ -214,8 +229,16 @@ public class ProfileController { } @GET - @Produces(MediaType.APPLICATION_JSON) @Path("/{identifier}/{version}") + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Get versioned profile", + description = "Retrieves a specific version of an account's profile. Requires either authentication or an unidentified access key.") + @ApiResponse(responseCode = "200", description = "Profile retrieved successfully.", useReturnTypeSchema = true) + @ApiResponse(responseCode = "400", description = "Malformed identifier") + @ApiResponse(responseCode = "401", description = "Not authorized to access this profile.") + @ApiResponse(responseCode = "404", description = "Profile or account not found.") + @ApiResponse(responseCode = "429", description = "Rate limit exceeded.") @ManagedAsync public VersionedProfileResponse getProfile( @Auth Optional maybeAuthenticatedDevice, @@ -241,9 +264,21 @@ public class ProfileController { } @GET - @Produces(MediaType.APPLICATION_JSON) @Path("/{identifier}/{version}/{credentialRequest}") - public CredentialProfileResponse getProfile( + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Get profile with credential", + description = "Retrieves a specific version of an account's profile along with an expiring profile key credential. Requires either authentication or an unidentified access key." + ) + @ApiResponse( + responseCode = "200", + description = "Account found. Profile information will be limited and credential will be null if the version was not found", + useReturnTypeSchema = true) + @ApiResponse(responseCode = "400", description = "Invalid credential type or credential request.") + @ApiResponse(responseCode = "401", description = "Not authorized to access this profile.") + @ApiResponse(responseCode = "404", description = "Profile or account not found.") + @ApiResponse(responseCode = "429", description = "Rate limit exceeded.") + public ExpiringProfileKeyCredentialProfileResponse getProfile( @Auth Optional maybeAuthenticatedDevice, @HeaderParam(HeaderUtils.UNIDENTIFIED_ACCESS_KEY) Optional accessKey, @Context ContainerRequestContext containerRequestContext, @@ -276,8 +311,16 @@ public class ProfileController { // Although clients should generally be using versioned profiles wherever possible, there are still a few lingering // use cases for getting profiles without a version (e.g. getting a contact's unidentified access key checksum). @GET - @Produces(MediaType.APPLICATION_JSON) @Path("/{identifier}") + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Get unversioned profile", + description = "Retrieves basic profile information without a specific version. Supports ACI and PNI identifiers. Requires authentication, an unidentified access key, or a group send token.") + @ApiResponse(responseCode = "200", description = "Unversioned profile retrieved successfully.", useReturnTypeSchema = true) + @ApiResponse(responseCode = "400", description = "Invalid request (e.g., multiple authorization types provided).") + @ApiResponse(responseCode = "401", description = "Not authorized to access this profile.") + @ApiResponse(responseCode = "404", description = "Account not found.") + @ApiResponse(responseCode = "429", description = "Rate limit exceeded.") @ManagedAsync public BaseProfileResponse getUnversionedProfile( @Auth Optional maybeAuthenticatedDevice, @@ -318,9 +361,17 @@ public class ProfileController { } @POST + @Path("/identity_check/batch") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @Path("/identity_check/batch") + @Operation( + summary = "Batch identity key check", + description = "Checks identity key fingerprints for multiple accounts. Returns accounts where the fingerprint does not match. Should not be authenticated.") + @ApiResponse( + responseCode = "200", + description = "Batch check completed successfully. Response may contain accounts with mismatched fingerprints.", + content = @Content(schema = @Schema(implementation = BatchIdentityCheckResponse.class))) + @ApiResponse(responseCode = "400", description = "Invalid request format or validation failed.") public CompletableFuture runBatchIdentityCheck(@NotNull @Valid BatchIdentityCheckRequest request) { return CompletableFuture.supplyAsync(() -> { List responseElements = Collections.synchronizedList(new ArrayList<>()); @@ -410,7 +461,7 @@ public class ProfileController { final Optional maybeProfile = profilesManager.get(account.getUuid(), version); if (maybeProfile.isEmpty()) { - // Hypothesis: this should basically never happen since clients can't delete versions + // this can happen if an account re-registers, which includes some device-transfer scenarios Metrics.counter( VERSION_NOT_FOUND_COUNTER_NAME, Tags.of( diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/BaseProfileResponse.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/BaseProfileResponse.java index b1eff26c5..ab770de40 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/entities/BaseProfileResponse.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/BaseProfileResponse.java @@ -8,6 +8,7 @@ package org.whispersystems.textsecuregcm.entities; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import io.swagger.v3.oas.annotations.media.Schema; import org.signal.libsignal.protocol.IdentityKey; import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; import org.whispersystems.textsecuregcm.util.ByteArrayBase64WithPaddingAdapter; @@ -17,27 +18,34 @@ import org.whispersystems.textsecuregcm.util.IdentityKeyAdapter; import java.util.List; import java.util.Map; +@Schema(description = "Unversioned profile containing basic information") public class BaseProfileResponse { + @Schema(description = "The account's public identity key (unpadded base64)") @JsonProperty @JsonSerialize(using = IdentityKeyAdapter.Serializer.class) @JsonDeserialize(using = IdentityKeyAdapter.Deserializer.class) private IdentityKey identityKey; + @Schema(description = "Checksum for unidentified access authentication (padded base64)") @JsonProperty @JsonSerialize(using = ByteArrayBase64WithPaddingAdapter.Serializing.class) @JsonDeserialize(using = ByteArrayBase64WithPaddingAdapter.Deserializing.class) private byte[] unidentifiedAccess; + @Schema(description = "Whether unidentified access is unrestricted for this account") @JsonProperty private boolean unrestrictedUnidentifiedAccess; + @Schema(description = "Device capabilities and whether they are enabled") @JsonProperty private Map capabilities; + @Schema(description = "List of badges displayed on the profile") @JsonProperty private List badges; + @Schema(description = "Service identifier (ACI or PNI)") @JsonProperty @JsonSerialize(using = ServiceIdentifierAdapter.ServiceIdentifierSerializer.class) @JsonDeserialize(using = ServiceIdentifierAdapter.ServiceIdentifierDeserializer.class) diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/BatchIdentityCheckRequest.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/BatchIdentityCheckRequest.java index ff8b7565b..7c659fd99 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/entities/BatchIdentityCheckRequest.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/BatchIdentityCheckRequest.java @@ -7,29 +7,36 @@ package org.whispersystems.textsecuregcm.entities; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import java.util.List; -import javax.annotation.Nullable; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; +import java.util.List; import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; import org.whispersystems.textsecuregcm.util.ExactlySize; import org.whispersystems.textsecuregcm.util.ServiceIdentifierAdapter; -public record BatchIdentityCheckRequest(@Valid @NotNull @Size(max = 1000) List elements) { +@Schema(description = "Request to check identity key fingerprints for multiple accounts") +public record BatchIdentityCheckRequest( + @Schema(description = "List of accounts to check") + @Valid @NotNull @Size(max = 1000) List elements) { /** * @param uuid account id or phone number id * @param fingerprint most significant 4 bytes of SHA-256 of the 33-byte identity key field (32-byte curve25519 public * key prefixed with 0x05) */ - public record Element(@NotNull - @JsonSerialize(using = ServiceIdentifierAdapter.ServiceIdentifierSerializer.class) - @JsonDeserialize(using = ServiceIdentifierAdapter.ServiceIdentifierDeserializer.class) - ServiceIdentifier uuid, + @Schema(description = "A service identifier and expected identity key fingerprint") + public record Element( + @Schema(description = "Identifier (ACI or PNI)") + @NotNull + @JsonSerialize(using = ServiceIdentifierAdapter.ServiceIdentifierSerializer.class) + @JsonDeserialize(using = ServiceIdentifierAdapter.ServiceIdentifierDeserializer.class) + ServiceIdentifier uuid, - @NotNull - @ExactlySize(4) - byte[] fingerprint) { - } + @Schema(description = "Expected identity key fingerprint (4 bytes, most significant bytes of SHA-256 hash of 33-byte identity key field)") + @NotNull + @ExactlySize(4) + byte[] fingerprint) { + } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/BatchIdentityCheckResponse.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/BatchIdentityCheckResponse.java index 7dd040ef5..70ea959d6 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/entities/BatchIdentityCheckResponse.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/BatchIdentityCheckResponse.java @@ -8,6 +8,7 @@ package org.whispersystems.textsecuregcm.entities; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import io.swagger.v3.oas.annotations.media.Schema; import java.util.List; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; @@ -16,17 +17,24 @@ import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; import org.whispersystems.textsecuregcm.util.IdentityKeyAdapter; import org.whispersystems.textsecuregcm.util.ServiceIdentifierAdapter; -public record BatchIdentityCheckResponse(@Valid List elements) { +@Schema(description = "Response containing accounts with mismatched identity key fingerprints") +public record BatchIdentityCheckResponse( + @Schema(description = "List of accounts where fingerprint did not match (empty if all matched)") + @Valid List elements) { - public record Element(@JsonInclude(JsonInclude.Include.NON_EMPTY) - @JsonSerialize(using = ServiceIdentifierAdapter.ServiceIdentifierSerializer.class) - @JsonDeserialize(using = ServiceIdentifierAdapter.ServiceIdentifierDeserializer.class) - @NotNull - ServiceIdentifier uuid, + @Schema(description = "An account with a mismatched identity key fingerprint") + public record Element( + @Schema(description = "Service identifier (ACI or PNI)") + @JsonInclude(JsonInclude.Include.NON_EMPTY) + @JsonSerialize(using = ServiceIdentifierAdapter.ServiceIdentifierSerializer.class) + @JsonDeserialize(using = ServiceIdentifierAdapter.ServiceIdentifierDeserializer.class) + @NotNull + ServiceIdentifier uuid, - @NotNull - @JsonSerialize(using = IdentityKeyAdapter.Serializer.class) - @JsonDeserialize(using = IdentityKeyAdapter.Deserializer.class) - IdentityKey identityKey) { + @Schema(description = "The actual identity key for this account") + @NotNull + @JsonSerialize(using = IdentityKeyAdapter.Serializer.class) + @JsonDeserialize(using = IdentityKeyAdapter.Deserializer.class) + IdentityKey identityKey) { } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/CreateProfileRequest.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/CreateProfileRequest.java index 4e53db690..fb0a36b24 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/entities/CreateProfileRequest.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/CreateProfileRequest.java @@ -8,6 +8,7 @@ package org.whispersystems.textsecuregcm.entities; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import io.swagger.v3.oas.annotations.media.Schema; import java.util.List; import java.util.Optional; import jakarta.validation.constraints.NotEmpty; @@ -17,52 +18,63 @@ import org.whispersystems.textsecuregcm.util.ByteArrayBase64WithPaddingAdapter; import org.whispersystems.textsecuregcm.util.ExactlySize; import org.whispersystems.textsecuregcm.util.ValidHexString; +@Schema(description = "Request to create or update a versioned profile") public record CreateProfileRequest( + @Schema(description = "Profile key commitment") @JsonProperty @NotNull @JsonDeserialize(using = ProfileKeyCommitmentAdapter.Deserializing.class) @JsonSerialize(using = ProfileKeyCommitmentAdapter.Serializing.class) ProfileKeyCommitment commitment, + @Schema(description = "Profile version identifier (hex-encoding of the public key)") @JsonProperty @NotEmpty @ValidHexString @ExactlySize({64}) String version, + @Schema(description = "Encrypted profile name. Padded base64.") @JsonProperty @JsonSerialize(using = ByteArrayBase64WithPaddingAdapter.Serializing.class) @JsonDeserialize(using = ByteArrayBase64WithPaddingAdapter.Deserializing.class) @ExactlySize({81, 285}) byte[] name, + @Schema(description = "Encrypted about emoji. Padded base64.") @JsonProperty @JsonSerialize(using = ByteArrayBase64WithPaddingAdapter.Serializing.class) @JsonDeserialize(using = ByteArrayBase64WithPaddingAdapter.Deserializing.class) @ExactlySize({0, 60}) byte[] aboutEmoji, + @Schema(description = "Encrypted about text. Padded base64.") @JsonProperty @JsonSerialize(using = ByteArrayBase64WithPaddingAdapter.Serializing.class) @JsonDeserialize(using = ByteArrayBase64WithPaddingAdapter.Deserializing.class) @ExactlySize({0, 156, 282, 540}) byte[] about, + @Schema(description = "Encrypted payment address. Padded base64.") @JsonProperty @JsonSerialize(using = ByteArrayBase64WithPaddingAdapter.Serializing.class) @JsonDeserialize(using = ByteArrayBase64WithPaddingAdapter.Deserializing.class) @ExactlySize({0, 582}) byte[] paymentAddress, + @Schema(description = "Whether the profile has an avatar") @JsonProperty("avatar") boolean hasAvatar, + @Schema(description = "Whether the avatar is unchanged from the previous version") @JsonProperty boolean sameAvatar, + @Schema(description = "List of badge IDs to display on the profile") @JsonProperty("badgeIds") Optional> badges, + @Schema(description = "Encrypted phone number sharing preference. Padded base64.") @JsonProperty @JsonSerialize(using = ByteArrayBase64WithPaddingAdapter.Serializing.class) @JsonDeserialize(using = ByteArrayBase64WithPaddingAdapter.Deserializing.class) diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/CredentialProfileResponse.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/CredentialProfileResponse.java deleted file mode 100644 index 01329d466..000000000 --- a/service/src/main/java/org/whispersystems/textsecuregcm/entities/CredentialProfileResponse.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2013-2021 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.whispersystems.textsecuregcm.entities; - -import com.fasterxml.jackson.annotation.JsonUnwrapped; - -public abstract class CredentialProfileResponse { - - @JsonUnwrapped - private VersionedProfileResponse versionedProfileResponse; - - protected CredentialProfileResponse() { - } - - protected CredentialProfileResponse(final VersionedProfileResponse versionedProfileResponse) { - this.versionedProfileResponse = versionedProfileResponse; - } - - public VersionedProfileResponse getVersionedProfileResponse() { - return versionedProfileResponse; - } -} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/ExpiringProfileKeyCredentialProfileResponse.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/ExpiringProfileKeyCredentialProfileResponse.java index 5ac0566a1..823a2709d 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/entities/ExpiringProfileKeyCredentialProfileResponse.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/ExpiringProfileKeyCredentialProfileResponse.java @@ -6,13 +6,20 @@ package org.whispersystems.textsecuregcm.entities; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonUnwrapped; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import io.swagger.v3.oas.annotations.media.Schema; import javax.annotation.Nullable; import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredentialResponse; -public class ExpiringProfileKeyCredentialProfileResponse extends CredentialProfileResponse { +@Schema(description = "Profile response with an expiring profile key credential") +public class ExpiringProfileKeyCredentialProfileResponse { + @JsonUnwrapped + private VersionedProfileResponse versionedProfileResponse; + + @Schema(description = "Expiring profile key credential response. Null if profile version was not found") @JsonProperty @JsonSerialize(using = ExpiringProfileKeyCredentialResponseAdapter.Serializing.class) @JsonDeserialize(using = ExpiringProfileKeyCredentialResponseAdapter.Deserializing.class) @@ -25,7 +32,7 @@ public class ExpiringProfileKeyCredentialProfileResponse extends CredentialProfi public ExpiringProfileKeyCredentialProfileResponse(final VersionedProfileResponse versionedProfileResponse, @Nullable final ExpiringProfileKeyCredentialResponse credential) { - super(versionedProfileResponse); + this.versionedProfileResponse = versionedProfileResponse; this.credential = credential; } @@ -33,4 +40,8 @@ public class ExpiringProfileKeyCredentialProfileResponse extends CredentialProfi public ExpiringProfileKeyCredentialResponse getCredential() { return credential; } + + public VersionedProfileResponse getVersionedProfileResponse() { + return versionedProfileResponse; + } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/ProfileAvatarUploadAttributes.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/ProfileAvatarUploadAttributes.java index 77b1016fa..d31a24275 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/entities/ProfileAvatarUploadAttributes.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/ProfileAvatarUploadAttributes.java @@ -6,44 +6,51 @@ package org.whispersystems.textsecuregcm.entities; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +@Schema(description = "Profile avatar upload form (S3 Post Policy, AWS Signature version 4)") public class ProfileAvatarUploadAttributes { + @Schema(description = "Object key for the avatar") @JsonProperty private String key; + @Schema(description = "Credential for the upload") @JsonProperty private String credential; + @Schema(description = "Access control list setting") @JsonProperty private String acl; + @Schema(description = "Signing algorithm") @JsonProperty private String algorithm; + @Schema(description = "Date in AWS format") @JsonProperty private String date; + @Schema(description = "Base64-encoded upload policy") @JsonProperty private String policy; + @Schema(description = "Signature calculated over the policy") @JsonProperty private String signature; public ProfileAvatarUploadAttributes() {} - public ProfileAvatarUploadAttributes(String key, String credential, - String acl, String algorithm, - String date, String policy, - String signature) - { - this.key = key; + public ProfileAvatarUploadAttributes(String key, String credential, String acl, String algorithm, String date, + String policy, String signature) { + + this.key = key; this.credential = credential; - this.acl = acl; - this.algorithm = algorithm; - this.date = date; - this.policy = policy; - this.signature = signature; + this.acl = acl; + this.algorithm = algorithm; + this.date = date; + this.policy = policy; + this.signature = signature; } public String getKey() { diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/VersionedProfileResponse.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/VersionedProfileResponse.java index 82fd68f2a..4abd60e06 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/entities/VersionedProfileResponse.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/VersionedProfileResponse.java @@ -9,36 +9,45 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonUnwrapped; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import io.swagger.v3.oas.annotations.media.Schema; import org.whispersystems.textsecuregcm.util.ByteArrayBase64WithPaddingAdapter; +@Schema(description = "Versioned profile containing encrypted fields. Versioned fields may be empty if the version was not found.") public record VersionedProfileResponse( + @Schema(description = "Base profile information") @JsonUnwrapped BaseProfileResponse baseProfileResponse, + @Schema(description = "Encrypted profile name. Padded base64.") @JsonProperty @JsonSerialize(using = ByteArrayBase64WithPaddingAdapter.Serializing.class) @JsonDeserialize(using = ByteArrayBase64WithPaddingAdapter.Deserializing.class) byte[] name, + @Schema(description = "Encrypted about text. Padded base64.") @JsonProperty @JsonSerialize(using = ByteArrayBase64WithPaddingAdapter.Serializing.class) @JsonDeserialize(using = ByteArrayBase64WithPaddingAdapter.Deserializing.class) byte[] about, + @Schema(description = "Encrypted about emoji. Padded base64.") @JsonProperty @JsonSerialize(using = ByteArrayBase64WithPaddingAdapter.Serializing.class) @JsonDeserialize(using = ByteArrayBase64WithPaddingAdapter.Deserializing.class) byte[] aboutEmoji, + @Schema(description = "Avatar CDN path") @JsonProperty String avatar, + @Schema(description = "Encrypted payment address. Padded base64.") @JsonProperty @JsonSerialize(using = ByteArrayBase64WithPaddingAdapter.Serializing.class) @JsonDeserialize(using = ByteArrayBase64WithPaddingAdapter.Deserializing.class) byte[] paymentAddress, + @Schema(description = "Encrypted phone number sharing preference. Padded base64.") @JsonProperty @JsonSerialize(using = ByteArrayBase64WithPaddingAdapter.Serializing.class) @JsonDeserialize(using = ByteArrayBase64WithPaddingAdapter.Deserializing.class)