feat(sdk/kotlin): add Database, Hosting, and Bridge SDKs

- SynorDatabase: Multi-model database with KV, Document, Vector, TimeSeries stores
- SynorHosting: Domain management, DNS, deployments, SSL, analytics
- SynorBridge: Cross-chain transfers with lock-mint and burn-unlock patterns
This commit is contained in:
Gulshan Yadav 2026-01-27 02:04:13 +05:30
parent dd01a06116
commit 14cd439552
3 changed files with 1124 additions and 0 deletions

View file

@ -0,0 +1,469 @@
package io.synor.bridge
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import kotlinx.coroutines.delay
import java.util.concurrent.atomic.AtomicBoolean
/**
* Synor Bridge SDK for Kotlin.
* Cross-chain asset transfers with lock-mint and burn-unlock patterns.
*/
class SynorBridge(private val config: BridgeConfig) : AutoCloseable {
companion object {
private val gson: Gson = GsonBuilder().create()
private val FINAL_STATUSES = setOf(TransferStatus.completed, TransferStatus.failed, TransferStatus.refunded)
}
private val httpClient = HttpClient(CIO) {
engine { requestTimeout = config.timeoutSecs * 1000 }
}
private val closed = AtomicBoolean(false)
constructor(apiKey: String) : this(BridgeConfig(apiKey))
// ==================== Chain Operations ====================
suspend fun getSupportedChains(): List<Chain> {
val resp = request("GET", "/chains")
val result = gson.fromJson<ChainsResponse>(resp, ChainsResponse::class.java)
return result.chains ?: emptyList()
}
suspend fun getChain(chainId: ChainId): Chain {
val resp = request("GET", "/chains/${chainId.name.lowercase()}")
return gson.fromJson(resp, Chain::class.java)
}
suspend fun isChainSupported(chainId: ChainId): Boolean = try {
getChain(chainId).supported
} catch (e: Exception) { false }
// ==================== Asset Operations ====================
suspend fun getSupportedAssets(chainId: ChainId): List<Asset> {
val resp = request("GET", "/chains/${chainId.name.lowercase()}/assets")
val result = gson.fromJson<AssetsResponse>(resp, AssetsResponse::class.java)
return result.assets ?: emptyList()
}
suspend fun getAsset(assetId: String): Asset {
val resp = request("GET", "/assets/${assetId.urlEncode()}")
return gson.fromJson(resp, Asset::class.java)
}
suspend fun getWrappedAsset(originalAssetId: String, targetChain: ChainId): WrappedAsset {
val path = "/assets/${originalAssetId.urlEncode()}/wrapped/${targetChain.name.lowercase()}"
val resp = request("GET", path)
return gson.fromJson(resp, WrappedAsset::class.java)
}
// ==================== Fee & Rate Operations ====================
suspend fun estimateFee(asset: String, amount: String, sourceChain: ChainId, targetChain: ChainId): FeeEstimate {
val body = mapOf(
"asset" to asset,
"amount" to amount,
"sourceChain" to sourceChain.name.lowercase(),
"targetChain" to targetChain.name.lowercase()
)
val resp = request("POST", "/fees/estimate", body)
return gson.fromJson(resp, FeeEstimate::class.java)
}
suspend fun getExchangeRate(fromAsset: String, toAsset: String): ExchangeRate {
val resp = request("GET", "/rates/${fromAsset.urlEncode()}/${toAsset.urlEncode()}")
return gson.fromJson(resp, ExchangeRate::class.java)
}
// ==================== Lock-Mint Flow ====================
suspend fun lock(asset: String, amount: String, targetChain: ChainId, options: LockOptions? = null): LockReceipt {
val body = mutableMapOf<String, Any>(
"asset" to asset,
"amount" to amount,
"targetChain" to targetChain.name.lowercase()
)
options?.recipient?.let { body["recipient"] = it }
options?.deadline?.let { body["deadline"] = it }
options?.slippage?.let { body["slippage"] = it }
val resp = request("POST", "/transfers/lock", body)
return gson.fromJson(resp, LockReceipt::class.java)
}
suspend fun getLockProof(lockReceiptId: String): LockProof {
val resp = request("GET", "/transfers/lock/${lockReceiptId.urlEncode()}/proof")
return gson.fromJson(resp, LockProof::class.java)
}
suspend fun waitForLockProof(lockReceiptId: String, pollIntervalMs: Long = 5000, maxWaitMs: Long = 600000): LockProof {
val deadline = System.currentTimeMillis() + maxWaitMs
while (System.currentTimeMillis() < deadline) {
try {
return getLockProof(lockReceiptId)
} catch (e: BridgeException) {
if (e.code == "CONFIRMATIONS_PENDING") {
delay(pollIntervalMs)
continue
}
throw e
}
}
throw BridgeException("Timeout waiting for lock proof", "CONFIRMATIONS_PENDING")
}
suspend fun mint(proof: LockProof, targetAddress: String, options: MintOptions? = null): SignedTransaction {
val body = mutableMapOf<String, Any>("proof" to proof, "targetAddress" to targetAddress)
options?.gasLimit?.let { body["gasLimit"] = it }
options?.maxFeePerGas?.let { body["maxFeePerGas"] = it }
options?.maxPriorityFeePerGas?.let { body["maxPriorityFeePerGas"] = it }
val resp = request("POST", "/transfers/mint", body)
return gson.fromJson(resp, SignedTransaction::class.java)
}
// ==================== Burn-Unlock Flow ====================
suspend fun burn(wrappedAsset: String, amount: String, options: BurnOptions? = null): BurnReceipt {
val body = mutableMapOf<String, Any>("wrappedAsset" to wrappedAsset, "amount" to amount)
options?.recipient?.let { body["recipient"] = it }
options?.deadline?.let { body["deadline"] = it }
val resp = request("POST", "/transfers/burn", body)
return gson.fromJson(resp, BurnReceipt::class.java)
}
suspend fun getBurnProof(burnReceiptId: String): BurnProof {
val resp = request("GET", "/transfers/burn/${burnReceiptId.urlEncode()}/proof")
return gson.fromJson(resp, BurnProof::class.java)
}
suspend fun waitForBurnProof(burnReceiptId: String, pollIntervalMs: Long = 5000, maxWaitMs: Long = 600000): BurnProof {
val deadline = System.currentTimeMillis() + maxWaitMs
while (System.currentTimeMillis() < deadline) {
try {
return getBurnProof(burnReceiptId)
} catch (e: BridgeException) {
if (e.code == "CONFIRMATIONS_PENDING") {
delay(pollIntervalMs)
continue
}
throw e
}
}
throw BridgeException("Timeout waiting for burn proof", "CONFIRMATIONS_PENDING")
}
suspend fun unlock(proof: BurnProof, options: UnlockOptions? = null): SignedTransaction {
val body = mutableMapOf<String, Any>("proof" to proof)
options?.gasLimit?.let { body["gasLimit"] = it }
options?.gasPrice?.let { body["gasPrice"] = it }
val resp = request("POST", "/transfers/unlock", body)
return gson.fromJson(resp, SignedTransaction::class.java)
}
// ==================== Transfer Management ====================
suspend fun getTransfer(transferId: String): Transfer {
val resp = request("GET", "/transfers/${transferId.urlEncode()}")
return gson.fromJson(resp, Transfer::class.java)
}
suspend fun getTransferStatus(transferId: String): TransferStatus = getTransfer(transferId).status
suspend fun listTransfers(filter: TransferFilter? = null): List<Transfer> {
val params = mutableListOf<String>()
filter?.status?.let { params.add("status=${it.name.lowercase()}") }
filter?.sourceChain?.let { params.add("sourceChain=${it.name.lowercase()}") }
filter?.targetChain?.let { params.add("targetChain=${it.name.lowercase()}") }
filter?.limit?.let { params.add("limit=$it") }
filter?.offset?.let { params.add("offset=$it") }
val path = if (params.isNotEmpty()) "/transfers?${params.joinToString("&")}" else "/transfers"
val resp = request("GET", path)
val result = gson.fromJson<TransfersResponse>(resp, TransfersResponse::class.java)
return result.transfers ?: emptyList()
}
suspend fun waitForTransfer(transferId: String, pollIntervalMs: Long = 10000, maxWaitMs: Long = 1800000): Transfer {
val deadline = System.currentTimeMillis() + maxWaitMs
while (System.currentTimeMillis() < deadline) {
val transfer = getTransfer(transferId)
if (transfer.status in FINAL_STATUSES) return transfer
delay(pollIntervalMs)
}
throw BridgeException("Timeout waiting for transfer completion")
}
// ==================== Convenience Methods ====================
suspend fun bridgeTo(
asset: String,
amount: String,
targetChain: ChainId,
targetAddress: String,
lockOptions: LockOptions? = null,
mintOptions: MintOptions? = null
): Transfer {
val lockReceipt = lock(asset, amount, targetChain, lockOptions)
if (config.debug) println("Locked: ${lockReceipt.id}, waiting for confirmations...")
val proof = waitForLockProof(lockReceipt.id)
if (config.debug) println("Proof ready, minting on ${targetChain.name}...")
mint(proof, targetAddress, mintOptions)
return waitForTransfer(lockReceipt.id)
}
suspend fun bridgeBack(
wrappedAsset: String,
amount: String,
burnOptions: BurnOptions? = null,
unlockOptions: UnlockOptions? = null
): Transfer {
val burnReceipt = burn(wrappedAsset, amount, burnOptions)
if (config.debug) println("Burned: ${burnReceipt.id}, waiting for confirmations...")
val proof = waitForBurnProof(burnReceipt.id)
if (config.debug) println("Proof ready, unlocking on ${burnReceipt.targetChain}...")
unlock(proof, unlockOptions)
return waitForTransfer(burnReceipt.id)
}
// ==================== Lifecycle ====================
override fun close() {
closed.set(true)
httpClient.close()
}
fun isClosed(): Boolean = closed.get()
suspend fun healthCheck(): Boolean = try {
val resp = request("GET", "/health")
val map = gson.fromJson<Map<String, Any>>(resp, object : TypeToken<Map<String, Any>>() {}.type)
map["status"] == "healthy"
} catch (e: Exception) { false }
// ==================== Private Methods ====================
private suspend fun request(method: String, path: String, body: Any? = null): String {
if (closed.get()) throw BridgeException("Client has been closed")
var lastError: Exception? = null
repeat(config.retries) { attempt ->
try {
return doRequest(method, path, body)
} catch (e: Exception) {
lastError = e
if (attempt < config.retries - 1) delay((1L shl attempt) * 1000)
}
}
throw lastError ?: BridgeException("Unknown error")
}
private suspend fun doRequest(method: String, path: String, body: Any?): String {
val url = "${config.endpoint}$path"
val response: HttpResponse = httpClient.request(url) {
this.method = HttpMethod.parse(method)
header("Authorization", "Bearer ${config.apiKey}")
header("Content-Type", "application/json")
header("X-SDK-Version", "kotlin/0.1.0")
body?.let { setBody(gson.toJson(it)) }
}
val responseBody = response.bodyAsText()
if (response.status.value >= 400) {
val errorMap = try {
gson.fromJson<Map<String, Any>>(responseBody, object : TypeToken<Map<String, Any>>() {}.type)
} catch (e: Exception) { emptyMap() }
throw BridgeException(
errorMap["message"] as? String ?: "HTTP ${response.status.value}",
errorMap["code"] as? String,
response.status.value
)
}
return responseBody
}
private fun String.urlEncode(): String = java.net.URLEncoder.encode(this, "UTF-8")
}
// ==================== Types ====================
data class BridgeConfig(
val apiKey: String,
val endpoint: String = "https://bridge.synor.io/v1",
val timeoutSecs: Long = 60,
val retries: Int = 3,
val debug: Boolean = false
)
enum class ChainId { synor, ethereum, polygon, arbitrum, optimism, bsc, avalanche, solana, cosmos }
enum class AssetType { native, erc20, erc721, erc1155 }
enum class TransferStatus { pending, locked, confirming, minting, completed, failed, refunded }
enum class TransferDirection { lock_mint, burn_unlock }
data class NativeCurrency(val name: String, val symbol: String, val decimals: Int)
data class Chain(
val id: ChainId,
val name: String,
val chainId: Long,
val rpcUrl: String,
val explorerUrl: String,
val nativeCurrency: NativeCurrency,
val confirmations: Int,
val estimatedBlockTime: Int,
val supported: Boolean
)
data class Asset(
val id: String,
val symbol: String,
val name: String,
val type: AssetType,
val chain: ChainId,
val contractAddress: String? = null,
val decimals: Int,
val logoUrl: String? = null,
val verified: Boolean = false
)
data class WrappedAsset(
val originalAsset: Asset,
val wrappedAsset: Asset,
val chain: ChainId,
val bridgeContract: String
)
data class ValidatorSignature(val validator: String, val signature: String, val timestamp: Long)
data class LockReceipt(
val id: String,
val txHash: String,
val sourceChain: ChainId,
val targetChain: ChainId,
val asset: Asset,
val amount: String,
val sender: String,
val recipient: String,
val lockTimestamp: Long,
val confirmations: Int,
val requiredConfirmations: Int
)
data class LockProof(
val lockReceipt: LockReceipt,
val merkleProof: List<String>,
val blockHeader: String,
val signatures: List<ValidatorSignature>
)
data class BurnReceipt(
val id: String,
val txHash: String,
val sourceChain: ChainId,
val targetChain: ChainId,
val wrappedAsset: Asset,
val originalAsset: Asset,
val amount: String,
val sender: String,
val recipient: String,
val burnTimestamp: Long,
val confirmations: Int,
val requiredConfirmations: Int
)
data class BurnProof(
val burnReceipt: BurnReceipt,
val merkleProof: List<String>,
val blockHeader: String,
val signatures: List<ValidatorSignature>
)
data class Transfer(
val id: String,
val direction: TransferDirection,
val status: TransferStatus,
val sourceChain: ChainId,
val targetChain: ChainId,
val asset: Asset,
val amount: String,
val sender: String,
val recipient: String,
val sourceTxHash: String? = null,
val targetTxHash: String? = null,
val fee: String,
val feeAsset: Asset,
val createdAt: Long,
val updatedAt: Long,
val completedAt: Long? = null,
val errorMessage: String? = null
)
data class FeeEstimate(
val bridgeFee: String,
val gasFeeSource: String,
val gasFeeTarget: String,
val totalFee: String,
val feeAsset: Asset,
val estimatedTime: Int,
val exchangeRate: String? = null
)
data class ExchangeRate(
val fromAsset: Asset,
val toAsset: Asset,
val rate: String,
val inverseRate: String,
val lastUpdated: Long,
val source: String
)
data class SignedTransaction(
val txHash: String,
val chain: ChainId,
val from: String,
val to: String,
val value: String,
val data: String,
val gasLimit: String,
val gasPrice: String? = null,
val maxFeePerGas: String? = null,
val maxPriorityFeePerGas: String? = null,
val nonce: Int,
val signature: String
)
data class LockOptions(val recipient: String? = null, val deadline: Long? = null, val slippage: Double? = null)
data class MintOptions(val gasLimit: String? = null, val maxFeePerGas: String? = null, val maxPriorityFeePerGas: String? = null)
data class BurnOptions(val recipient: String? = null, val deadline: Long? = null)
data class UnlockOptions(val gasLimit: String? = null, val gasPrice: String? = null)
data class TransferFilter(
val status: TransferStatus? = null,
val sourceChain: ChainId? = null,
val targetChain: ChainId? = null,
val asset: String? = null,
val sender: String? = null,
val recipient: String? = null,
val limit: Int? = null,
val offset: Int? = null
)
internal data class ChainsResponse(val chains: List<Chain>?)
internal data class AssetsResponse(val assets: List<Asset>?)
internal data class TransfersResponse(val transfers: List<Transfer>?)
class BridgeException(
message: String,
val code: String? = null,
val statusCode: Int = 0
) : RuntimeException(message)

