diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/DocModels.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/DocModels.scala
new file mode 100644
index 0000000000..a45080eca0
--- /dev/null
+++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/DocModels.scala
@@ -0,0 +1,126 @@
+package io.iohk.atala.agent.server.http
+
+import io.iohk.atala.connect.controller.ConnectionEndpoints
+import sttp.apispec.openapi.*
+import sttp.apispec.{SecurityScheme, Tag}
+import sttp.model.headers.AuthenticationScheme
+
+import scala.collection.immutable.ListMap
+
+object DocModels {
+
+ private val apiKeySecuritySchema = SecurityScheme(
+ `type` = "apiKey",
+ description = Some("API Key Authentication. The header `apikey` must be set with the API key."),
+ name = Some("apikey"),
+ in = Some("header"),
+ scheme = None,
+ bearerFormat = None,
+ flows = None,
+ openIdConnectUrl = None
+ )
+
+ private val adminApiKeySecuritySchema = SecurityScheme(
+ `type` = "apiKey",
+ description =
+ Some("Admin API Key Authentication. The header `x-admin-api-key` must be set with the Admin API key."),
+ name = Some("x-admin-api-key"),
+ in = Some("header"),
+ scheme = None,
+ bearerFormat = None,
+ flows = None,
+ openIdConnectUrl = None
+ )
+
+ private val jwtSecurityScheme = SecurityScheme(
+ `type` = "http",
+ description =
+ Some("JWT Authentication. The header `Authorization` must be set with the JWT token using `Bearer` scheme"),
+ name = Some("Authorization"),
+ in = Some("header"),
+ scheme = Some(AuthenticationScheme.Bearer.name),
+ bearerFormat = None,
+ flows = None,
+ openIdConnectUrl = None
+ )
+
+ val customiseDocsModel: OpenAPI => OpenAPI = { oapi =>
+ oapi
+ .info(
+ Info(
+ title = "Open Enterprise Agent API Reference",
+ version = "1.0", // Will be replaced dynamically by 'Tapir2StaticOAS'
+ summary = Some("Info - Summary"),
+ description = Some("Info - Description"),
+ termsOfService = Some("Info - Terms Of Service"),
+ contact = Some(
+ Contact(
+ name = Some("Contact - Name"),
+ email = Some("Contact - Email"),
+ url = Some("Contact - URL"),
+ extensions = ListMap.empty
+ )
+ ),
+ license = Some(
+ License(
+ name = "License - Name",
+ url = Some("License - URL"),
+ extensions = ListMap.empty
+ )
+ ),
+ extensions = ListMap.empty
+ )
+ )
+ .servers(
+ List(
+ Server(url = "http://localhost:8085", description = Some("Local Prism Agent")),
+ Server(url = "http://localhost/prism-agent", description = Some("Local Prism Agent with APISIX proxy")),
+ Server(
+ url = "https://k8s-dev.atalaprism.io/prism-agent",
+ description = Some("Prism Agent on the Staging Environment")
+ ),
+ )
+ )
+ .components(
+ oapi.components
+ .getOrElse(sttp.apispec.openapi.Components.Empty)
+ .copy(securitySchemes =
+ ListMap(
+ "apiKeyAuth" -> Right(apiKeySecuritySchema),
+ "adminApiKeyAuth" -> Right(adminApiKeySecuritySchema),
+ "jwtAuth" -> Right(jwtSecurityScheme)
+ )
+ )
+ )
+ .addSecurity(
+ ListMap(
+ "apiKeyAuth" -> Vector.empty[String],
+ "adminApiKeyAuth" -> Vector.empty[String],
+ "jwtAuth" -> Vector.empty[String]
+ )
+ )
+ .tags(
+ List(
+ Tag(
+ ConnectionEndpoints.TAG,
+ Some(
+ s"""
+ |The '${ConnectionEndpoints.TAG}' endpoints facilitate the initiation of connection flows between the current agent and peer agents, regardless of whether they reside in cloud or edge environments.
+ |
+ |This implementation adheres to the DIDComm Messaging v2.0 - [Out of Band Messages](https://identity.foundation/didcomm-messaging/spec/v2.0/#out-of-band-messages) specification [section 9.5.4](https://identity.foundation/didcomm-messaging/spec/v2.0/#invitation) - to generate invitations.
+ |The from field of the out-of-band invitation message contains a freshly generated Peer DID that complies with the [did:peer:2](https://identity.foundation/peer-did-method-spec/#generating-a-didpeer2) specification.
+ |This Peer DID includes the 'uri' location of the DIDComm messaging service, essential for the invitee's subsequent execution of the connection flow.
+ |
+ |Upon accepting an invitation, the invitee sends a connection request to the inviter's DIDComm messaging service endpoint.
+ |The connection request's 'type' attribute must be specified as "https://atalaprism.io/mercury/connections/1.0/request".
+ |The inviter agent responds with a connection response message, indicated by a 'type' attribute of "https://atalaprism.io/mercury/connections/1.0/response".
+ |Both request and response types are proprietary to the Open Enterprise Agent ecosystem.
+ |""".stripMargin
+ )
+ )
+ )
+ )
+
+ }
+
+}
diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/ZHttpEndpoints.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/ZHttpEndpoints.scala
index 0cec3ca389..721dcae0a0 100644
--- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/ZHttpEndpoints.scala
+++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/ZHttpEndpoints.scala
@@ -13,84 +13,19 @@ import scala.collection.immutable.ListMap
object ZHttpEndpoints {
- val swaggerUIOptions = SwaggerUIOptions.default
+ private val swaggerUIOptions = SwaggerUIOptions.default
.contextPath(List("docs", "prism-agent", "api"))
- val customiseDocsModel: OpenAPI => OpenAPI = { oapi =>
- oapi
- .servers(
- List(
- Server(url = "http://localhost:8085", description = Some("Local Prism Agent")),
- Server(url = "http://localhost/prism-agent", description = Some("Local Prism Agent with APISIX proxy")),
- Server(
- url = "https://k8s-dev.atalaprism.io/prism-agent",
- description = Some("Prism Agent on the Staging Environment")
- ),
- )
- )
- .components(
- oapi.components
- .getOrElse(sttp.apispec.openapi.Components.Empty)
- .copy(securitySchemes =
- ListMap(
- "apiKeyAuth" -> Right(apiKeySecuritySchema),
- "adminApiKeyAuth" -> Right(adminApiKeySecuritySchema),
- "jwtAuth" -> Right(jwtSecurityScheme)
- )
- )
- )
- .addSecurity(
- ListMap(
- "apiKeyAuth" -> Vector.empty[String],
- "adminApiKeyAuth" -> Vector.empty[String],
- "jwtAuth" -> Vector.empty[String]
- )
- )
-
- }
-
- private val apiKeySecuritySchema = SecurityScheme(
- `type` = "apiKey",
- description = Some("API Key Authentication. The header `apikey` must be set with the API key."),
- name = Some("apikey"),
- in = Some("header"),
- scheme = None,
- bearerFormat = None,
- flows = None,
- openIdConnectUrl = None
- )
-
- private val adminApiKeySecuritySchema = SecurityScheme(
- `type` = "apiKey",
- description =
- Some("Admin API Key Authentication. The header `x-admin-api-key` must be set with the Admin API key."),
- name = Some("x-admin-api-key"),
- in = Some("header"),
- scheme = None,
- bearerFormat = None,
- flows = None,
- openIdConnectUrl = None
- )
-
- private val jwtSecurityScheme = SecurityScheme(
- `type` = "http",
- description =
- Some("JWT Authentication. The header `Authorization` must be set with the JWT token using `Bearer` scheme"),
- name = Some("Authorization"),
- in = Some("header"),
- scheme = Some(AuthenticationScheme.Bearer.name),
- bearerFormat = None,
- flows = None,
- openIdConnectUrl = None
- )
+ private val redocUIOptions = RedocUIOptions.default
+ .copy(pathPrefix = List("redoc"))
def swaggerEndpoints[F[_]](apiEndpoints: List[ServerEndpoint[Any, F]]): List[ServerEndpoint[Any, F]] =
- SwaggerInterpreter(swaggerUIOptions = swaggerUIOptions, customiseDocsModel = customiseDocsModel)
+ SwaggerInterpreter(swaggerUIOptions = swaggerUIOptions, customiseDocsModel = DocModels.customiseDocsModel)
.fromServerEndpoints[F](apiEndpoints, "Prism Agent", "1.0.0")
def redocEndpoints[F[_]](apiEndpoints: List[ServerEndpoint[Any, F]]): List[ServerEndpoint[Any, F]] =
- RedocInterpreter(redocUIOptions = RedocUIOptions.default.copy(pathPrefix = List("redoc")))
- .fromServerEndpoints[F](apiEndpoints, title = "Prism Agent", version = "1.0.0")
+ RedocInterpreter(redocUIOptions = redocUIOptions, customiseDocsModel = DocModels.customiseDocsModel)
+ .fromServerEndpoints[F](apiEndpoints, "Prism Agent", "1.0.0")
def withDocumentations[F[_]](apiEndpoints: List[ServerEndpoint[Any, F]]): List[ServerEndpoint[Any, F]] = {
apiEndpoints ++ swaggerEndpoints[F](apiEndpoints) ++ redocEndpoints[F](apiEndpoints)
diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/api/http/model/PaginationInput.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/api/http/model/PaginationInput.scala
index ed20e9b1e0..ed6d17dba4 100644
--- a/prism-agent/service/server/src/main/scala/io/iohk/atala/api/http/model/PaginationInput.scala
+++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/api/http/model/PaginationInput.scala
@@ -1,17 +1,16 @@
package io.iohk.atala.api.http.model
-import sttp.tapir.EndpointIO.annotations.query
+import sttp.tapir.EndpointIO.annotations.{description, query}
import sttp.tapir.Schema.annotations.validateEach
-import sttp.tapir.Validator
-import sttp.tapir.Schema
-
-import io.iohk.atala.api.http.Annotation
+import sttp.tapir.{Schema, Validator}
case class PaginationInput(
@query
@validateEach(Validator.positiveOrZero[Int])
+ @description("The number of items to skip before returning results. Default is 0 if not specified.")
offset: Option[Int] = None,
@query
@validateEach(Validator.positive[Int])
+ @description("The maximum number of items to return. Defaults to 100 if not specified.")
limit: Option[Int] = None
) {
def toPagination = Pagination.apply(this)
@@ -23,21 +22,6 @@ case class Pagination(offset: Int, limit: Int) {
}
object Pagination {
-
- object annotations {
- object offset
- extends Annotation[Int](
- description = "The number of items to skip before returning results. Default is 0 if not specified",
- example = 0
- )
-
- object limit
- extends Annotation[Int](
- description = "The maximum number of items to return. Defaults to 100 if not specified.",
- example = 100
- )
- }
-
def apply(in: PaginationInput): Pagination =
Pagination(in.offset.getOrElse(0), in.limit.getOrElse(100))
}
diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/connect/controller/ConnectionEndpoints.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/connect/controller/ConnectionEndpoints.scala
index 2c557c56e3..91b611f0a4 100644
--- a/prism-agent/service/server/src/main/scala/io/iohk/atala/connect/controller/ConnectionEndpoints.scala
+++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/connect/controller/ConnectionEndpoints.scala
@@ -21,6 +21,8 @@ import java.util.UUID
object ConnectionEndpoints {
+ val TAG: String = "Connections Management"
+
private val paginationInput: EndpointInput[PaginationInput] = EndpointInput.derived[PaginationInput]
val createConnection: Endpoint[
@@ -47,16 +49,19 @@ object ConnectionEndpoints {
)
)
.out(jsonBody[Connection])
- .description("The created connection record.")
+ .description("The newly created connection record.")
.errorOut(basicFailuresAndForbidden)
.name("createConnection")
- .summary("Creates a new connection record and returns an Out of Band invitation.")
+ .summary("Create a new connection invitation that can be delivered out-of-band to a peer agent.")
.description("""
- |Generates a new Peer DID and creates an [Out of Band 2.0](https://identity.foundation/didcomm-messaging/spec/v2.0/#out-of-band-messages) invitation.
- |It returns a new connection record in `InvitationGenerated` state.
- |The request body may contain a `label` that can be used as a human readable alias for the connection, for example `{'label': "Bob"}`
+ |Create a new connection invitation that can be delivered out-of-band to a peer agent, regardless of whether it resides in cloud or edge environment.
+ |The generated invitation adheres to the DIDComm Messaging v2.0 - [Out of Band Messages](https://identity.foundation/didcomm-messaging/spec/v2.0/#out-of-band-messages) specification [section 9.5.4](https://identity.foundation/didcomm-messaging/spec/v2.0/#invitation).
+ |The from field of the out-of-band invitation message contains a freshly generated Peer DID that complies with the [did:peer:2](https://identity.foundation/peer-did-method-spec/#generating-a-didpeer2) specification.
+ |This Peer DID includes the 'uri' location of the DIDComm messaging service, essential for the invitee's subsequent execution of the connection flow.
+ |In the agent database, the created connection record has an initial state set to `InvitationGenerated`.
+ |The request body may contain a `label` that can be used as a human readable alias for the connection, for example `{'label': "Connection with Bob"}`
|""".stripMargin)
- .tag("Connections Management")
+ .tag(TAG)
val getConnection
: Endpoint[(ApiKeyCredentials, JwtCredentials), (RequestContext, UUID), ErrorResponse, Connection, Any] =
@@ -66,15 +71,21 @@ object ConnectionEndpoints {
.in(extractFromRequest[RequestContext](RequestContext.apply))
.in(
"connections" / path[UUID]("connectionId").description(
- "The unique identifier of the connection record."
+ "The `connectionId` uniquely identifying the connection flow record."
)
)
- .out(jsonBody[Connection].description("The connection record."))
+ .out(jsonBody[Connection].description("The specific connection flow record."))
.errorOut(basicFailureAndNotFoundAndForbidden)
.name("getConnection")
- .summary("Gets an existing connection record by its unique identifier.")
- .description("Gets an existing connection record by its unique identifier")
- .tag("Connections Management")
+ .summary(
+ "Retrieves a specific connection flow record from the agent's database based on its unique `connectionId`."
+ )
+ .description("""
+ |Retrieve a specific connection flow record from the agent's database based in its unique `connectionId`.
+ |The API returns a comprehensive collection of connection flow records within the system, regardless of their state.
+ |The returned connection item includes essential metadata such as connection ID, thread ID, state, role, participant information, and other relevant details.
+ |""".stripMargin)
+ .tag(TAG)
val getConnections: Endpoint[
(ApiKeyCredentials, JwtCredentials),
@@ -89,13 +100,24 @@ object ConnectionEndpoints {
.in(extractFromRequest[RequestContext](RequestContext.apply))
.in("connections")
.in(paginationInput)
- .in(query[Option[String]]("thid").description("The thid of a DIDComm communication."))
- .out(jsonBody[ConnectionsPage].description("The list of connection records."))
+ .in(
+ query[Option[String]]("thid").description(
+ "The `thid`, shared between the inviter and the invitee, that uniquely identifies a connection flow."
+ )
+ )
+ .out(
+ jsonBody[ConnectionsPage].description("The list of connection flow records available from the agent's database")
+ )
.errorOut(basicFailuresAndForbidden)
.name("getConnections")
- .summary("Gets the list of connection records.")
- .description("Get the list of connection records paginated")
- .tag("Connections Management")
+ .summary("Retrieves the list of connection flow records available from the agent's database.")
+ .description("""
+ |Retrieve of a list containing connections available from the agent's database.
+ |The API returns a comprehensive collection of connection flow records within the system, regardless of their state.
+ |Each connection item includes essential metadata such as connection ID, thread ID, state, role, participant information, and other relevant details.
+ |Pagination support is available, allowing for efficient handling of large datasets.
+ |""".stripMargin)
+ .tag(TAG)
val acceptConnectionInvitation: Endpoint[
(ApiKeyCredentials, JwtCredentials),
@@ -121,15 +143,17 @@ object ConnectionEndpoints {
)
)
.out(jsonBody[Connection])
- .description("The created connection record.")
+ .description("The newly connection record.")
.errorOut(basicFailuresAndForbidden)
.name("acceptConnectionInvitation")
- .summary("Accepts an Out of Band invitation.")
+ .summary("Accept a new connection invitation received out-of-band from another peer agent.")
.description("""
- |Accepts an [Out of Band 2.0](https://identity.foundation/didcomm-messaging/spec/v2.0/#out-of-band-messages) invitation, generates a new Peer DID,
- |and submits a Connection Request to the inviter.
- |It returns a connection object in `ConnectionRequestPending` state, until the Connection Request is eventually sent to the inviter by the prism-agent's background process. The connection object state will then automatically move to `ConnectionRequestSent`.
+ |Accept an new connection invitation received out-of-band from another peer agent.
+ |The invitation must be compliant with the DIDComm Messaging v2.0 - [Out of Band Messages](https://identity.foundation/didcomm-messaging/spec/v2.0/#out-of-band-messages) specification [section 9.5.4](https://identity.foundation/didcomm-messaging/spec/v2.0/#invitation).
+ |A new connection record with state `ConnectionRequestPending` will be created in the agent database and later processed by a background job to send a connection request to the peer agent.
+ |The created record will contain a newly generated pairwise Peer DID used for that connection.
+ |A connection request will then be sent to the peer agent to actually establish the connection, moving the record state to `ConnectionRequestSent`, and waiting the connection response from the peer agent.
|""".stripMargin)
- .tag("Connections Management")
+ .tag(TAG)
}
diff --git a/prism-agent/service/server/src/test/scala/io/iohk/atala/api/util/Tapir2StaticOAS.scala b/prism-agent/service/server/src/test/scala/io/iohk/atala/api/util/Tapir2StaticOAS.scala
index 6d07a1bb41..b26bd2fea1 100644
--- a/prism-agent/service/server/src/test/scala/io/iohk/atala/api/util/Tapir2StaticOAS.scala
+++ b/prism-agent/service/server/src/test/scala/io/iohk/atala/api/util/Tapir2StaticOAS.scala
@@ -1,8 +1,9 @@
package io.iohk.atala.api.util
import io.iohk.atala.agent.server.AgentHttpServer
+import io.iohk.atala.agent.server.http.DocModels
import io.iohk.atala.castor.controller.{DIDController, DIDRegistrarController}
-import io.iohk.atala.connect.controller.ConnectionController
+import io.iohk.atala.connect.controller.{ConnectionController, ConnectionEndpoints}
import io.iohk.atala.event.controller.EventController
import io.iohk.atala.iam.authentication.DefaultAuthenticator
import io.iohk.atala.iam.entity.http.controller.EntityController
@@ -13,6 +14,7 @@ import io.iohk.atala.pollux.credentialschema.controller.{CredentialSchemaControl
import io.iohk.atala.presentproof.controller.PresentProofController
import io.iohk.atala.system.controller.SystemController
import org.scalatestplus.mockito.MockitoSugar.*
+import sttp.apispec.Tag
import sttp.tapir.docs.openapi.OpenAPIDocsInterpreter
import zio.{Scope, ZIO, ZIOAppArgs, ZIOAppDefault, ZLayer}
@@ -28,7 +30,8 @@ object Tapir2StaticOAS extends ZIOAppDefault {
allEndpoints <- AgentHttpServer.agentRESTServiceEndpoints
} yield {
import sttp.apispec.openapi.circe.yaml.*
- val yaml = OpenAPIDocsInterpreter().toOpenAPI(allEndpoints.map(_.endpoint), "Prism Agent", args(1)).toYaml
+ val model = DocModels.customiseDocsModel(OpenAPIDocsInterpreter().toOpenAPI(allEndpoints.map(_.endpoint), "", ""))
+ val yaml = model.info(model.info.copy(version = args(1))).toYaml
val path = Path.of(args.head)
Using(Files.newBufferedWriter(path, StandardCharsets.UTF_8)) { writer => writer.write(yaml) }
}