Skip to content

Commit b8ae1b9

Browse files
committed
Misc Stargate updates
1 parent f7429aa commit b8ae1b9

File tree

8 files changed

+238
-12
lines changed

8 files changed

+238
-12
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.mooncloak.vpn.component.stargate.entanglement
2+
3+
import kotlinx.serialization.SerialName
4+
import kotlinx.serialization.Serializable
5+
6+
/**
7+
* Represents information about the contact. Not to be confused with Profile which is Identity defined, this Contact
8+
* model is user defined.
9+
*/
10+
@Serializable
11+
public data class Contact public constructor(
12+
@SerialName(value = "name") public val name: String
13+
)
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package com.mooncloak.vpn.component.stargate.entanglement
2+
3+
import kotlinx.serialization.KSerializer
4+
import kotlinx.serialization.SerialName
5+
import kotlinx.serialization.Serializable
6+
import kotlinx.serialization.descriptors.SerialDescriptor
7+
import kotlinx.serialization.encoding.Decoder
8+
import kotlinx.serialization.encoding.Encoder
9+
import kotlinx.serialization.json.JsonElement
10+
import kotlinx.serialization.json.JsonObject
11+
12+
/**
13+
* Represents a DID Document.
14+
*
15+
* > [!Note]
16+
* > This is an interface and not a serializable data class because the DID Document can be in different formats
17+
* > (JSON, CBOR, YAML, etc.).
18+
*/
19+
@Serializable(with = DIDDocumentSerializer::class)
20+
public interface DIDDocument {
21+
22+
public val id: String
23+
24+
public val alsoKnownAs: List<String>
25+
26+
public val verificationMethod: List<VerificationMethod>
27+
28+
public val keyAgreement: List<VerificationMethod>
29+
30+
public val service: List<Service>
31+
32+
public companion object
33+
}
34+
35+
/**
36+
* Represents a verification method (e.g., public key).
37+
*/
38+
@Serializable
39+
public data class VerificationMethod public constructor(
40+
@SerialName(value = "id") public val id: String,
41+
@SerialName(value = "type") public val type: String,
42+
@SerialName(value = "controller") public val controller: String,
43+
@SerialName(value = "publicKeyBase58") public val publicKeyBase58: String? = null,
44+
@SerialName(value = "publicKeyJwk") public val publicKeyJwk: JsonObject? = null, // TODO: Is this a JSON object or a String? How can we represent this in non-JSON?
45+
@SerialName(value = "publicKeyMultibase") public val publicKeyMultibase: String? = null
46+
)
47+
48+
/**
49+
* Represents a service endpoint.
50+
*/
51+
@Serializable
52+
public data class Service public constructor(
53+
@SerialName(value = "id") public val id: String,
54+
@SerialName(value = "type") public val type: String,
55+
@SerialName(value = "serviceEndpoint") public val serviceEndpoint: String // Simplified; extend to JsonElement if needed
56+
)
57+
58+
@Serializable
59+
internal data class JsonDIDDocument internal constructor(
60+
@SerialName(value = "id") override val id: String,
61+
@SerialName(value = "alsoKnownAs") override val alsoKnownAs: List<String> = emptyList(),
62+
@SerialName(value = "verificationMethod") override val verificationMethod: List<VerificationMethod> = emptyList(),
63+
@SerialName(value = "keyAgreement") override val keyAgreement: List<VerificationMethod> = emptyList(),
64+
@SerialName(value = "service") override val service: List<Service> = emptyList()
65+
) : DIDDocument
66+
67+
// TODO: Support other formats like CBOR
68+
internal object DIDDocumentSerializer : KSerializer<DIDDocument> {
69+
70+
override val descriptor: SerialDescriptor = JsonDIDDocument.serializer().descriptor
71+
72+
override fun serialize(encoder: Encoder, value: DIDDocument) {
73+
val delegate = if (value is JsonDIDDocument) {
74+
value
75+
} else {
76+
JsonDIDDocument(
77+
id = value.id,
78+
alsoKnownAs = value.alsoKnownAs,
79+
verificationMethod = value.verificationMethod,
80+
keyAgreement = value.keyAgreement,
81+
service = value.service
82+
)
83+
}
84+
85+
encoder.encodeSerializableValue(
86+
serializer = JsonDIDDocument.serializer(),
87+
value = delegate
88+
)
89+
}
90+
91+
override fun deserialize(decoder: Decoder): DIDDocument =
92+
decoder.decodeSerializableValue(deserializer = JsonDIDDocument.serializer())
93+
}