View file

@ -0,0 +1,271 @@
package io.synor.database
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import kotlinx.coroutines.delay
import java.util.concurrent.atomic.AtomicBoolean
/**
* Synor Database SDK for Kotlin.
* Multi-model database with Key-Value, Document, Vector, and Time Series stores.
*/
class SynorDatabase(private val config: DatabaseConfig) : AutoCloseable {
companion object {
private const val DEFAULT_ENDPOINT = "https://db.synor.io/v1"
private val gson: Gson = GsonBuilder().create()
}
private val httpClient = HttpClient(CIO) {
engine {
requestTimeout = config.timeoutSecs * 1000
}
}
private val closed = AtomicBoolean(false)
val kv = KeyValueStore(this)
val documents = DocumentStore(this)
val vectors = VectorStore(this)
val timeseries = TimeSeriesStore(this)
constructor(apiKey: String) : this(DatabaseConfig(apiKey))
// ==================== Key-Value Store ====================
class KeyValueStore internal constructor(private val db: SynorDatabase) {
suspend fun get(key: String): Any? {
val resp = db.request("GET", "/kv/${key.urlEncode()}")
val map = gson.fromJson<Map<String, Any>>(resp, object : TypeToken<Map<String, Any>>() {}.type)
return map["value"]
}
suspend fun set(key: String, value: Any, ttl: Int? = null) {
val body = mutableMapOf<String, Any>("key" to key, "value" to value)
ttl?.let { body["ttl"] = it }
db.request("PUT", "/kv/${key.urlEncode()}", body)
}
suspend fun delete(key: String) {
db.request("DELETE", "/kv/${key.urlEncode()}")
}
suspend fun list(prefix: String): List<KeyValue> {
val resp = db.request("GET", "/kv?prefix=${prefix.urlEncode()}")
val result = gson.fromJson<ListResponse<KeyValue>>(resp, object : TypeToken<ListResponse<KeyValue>>() {}.type)
return result.items ?: emptyList()
}
}
// ==================== Document Store ====================
class DocumentStore internal constructor(private val db: SynorDatabase) {
suspend fun create(collection: String, document: Map<String, Any>): String {
val resp = db.request("POST", "/collections/${collection.urlEncode()}/documents", document)
val map = gson.fromJson<Map<String, Any>>(resp, object : TypeToken<Map<String, Any>>() {}.type)
return map["id"] as String
}
suspend fun get(collection: String, id: String): Document {
val resp = db.request("GET", "/collections/${collection.urlEncode()}/documents/${id.urlEncode()}")
return gson.fromJson(resp, Document::class.java)
}
suspend fun update(collection: String, id: String, update: Map<String, Any>) {
db.request("PATCH", "/collections/${collection.urlEncode()}/documents/${id.urlEncode()}", update)
}
suspend fun delete(collection: String, id: String) {
db.request("DELETE", "/collections/${collection.urlEncode()}/documents/${id.urlEncode()}")
}
suspend fun query(collection: String, query: Query): List<Document> {
val resp = db.request("POST", "/collections/${collection.urlEncode()}/query", query)
val result = gson.fromJson<DocumentListResponse>(resp, DocumentListResponse::class.java)
return result.documents ?: emptyList()
}
}
// ==================== Vector Store ====================
class VectorStore internal constructor(private val db: SynorDatabase) {
suspend fun upsert(collection: String, vectors: List<VectorEntry>) {
db.request("POST", "/vectors/${collection.urlEncode()}/upsert", mapOf("vectors" to vectors))
}
suspend fun search(collection: String, vector: DoubleArray, k: Int): List<SearchResult> {
val resp = db.request("POST", "/vectors/${collection.urlEncode()}/search", mapOf("vector" to vector, "k" to k))
val result = gson.fromJson<SearchListResponse>(resp, SearchListResponse::class.java)
return result.results ?: emptyList()
}
suspend fun delete(collection: String, ids: List<String>) {
db.request("DELETE", "/vectors/${collection.urlEncode()}", mapOf("ids" to ids))
}
}
// ==================== Time Series Store ====================
class TimeSeriesStore internal constructor(private val db: SynorDatabase) {
suspend fun write(series: String, points: List<DataPoint>) {
db.request("POST", "/timeseries/${series.urlEncode()}/write", mapOf("points" to points))
}
suspend fun query(series: String, range: TimeRange, aggregation: Aggregation? = null): List<DataPoint> {
val body = mutableMapOf<String, Any>("range" to range)
aggregation?.let { body["aggregation"] = it }
val resp = db.request("POST", "/timeseries/${series.urlEncode()}/query", body)
val result = gson.fromJson<DataPointListResponse>(resp, DataPointListResponse::class.java)
return result.points ?: emptyList()
}
}
// ==================== Lifecycle ====================
override fun close() {
closed.set(true)
httpClient.close()
}
fun isClosed(): Boolean = closed.get()
suspend fun healthCheck(): Boolean {
return try {
val resp = request("GET", "/health")
val map = gson.fromJson<Map<String, Any>>(resp, object : TypeToken<Map<String, Any>>() {}.type)
map["status"] == "healthy"
} catch (e: Exception) {
false
}
}
// ==================== Private Methods ====================
internal suspend fun request(method: String, path: String, body: Any? = null): String {
if (closed.get()) throw DatabaseException("Client has been closed")
var lastError: Exception? = null
repeat(config.retries) { attempt ->
try {
return doRequest(method, path, body)
} catch (e: Exception) {
lastError = e
if (attempt < config.retries - 1) {
delay((1L shl attempt) * 1000)
}
}
}
throw lastError ?: DatabaseException("Unknown error")
}
private suspend fun doRequest(method: String, path: String, body: Any?): String {
val url = "${config.endpoint}$path"
val response: HttpResponse = httpClient.request(url) {
this.method = HttpMethod.parse(method)
header("Authorization", "Bearer ${config.apiKey}")
header("Content-Type", "application/json")
header("X-SDK-Version", "kotlin/0.1.0")
body?.let { setBody(gson.toJson(it)) }
}
val responseBody = response.bodyAsText()
if (response.status.value >= 400) {
val errorMap = try {
gson.fromJson<Map<String, Any>>(responseBody, object : TypeToken<Map<String, Any>>() {}.type)
} catch (e: Exception) { emptyMap() }
val message = errorMap["message"] as? String ?: "HTTP ${response.status.value}"
throw DatabaseException(message, errorMap["code"] as? String, response.status.value)
}
return responseBody
}
private fun String.urlEncode(): String = java.net.URLEncoder.encode(this, "UTF-8")
}
// ==================== Types ====================
data class DatabaseConfig(
val apiKey: String,
val endpoint: String = "https://db.synor.io/v1",
val timeoutSecs: Long = 60,
val retries: Int = 3,
val debug: Boolean = false
)
data class KeyValue(
val key: String,
val value: Any?,
val ttl: Int? = null,
val createdAt: Long? = null,
val updatedAt: Long? = null
)
data class Document(
val id: String,
val collection: String,
val data: Map<String, Any>,
val createdAt: Long,
val updatedAt: Long
)
data class Query(
val filter: Map<String, Any>? = null,
val sort: Map<String, Int>? = null,
val limit: Int? = null,
val offset: Int? = null,
val projection: List<String>? = null
)
data class VectorEntry(
val id: String,
val vector: DoubleArray,
val metadata: Map<String, Any>? = null
)
data class SearchResult(
val id: String,
val score: Double,
val vector: DoubleArray? = null,
val metadata: Map<String, Any>? = null
)
data class DataPoint(
val timestamp: Long,
val value: Double,
val tags: Map<String, String>? = null
)
data class TimeRange(
val start: Long,
val end: Long
)
data class Aggregation(
val function: String,
val interval: String
)
// Response types
internal data class ListResponse<T>(val items: List<T>?)
internal data class DocumentListResponse(val documents: List<Document>?)
internal data class SearchListResponse(val results: List<SearchResult>?)
internal data class DataPointListResponse(val points: List<DataPoint>?)
class DatabaseException(
message: String,
val code: String? = null,
val statusCode: Int = 0
) : RuntimeException(message)

