// SPDX-License-Identifier: MIT // Copyright (c) 2026 Thibault Ducray // // This file is part of MyPwdTool's open-source sync/encryption core — see // LICENSE-SYNC-CRYPTO.md at the repo root and https://tducray.fr/mypwdtool/open-source/. // The rest of this application is proprietary and NOT covered by this license. package fr.tducray.mypwdtool.relay import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable /** * Port of the Codable structs in `Sync/RelayAPIClient.swift` and `ServerLimits` in * `Sync/SyncModels.swift`. Field names/JSON keys match exactly — these travel over the wire to * the same relay server the Swift clients talk to, so any drift here is a real interop bug, not * just a style choice. */ @Serializable data class RegistrationChallenge( val nonce: String, val version: String, @SerialName("server_date") val serverDate: String, @SerialName("expires_at") val expiresAt: String, ) @Serializable data class ServerLimits( @SerialName("max_message_bytes") val maxMessageBytes: Int, @SerialName("max_messages_per_day") val maxMessagesPerDay: Int, @SerialName("default_ttl_seconds") val defaultTtlSeconds: Int, @SerialName("max_ttl_seconds") val maxTtlSeconds: Int, @SerialName("max_recipients_per_message") val maxRecipientsPerMessage: Int, ) { companion object { val DEFAULT = ServerLimits( maxMessageBytes = 262144, maxMessagesPerDay = 20000, defaultTtlSeconds = 1296000, maxTtlSeconds = 2592000, maxRecipientsPerMessage = 50, ) } } @Serializable data class RegisterResponse( @SerialName("device_id") val deviceId: String, @SerialName("inbox_id") val inboxId: String, @SerialName("send_token") val sendToken: String, @SerialName("recv_token") val recvToken: String, val limits: ServerLimits, ) data class OutboundRelayMessage( val messageId: String, // UUID, for idempotency val streamId: String, val senderDeviceId: String, val recipientInboxIds: List, val ciphertext: String, // base64url val encryptedIdentifier: String? = null, // optional base64url val ttlSeconds: Int? = null, val cipherVersion: Int = 1, // always 1 ) @Serializable data class SendResponse( val accepted: Boolean, @SerialName("message_id") val messageId: String, ) @Serializable data class PollResponse( @SerialName("inbox_id") val inboxId: String, @SerialName("next_cursor") val nextCursor: String, val messages: List, ) @Serializable data class InboundRelayMessage( @SerialName("message_id") val messageId: String, @SerialName("stream_id") val streamId: String, @SerialName("sender_device_id") val senderDeviceId: String, @SerialName("recipient_inbox_ids") val recipientInboxIds: List, @SerialName("created_at") val createdAt: String, @SerialName("cipher_version") val cipherVersion: Int, @SerialName("encrypted_identifier") val encryptedIdentifier: String? = null, val ciphertext: String, ) /** Server error envelope, used to discriminate token_expired/challenge_failed from a plain 401. */ @Serializable data class RelayApiErrorEnvelope(val error: RelayApiErrorBody) @Serializable data class RelayApiErrorBody(val code: String)