component-stargate/src/commonMain/kotlin/com/mooncloak/vpn/component/stargate/entanglement/DIDDocumentResolver.kt

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,20 @@ package com.mooncloak.vpn.component.stargate.entanglement
33
import io.ktor.client.HttpClient
44
import io.ktor.client.call.body
55
import io.ktor.client.request.get
6-
import kotlinx.serialization.json.JsonObject
7-
import kotlinx.serialization.json.contentOrNull
8-
import kotlinx.serialization.json.jsonPrimitive
96

107
public class DIDDocumentResolver public constructor(
118
private val httpClient: HttpClient
129
) {
1310

14-
public suspend fun resolve(did: DID): JsonObject =
11+
public suspend fun resolve(did: DID): DIDDocument =
1512
when (val method = did.method) {
1613
"web" -> resolveDidWeb(did)
1714
"plc" -> resolveDidPlc(did)
1815
// TODO: Possibly support other well known DID methods.
1916
else -> error("Unsupported DID method '$method'")
2017
}
2118

22-
private suspend fun resolveDidWeb(did: DID): JsonObject {
19+
private suspend fun resolveDidWeb(did: DID): DIDDocument {
2320
val identifier = did.id ?: error("DID id part was required but was missing.")
2421

2522
// Convert identifier to URL (e.g., chris.keenan -> https://chris.keenan/.well-known/did.json)
@@ -28,35 +25,35 @@ public class DIDDocumentResolver public constructor(
2825
val fallbackUrl = "https://$domain/did.json"
2926

3027
return try {
31-
val document: JsonObject = httpClient.get(wellKnownUrl).body()
28+
val document: DIDDocument = httpClient.get(wellKnownUrl).body()
3229

3330
document.validateDidDocument(did)
3431

3532
document
3633
} catch (e: Exception) {
3734
// Try alternative path (e.g., https://chris.keenan/did.json)
38-
val document: JsonObject = httpClient.get(fallbackUrl).body()
35+
val document: DIDDocument = httpClient.get(fallbackUrl).body()
3936

4037
document.validateDidDocument(did)
4138

4239
document
4340
}
4441
}
4542

46-
private suspend fun resolveDidPlc(did: DID): JsonObject {
43+
private suspend fun resolveDidPlc(did: DID): DIDDocument {
4744
// Query PLC directory (e.g., https://plc.directory/did:plc:abc123)
4845
val url = "https://plc.directory/${did.value}"
4946

50-
val document: JsonObject = httpClient.get(url).body()
47+
val document: DIDDocument = httpClient.get(url).body()
5148

5249
document.validateDidDocument(did)
5350

5451
return document
5552
}
5653

57-
private fun JsonObject.validateDidDocument(did: DID) {
58-
if (this["id"]?.jsonPrimitive?.contentOrNull != did.value) {
59-
error("DID does not match.")
54+
private fun DIDDocument.validateDidDocument(did: DID) {
55+
if (this.id != did.value) {
56+
error("DID document id '${this.id}' does not match expected DID '${did.value}'.")
6057
}
6158
}
6259
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.mooncloak.vpn.component.stargate.entanglement
2+
3+
import kotlinx.serialization.SerialName
4+
import kotlinx.serialization.Serializable
5+
6+
@Serializable
7+
public data class Identity public constructor(
8+
@SerialName(value = "did") public val did: DID,
9+
@SerialName(value = "document") public val document: DIDDocument,
10+
@SerialName(value = "handle") public val handle: IdentityHandle? = null,
11+
@SerialName(value = "profile") public val profile: Profile? = null,
12+
@SerialName(value = "contact") public val contact: Contact? = null
13+
)
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package com.mooncloak.vpn.component.stargate.entanglement
2+
3+
import com.mooncloak.vpn.util.shared.validation.fromOrNull
4+
import kotlin.coroutines.cancellation.CancellationException
5+
6+
public interface IdentityResolver {
7+
8+
@Throws(NoSuchElementException::class, IllegalArgumentException::class, CancellationException::class)
9+
public suspend fun resolve(did: DID): Identity
10+
11+
@Throws(NoSuchElementException::class, IllegalArgumentException::class, CancellationException::class)
12+
public suspend fun resolve(handle: IdentityHandle): Identity
13+
14+
@Throws(NoSuchElementException::class, IllegalArgumentException::class, CancellationException::class)
15+
public suspend fun resolve(value: String): Identity {
16+
val did = DID.fromOrNull(value)
17+
18+
if (did != null) {
19+
return resolve(did = did)
20+
}
21+
22+
val handle = IdentityHandle.fromOrNull(value)
23+
24+
if (handle != null) {
25+
return resolve(handle = handle)
26+
}
27+
28+
throw IllegalArgumentException("Invalid DID or Handle '$value'.")
29+
}
30+
31+
public companion object
32+
}
33+
34+
public suspend fun IdentityResolver.resolveOrNull(did: DID): Identity? =
35+
try {
36+
resolve(did = did)
37+
} catch (_: NoSuchElementException) {
38+
null
39+
} catch (_: IllegalArgumentException) {
40+
null
41+
}
42+
43+
public suspend fun IdentityResolver.resolveOrNull(handle: IdentityHandle): Identity? =
44+
try {
45+
resolve(handle = handle)
46+
} catch (_: NoSuchElementException) {
47+
null
48+
} catch (_: IllegalArgumentException) {
49+
null
50+
}
51+
52+
public suspend fun IdentityResolver.resolveOrNull(value: String): Identity? =
53+
try {
54+
resolve(value = value)
55+
} catch (_: NoSuchElementException) {
56+
null
57+
} catch (_: IllegalArgumentException) {
58+
null
59+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.mooncloak.vpn.component.stargate.entanglement
2+
3+
import androidx.compose.ui.text.AnnotatedString
4+
import com.mooncloak.kodetools.compose.serialization.AnnotatedStringSerializer
5+
import kotlinx.serialization.SerialName
6+
import kotlinx.serialization.Serializable
7+
8+
@Serializable
9+
public data class Profile public constructor(
10+
@SerialName(value = "display_name") public val displayName: String? = null,
11+
@SerialName(value = "description") @Serializable(with = AnnotatedStringSerializer::class) public val description: AnnotatedString? = null,
12+
@SerialName(value = "images") public val images: ProfileImages? = null,
13+
@SerialName(value = "links") public val links: List<ProfileLink> = emptyList()
14+
)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.mooncloak.vpn.component.stargate.entanglement
2+
3+
import com.mooncloak.vpn.component.stargate.message.model.ImageContent
4+
import kotlinx.serialization.SerialName
5+
import kotlinx.serialization.Serializable
6+
7+
@Serializable
8+
public data class ProfileImages public constructor(
9+
@SerialName(value = "avatar") public val avatar: ImageContent? = null,
10+
@SerialName(value = "banner") public val banner: ImageContent? = null
11+
)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.mooncloak.vpn.component.stargate.entanglement
2+
3+
import kotlinx.serialization.SerialName
4+
import kotlinx.serialization.Serializable
5+
6+
/**
7+
* Represents a link, or URI, and information about the link.
8+
*
9+
* @property [uri] The URI [String].
10+
*
11+
* @property [title] An optional title value that should be displayed in the UI, instead of the [uri] value. Defaults
12+
* to `null`.
13+
*
14+
* @property [description] An optional description value, that should be displayed in the UI, that provides extra
15+
* context about the link. Defaults to `null`.
16+
*
17+
* @property [icon] An optional URI [String] pointing to an icon to display for this link in the UI. Defaults to
18+
* `null`.
19+
*/
20+
@Serializable
21+
public data class ProfileLink public constructor(
22+
@SerialName(value = "uri") public val uri: String,
23+
@SerialName(value = "title") public val title: String? = null,
24+
@SerialName(value = "description") public val description: String? = null,
25+
@SerialName(value = "icon") public val icon: String? = null
26+
)

0 commit comments

Comments
 (0)