View file

@ -0,0 +1,384 @@
package io.synor.hosting
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import kotlinx.coroutines.delay
import java.util.concurrent.atomic.AtomicBoolean
/**
* Synor Hosting SDK for Kotlin.
* Decentralized web hosting with domain management, DNS, and deployments.
*/
class SynorHosting(private val config: HostingConfig) : AutoCloseable {
companion object {
private val gson: Gson = GsonBuilder().create()
}
private val httpClient = HttpClient(CIO) {
engine { requestTimeout = config.timeoutSecs * 1000 }
}
private val closed = AtomicBoolean(false)
constructor(apiKey: String) : this(HostingConfig(apiKey))
// ==================== Domain Operations ====================
suspend fun checkAvailability(name: String): DomainAvailability {
val resp = request("GET", "/domains/check/${name.urlEncode()}")
return gson.fromJson(resp, DomainAvailability::class.java)
}
suspend fun registerDomain(name: String, options: RegisterDomainOptions? = null): Domain {
val body = mutableMapOf<String, Any>("name" to name)
options?.years?.let { body["years"] = it }
options?.autoRenew?.let { body["autoRenew"] = it }
val resp = request("POST", "/domains", body)
return gson.fromJson(resp, Domain::class.java)
}
suspend fun getDomain(name: String): Domain {
val resp = request("GET", "/domains/${name.urlEncode()}")
return gson.fromJson(resp, Domain::class.java)
}
suspend fun listDomains(): List<Domain> {
val resp = request("GET", "/domains")
val result = gson.fromJson<DomainsResponse>(resp, DomainsResponse::class.java)
return result.domains ?: emptyList()
}
suspend fun updateDomainRecord(name: String, record: DomainRecord): Domain {
val resp = request("PUT", "/domains/${name.urlEncode()}/record", record)
return gson.fromJson(resp, Domain::class.java)
}
suspend fun resolveDomain(name: String): DomainRecord {
val resp = request("GET", "/domains/${name.urlEncode()}/resolve")
return gson.fromJson(resp, DomainRecord::class.java)
}
suspend fun renewDomain(name: String, years: Int): Domain {
val resp = request("POST", "/domains/${name.urlEncode()}/renew", mapOf("years" to years))
return gson.fromJson(resp, Domain::class.java)
}
// ==================== DNS Operations ====================
suspend fun getDnsZone(domain: String): DnsZone {
val resp = request("GET", "/dns/${domain.urlEncode()}")
return gson.fromJson(resp, DnsZone::class.java)
}
suspend fun setDnsRecords(domain: String, records: List<DnsRecord>): DnsZone {
val resp = request("PUT", "/dns/${domain.urlEncode()}", mapOf("records" to records))
return gson.fromJson(resp, DnsZone::class.java)
}
suspend fun addDnsRecord(domain: String, record: DnsRecord): DnsZone {
val resp = request("POST", "/dns/${domain.urlEncode()}/records", record)
return gson.fromJson(resp, DnsZone::class.java)
}
suspend fun deleteDnsRecord(domain: String, recordType: String, name: String): DnsZone {
val path = "/dns/${domain.urlEncode()}/records/$recordType/${name.urlEncode()}"
val resp = request("DELETE", path)
return gson.fromJson(resp, DnsZone::class.java)
}
// ==================== Deployment Operations ====================
suspend fun deploy(cid: String, options: DeployOptions? = null): Deployment {
val body = mutableMapOf<String, Any>("cid" to cid)
options?.domain?.let { body["domain"] = it }
options?.subdomain?.let { body["subdomain"] = it }
options?.spa?.let { body["spa"] = it }
options?.cleanUrls?.let { body["cleanUrls"] = it }
val resp = request("POST", "/deployments", body)
return gson.fromJson(resp, Deployment::class.java)
}
suspend fun getDeployment(id: String): Deployment {
val resp = request("GET", "/deployments/${id.urlEncode()}")
return gson.fromJson(resp, Deployment::class.java)
}
suspend fun listDeployments(domain: String? = null): List<Deployment> {
val path = domain?.let { "/deployments?domain=${it.urlEncode()}" } ?: "/deployments"
val resp = request("GET", path)
val result = gson.fromJson<DeploymentsResponse>(resp, DeploymentsResponse::class.java)
return result.deployments ?: emptyList()
}
suspend fun rollback(domain: String, deploymentId: String): Deployment {
val resp = request("POST", "/deployments/${deploymentId.urlEncode()}/rollback", mapOf("domain" to domain))
return gson.fromJson(resp, Deployment::class.java)
}
suspend fun deleteDeployment(id: String) {
request("DELETE", "/deployments/${id.urlEncode()}")
}
// ==================== SSL Operations ====================
suspend fun provisionSsl(domain: String, options: ProvisionSslOptions? = null): Certificate {
val resp = request("POST", "/ssl/${domain.urlEncode()}", options)
return gson.fromJson(resp, Certificate::class.java)
}
suspend fun getCertificate(domain: String): Certificate {
val resp = request("GET", "/ssl/${domain.urlEncode()}")
return gson.fromJson(resp, Certificate::class.java)
}
suspend fun renewCertificate(domain: String): Certificate {
val resp = request("POST", "/ssl/${domain.urlEncode()}/renew")
return gson.fromJson(resp, Certificate::class.java)
}
suspend fun deleteCertificate(domain: String) {
request("DELETE", "/ssl/${domain.urlEncode()}")
}
// ==================== Site Configuration ====================
suspend fun getSiteConfig(domain: String): SiteConfig {
val resp = request("GET", "/sites/${domain.urlEncode()}/config")
return gson.fromJson(resp, SiteConfig::class.java)
}
suspend fun updateSiteConfig(domain: String, config: Map<String, Any>): SiteConfig {
val resp = request("PATCH", "/sites/${domain.urlEncode()}/config", config)
return gson.fromJson(resp, SiteConfig::class.java)
}
suspend fun purgeCache(domain: String, paths: List<String>? = null): Long {
val body = paths?.let { mapOf("paths" to it) }
val resp = request("DELETE", "/sites/${domain.urlEncode()}/cache", body)
val map = gson.fromJson<Map<String, Any>>(resp, object : TypeToken<Map<String, Any>>() {}.type)
return (map["purged"] as Number).toLong()
}
// ==================== Analytics ====================
suspend fun getAnalytics(domain: String, options: AnalyticsOptions? = null): AnalyticsData {
val params = mutableListOf<String>()
options?.period?.let { params.add("period=${it.urlEncode()}") }
options?.start?.let { params.add("start=${it.urlEncode()}") }
options?.end?.let { params.add("end=${it.urlEncode()}") }
val path = if (params.isNotEmpty()) {
"/sites/${domain.urlEncode()}/analytics?${params.joinToString("&")}"
} else {
"/sites/${domain.urlEncode()}/analytics"
}
val resp = request("GET", path)
return gson.fromJson(resp, AnalyticsData::class.java)
}
// ==================== Lifecycle ====================
override fun close() {
closed.set(true)
httpClient.close()
}
fun isClosed(): Boolean = closed.get()
suspend fun healthCheck(): Boolean = try {
val resp = request("GET", "/health")
val map = gson.fromJson<Map<String, Any>>(resp, object : TypeToken<Map<String, Any>>() {}.type)
map["status"] == "healthy"
} catch (e: Exception) { false }
// ==================== Private Methods ====================
private suspend fun request(method: String, path: String, body: Any? = null): String {
if (closed.get()) throw HostingException("Client has been closed")
var lastError: Exception? = null
repeat(config.retries) { attempt ->
try {
return doRequest(method, path, body)
} catch (e: Exception) {
lastError = e
if (attempt < config.retries - 1) delay((1L shl attempt) * 1000)
}
}
throw lastError ?: HostingException("Unknown error")
}
private suspend fun doRequest(method: String, path: String, body: Any?): String {
val url = "${config.endpoint}$path"
val response: HttpResponse = httpClient.request(url) {
this.method = HttpMethod.parse(method)
header("Authorization", "Bearer ${config.apiKey}")
header("Content-Type", "application/json")
header("X-SDK-Version", "kotlin/0.1.0")
body?.let { setBody(gson.toJson(it)) }
}
val responseBody = response.bodyAsText()
if (response.status.value >= 400) {
val errorMap = try {
gson.fromJson<Map<String, Any>>(responseBody, object : TypeToken<Map<String, Any>>() {}.type)
} catch (e: Exception) { emptyMap() }
throw HostingException(
errorMap["message"] as? String ?: "HTTP ${response.status.value}",
errorMap["code"] as? String,
response.status.value
)
}
return responseBody
}
private fun String.urlEncode(): String = java.net.URLEncoder.encode(this, "UTF-8")
}
// ==================== Types ====================
data class HostingConfig(
val apiKey: String,
val endpoint: String = "https://hosting.synor.io/v1",
val timeoutSecs: Long = 60,
val retries: Int = 3,
val debug: Boolean = false
)
enum class DomainStatus { pending, active, expired, suspended }
enum class DeploymentStatus { pending, building, deploying, active, failed, inactive }
enum class CertificateStatus { pending, issued, expired, revoked }
enum class DnsRecordType { A, AAAA, CNAME, TXT, MX, NS, SRV, CAA }
data class Domain(
val name: String,
val status: DomainStatus,
val owner: String,
val registeredAt: Long,
val expiresAt: Long,
val autoRenew: Boolean,
val records: DomainRecord? = null
)
data class DomainRecord(
val cid: String? = null,
val ipv4: List<String>? = null,
val ipv6: List<String>? = null,
val cname: String? = null,
val txt: List<String>? = null,
val metadata: Map<String, String>? = null
)
data class DomainAvailability(
val name: String,
val available: Boolean,
val price: Double? = null,
val premium: Boolean = false
)
data class RegisterDomainOptions(
val years: Int? = null,
val autoRenew: Boolean? = null
)
data class DnsRecord(
val type: DnsRecordType,
val name: String,
val value: String,
val ttl: Int = 3600,
val priority: Int? = null
)
data class DnsZone(
val domain: String,
val records: List<DnsRecord>,
val updatedAt: Long
)
data class Deployment(
val id: String,
val domain: String,
val cid: String,
val status: DeploymentStatus,
val url: String,
val createdAt: Long,
val updatedAt: Long,
val buildLogs: String? = null,
val errorMessage: String? = null
)
data class DeployOptions(
val domain: String? = null,
val subdomain: String? = null,
val spa: Boolean? = null,
val cleanUrls: Boolean? = null,
val trailingSlash: Boolean? = null
)
data class Certificate(
val domain: String,
val status: CertificateStatus,
val autoRenew: Boolean,
val issuer: String,
val issuedAt: Long? = null,
val expiresAt: Long? = null,
val fingerprint: String? = null
)
data class ProvisionSslOptions(
val includeWww: Boolean? = null,
val autoRenew: Boolean? = null
)
data class SiteConfig(
val domain: String,
val cid: String? = null,
val headers: Map<String, String>? = null,
val redirects: List<RedirectRule>? = null,
val errorPages: Map<String, String>? = null,
val spa: Boolean = false,
val cleanUrls: Boolean = true,
val trailingSlash: Boolean = false
)
data class RedirectRule(
val source: String,
val destination: String,
val statusCode: Int = 301,
val permanent: Boolean = true
)
data class AnalyticsData(
val domain: String,
val period: String,
val pageViews: Long,
val uniqueVisitors: Long,
val bandwidth: Long,
val topPages: List<PageView>,
val topReferrers: List<Referrer>,
val topCountries: List<Country>
)
data class PageView(val path: String, val views: Long)
data class Referrer(val referrer: String, val count: Long)
data class Country(val country: String, val count: Long)
data class AnalyticsOptions(
val period: String? = null,
val start: String? = null,
val end: String? = null
)
internal data class DomainsResponse(val domains: List<Domain>?)
internal data class DeploymentsResponse(val deployments: List<Deployment>?)
class HostingException(
message: String,
val code: String? = null,
val statusCode: Int = 0
) : RuntimeException(message)