import Foundation
/// The app's self-description at `GET https://<host>/.well-known/ap2p`.
/// `relay` and `mailbox` are present only when those features are enabled —
/// feature-detect from their presence rather than probing.
public struct AppDescriptor: Sendable, Equatable, Decodable {
public struct Endpoints: Sendable, Equatable, Decodable {
public var ws: URL
public var turnCredentials: URL
enum CodingKeys: String, CodingKey { case ws, turnCredentials = "turn_credentials" }
public init(ws: URL, turnCredentials: URL) { self.ws = ws; self.turnCredentials = turnCredentials }
}
public struct Relay: Sendable, Equatable, Decodable {
public var peerId: String
public var multiaddrs: [String]
public var pubsubDiscoveryTopic: String?
enum CodingKeys: String, CodingKey { case peerId = "peer_id", multiaddrs, pubsubDiscoveryTopic = "pubsub_discovery_topic" }
}
public struct Mailbox: Sendable, Equatable, Decodable {
public var endpoint: URL
public var ttlHours: Int
public var maxMB: Int
public var maxMessageKB: Int
public var openMint: Bool
/// Present only while `openMint` is true.
public var mintEndpoint: URL?
public var mintIPPerMin: Int?
public var mintAppPerMin: Int?
enum CodingKeys: String, CodingKey {
case endpoint, ttlHours = "ttl_hours", maxMB = "max_mb", maxMessageKB = "max_message_kb", openMint = "open_mint"
case mintEndpoint = "mint_endpoint", mintIPPerMin = "mint_ip_per_min", mintAppPerMin = "mint_app_per_min"
}
public init(endpoint: URL, ttlHours: Int, maxMB: Int, maxMessageKB: Int, openMint: Bool, mintEndpoint: URL?, mintIPPerMin: Int? = nil, mintAppPerMin: Int? = nil) {
self.endpoint = endpoint; self.ttlHours = ttlHours; self.maxMB = maxMB; self.maxMessageKB = maxMessageKB
self.openMint = openMint; self.mintEndpoint = mintEndpoint; self.mintIPPerMin = mintIPPerMin; self.mintAppPerMin = mintAppPerMin
}
}
public struct Limits: Sendable, Equatable, Decodable {
public var maxRooms: Int
public var maxRoomPeers: Int
public var maxConnsPerIP: Int
public var maxBytesPerSec: Int
public var messagesPerSec: Int
public var messageBurst: Int
public var maxFrameBytes: Int
enum CodingKeys: String, CodingKey {
case maxRooms = "max_rooms", maxRoomPeers = "max_room_peers", maxConnsPerIP = "max_conns_per_ip"
case maxBytesPerSec = "max_bytes_per_sec", messagesPerSec = "messages_per_sec", messageBurst = "message_burst", maxFrameBytes = "max_frame_bytes"
}
/// The node-wide defaults, used when the descriptor cannot be fetched.
public static let defaults = Limits(maxRooms: 500, maxRoomPeers: 16, maxConnsPerIP: 20, maxBytesPerSec: 262_144, messagesPerSec: 30, messageBurst: 60, maxFrameBytes: 65_536)
public init(maxRooms: Int, maxRoomPeers: Int, maxConnsPerIP: Int, maxBytesPerSec: Int, messagesPerSec: Int, messageBurst: Int, maxFrameBytes: Int) {
self.maxRooms = maxRooms; self.maxRoomPeers = maxRoomPeers; self.maxConnsPerIP = maxConnsPerIP
self.maxBytesPerSec = maxBytesPerSec; self.messagesPerSec = messagesPerSec; self.messageBurst = messageBurst; self.maxFrameBytes = maxFrameBytes
}
}
public var protocolName: String
public var version: String
public var spec: URL?
public var endpoints: Endpoints
public var relay: Relay?
public var mailbox: Mailbox?
public var limits: Limits
/// True when the descriptor was synthesized because the fetch failed.
public var isFallback: Bool = false
enum CodingKeys: String, CodingKey { case protocolName = "protocol", version, spec, endpoints, relay, mailbox, limits }
public init(from decoder: any Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
protocolName = try c.decodeIfPresent(String.self, forKey: .protocolName) ?? "ap2p-signal"
version = try c.decodeIfPresent(String.self, forKey: .version) ?? "1"
spec = try? c.decodeIfPresent(URL.self, forKey: .spec)
endpoints = try c.decode(Endpoints.self, forKey: .endpoints)
relay = try? c.decodeIfPresent(Relay.self, forKey: .relay)
mailbox = try? c.decodeIfPresent(Mailbox.self, forKey: .mailbox)
limits = try c.decodeIfPresent(Limits.self, forKey: .limits) ?? .defaults
}
public init(protocolName: String = "ap2p-signal", version: String = "1", spec: URL? = nil, endpoints: Endpoints, relay: Relay? = nil, mailbox: Mailbox? = nil, limits: Limits = .defaults, isFallback: Bool = false) {
self.protocolName = protocolName; self.version = version; self.spec = spec; self.endpoints = endpoints
self.relay = relay; self.mailbox = mailbox; self.limits = limits; self.isFallback = isFallback
}
public static func decode(_ data: Data) throws -> AppDescriptor {
try JSONDecoder().decode(AppDescriptor.self, from: data)
}
/// What the descriptor of `host` would say if only the documented default
/// paths and node-wide limits were known. No mailbox (cannot be detected offline).
public static func fallback(host: String) -> AppDescriptor {
AppDescriptor(
endpoints: Endpoints(ws: URL(string: "wss://\(host)/ws")!, turnCredentials: URL(string: "https://\(host)/turn-credentials")!),
limits: .defaults, isFallback: true
)
}
}