diff --git a/catalyst-gateway/bin/Cargo.toml b/catalyst-gateway/bin/Cargo.toml index 565f0ed396c..39737cc3898 100644 --- a/catalyst-gateway/bin/Cargo.toml +++ b/catalyst-gateway/bin/Cargo.toml @@ -18,6 +18,8 @@ workspace = true cardano-chain-follower = {version = "0.0.6", git = "https://github.com/input-output-hk/catalyst-libs.git", tag="v0.0.10" } c509-certificate = { version = "0.0.3", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "v0.0.3" } rbac-registration = { version = "0.0.2", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "v0.0.8" } +catalyst-signed-doc = { version = "0.0.1", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250116-00" } + pallas = { version = "0.30.1", git = "https://github.com/input-output-hk/catalyst-pallas.git", rev = "9b5183c8b90b90fe2cc319d986e933e9518957b3" } pallas-traverse = { version = "0.30.1", git = "https://github.com/input-output-hk/catalyst-pallas.git", rev = "9b5183c8b90b90fe2cc319d986e933e9518957b3" } diff --git a/catalyst-gateway/bin/src/db/event/signed_docs/full_signed_doc.rs b/catalyst-gateway/bin/src/db/event/signed_docs/full_signed_doc.rs index bf4b9e10529..1bf39a76d63 100644 --- a/catalyst-gateway/bin/src/db/event/signed_docs/full_signed_doc.rs +++ b/catalyst-gateway/bin/src/db/event/signed_docs/full_signed_doc.rs @@ -47,8 +47,8 @@ impl FullSignedDoc { /// Returns the document author. #[allow(dead_code)] - pub(crate) fn author(&self) -> &String { - self.body.author() + pub(crate) fn authors(&self) -> &Vec { + self.body.authors() } /// Returns the `SignedDocBody`. @@ -57,7 +57,15 @@ impl FullSignedDoc { &self.body } + /// Returns the document raw bytes. + #[allow(dead_code)] + pub(crate) fn raw(&self) -> &Vec { + &self.raw + } + /// Uploads a `FullSignedDoc` to the event db. + /// Returns `true` if document was added into the db, `false` if it was already added + /// previously. /// /// Make an insert query into the `event-db` by adding data into the `signed_docs` /// table. @@ -73,21 +81,21 @@ impl FullSignedDoc { /// - `ver` is a UUID v7 /// - `doc_type` is a UUID v4 #[allow(dead_code)] - pub(crate) async fn store(&self) -> anyhow::Result<()> { + pub(crate) async fn store(&self) -> anyhow::Result { match Self::retrieve(self.id(), Some(self.ver())).await { Ok(res_doc) => { anyhow::ensure!( &res_doc == self, "Document with the same `id` and `ver` already exists" ); - return Ok(()); + Ok(false) }, - Err(err) if err.is::() => {}, - Err(err) => return Err(err), + Err(err) if err.is::() => { + EventDB::modify(INSERT_SIGNED_DOCS, &self.postgres_db_fields()).await?; + Ok(true) + }, + Err(err) => Err(err), } - - EventDB::modify(INSERT_SIGNED_DOCS, &self.postgres_db_fields()).await?; - Ok(()) } /// Loads a `FullSignedDoc` from the event db. @@ -146,7 +154,7 @@ impl FullSignedDoc { *id, ver, row.try_get("type")?, - row.try_get("author")?, + row.try_get("authors")?, row.try_get("metadata")?, ), payload: row.try_get("payload")?, diff --git a/catalyst-gateway/bin/src/db/event/signed_docs/query_filter.rs b/catalyst-gateway/bin/src/db/event/signed_docs/query_filter.rs index 9948fc92b48..2f8ca4ab220 100644 --- a/catalyst-gateway/bin/src/db/event/signed_docs/query_filter.rs +++ b/catalyst-gateway/bin/src/db/event/signed_docs/query_filter.rs @@ -13,7 +13,7 @@ pub(crate) enum DocsQueryFilter { DocId(uuid::Uuid), /// Select docs with the specific `id` and `ver` field DocVer(uuid::Uuid, uuid::Uuid), - /// Select docs with the specific `author` field + /// Select docs with the specific `authors` field Author(String), } @@ -26,7 +26,7 @@ impl Display for DocsQueryFilter { Self::DocVer(id, ver) => { write!(f, "signed_docs.id = '{id}' AND signed_docs.ver = '{ver}'") }, - Self::Author(author) => write!(f, "signed_docs.author = '{author}'"), + Self::Author(author) => write!(f, "signed_docs.authors @> '{{ \"{author}\" }}'"), } } } diff --git a/catalyst-gateway/bin/src/db/event/signed_docs/signed_doc_body.rs b/catalyst-gateway/bin/src/db/event/signed_docs/signed_doc_body.rs index df85b1c40a0..1e1c6c8ac67 100644 --- a/catalyst-gateway/bin/src/db/event/signed_docs/signed_doc_body.rs +++ b/catalyst-gateway/bin/src/db/event/signed_docs/signed_doc_body.rs @@ -23,8 +23,8 @@ pub(crate) struct SignedDocBody { ver: uuid::Uuid, /// `signed_doc` table `type` field doc_type: uuid::Uuid, - /// `signed_doc` table `author` field - author: String, + /// `signed_doc` table `authors` field + authors: Vec, /// `signed_doc` table `metadata` field metadata: Option, } @@ -40,9 +40,9 @@ impl SignedDocBody { &self.ver } - /// Returns the document author. - pub(crate) fn author(&self) -> &String { - &self.author + /// Returns the document authors. + pub(crate) fn authors(&self) -> &Vec { + &self.authors } /// Returns all signed document fields for the event db queries @@ -51,22 +51,21 @@ impl SignedDocBody { &self.id, &self.ver, &self.doc_type, - &self.author, + &self.authors, &self.metadata, ] } /// Creates a `SignedDocBody` instance. - #[allow(dead_code)] pub(crate) fn new( - id: uuid::Uuid, ver: uuid::Uuid, doc_type: uuid::Uuid, author: String, + id: uuid::Uuid, ver: uuid::Uuid, doc_type: uuid::Uuid, authors: Vec, metadata: Option, ) -> Self { Self { id, ver, doc_type, - author, + authors, metadata, } } @@ -91,13 +90,13 @@ impl SignedDocBody { let id = row.try_get("id")?; let ver = row.try_get("ver")?; let doc_type = row.try_get("type")?; - let author = row.try_get("author")?; + let authors = row.try_get("authors")?; let metadata = row.try_get("metadata")?; Ok(Self { id, ver, doc_type, - author, + authors, metadata, }) } diff --git a/catalyst-gateway/bin/src/db/event/signed_docs/sql/filtered_select_signed_documents.sql.jinja b/catalyst-gateway/bin/src/db/event/signed_docs/sql/filtered_select_signed_documents.sql.jinja index 3210e1c6dff..eacbfb327d6 100644 --- a/catalyst-gateway/bin/src/db/event/signed_docs/sql/filtered_select_signed_documents.sql.jinja +++ b/catalyst-gateway/bin/src/db/event/signed_docs/sql/filtered_select_signed_documents.sql.jinja @@ -2,7 +2,7 @@ SELECT signed_docs.id, signed_docs.ver, signed_docs.type, - signed_docs.author, + signed_docs.authors, signed_docs.metadata FROM signed_docs WHERE diff --git a/catalyst-gateway/bin/src/db/event/signed_docs/sql/insert_signed_documents.sql b/catalyst-gateway/bin/src/db/event/signed_docs/sql/insert_signed_documents.sql index 61183ea693a..2f69b49efad 100644 --- a/catalyst-gateway/bin/src/db/event/signed_docs/sql/insert_signed_documents.sql +++ b/catalyst-gateway/bin/src/db/event/signed_docs/sql/insert_signed_documents.sql @@ -3,7 +3,7 @@ INSERT INTO signed_docs id, ver, type, - author, + authors, metadata, payload, raw diff --git a/catalyst-gateway/bin/src/db/event/signed_docs/sql/select_signed_documents.sql.jinja b/catalyst-gateway/bin/src/db/event/signed_docs/sql/select_signed_documents.sql.jinja index a1a16a7bcb9..386387fa7a2 100644 --- a/catalyst-gateway/bin/src/db/event/signed_docs/sql/select_signed_documents.sql.jinja +++ b/catalyst-gateway/bin/src/db/event/signed_docs/sql/select_signed_documents.sql.jinja @@ -1,7 +1,7 @@ SELECT {% if not ver %} signed_docs.ver, {% endif %} signed_docs.type, - signed_docs.author, + signed_docs.authors, signed_docs.metadata, signed_docs.payload, signed_docs.raw diff --git a/catalyst-gateway/bin/src/db/event/signed_docs/tests/mod.rs b/catalyst-gateway/bin/src/db/event/signed_docs/tests/mod.rs index 65fd2c01d3f..c3578f1fdb9 100644 --- a/catalyst-gateway/bin/src/db/event/signed_docs/tests/mod.rs +++ b/catalyst-gateway/bin/src/db/event/signed_docs/tests/mod.rs @@ -18,7 +18,7 @@ async fn queries_test() { uuid::Uuid::now_v7(), uuid::Uuid::now_v7(), doc_type, - "Alex".to_string(), + vec!["Alex".to_string()], Some(serde_json::Value::Null), ), Some(serde_json::Value::Null), @@ -29,7 +29,7 @@ async fn queries_test() { uuid::Uuid::now_v7(), uuid::Uuid::now_v7(), doc_type, - "Steven".to_string(), + vec!["Steven".to_string()], Some(serde_json::Value::Null), ), Some(serde_json::Value::Null), @@ -40,7 +40,7 @@ async fn queries_test() { uuid::Uuid::now_v7(), uuid::Uuid::now_v7(), doc_type, - "Sasha".to_string(), + vec!["Sasha".to_string()], None, ), None, @@ -49,12 +49,18 @@ async fn queries_test() { ]; for doc in &docs { - doc.store().await.unwrap(); + assert!(doc.store().await.unwrap()); // try to insert the same data again - doc.store().await.unwrap(); + assert!(!doc.store().await.unwrap()); // try another doc with the same `id` and `ver` and with different other fields let another_doc = FullSignedDoc::new( - SignedDocBody::new(*doc.id(), *doc.ver(), doc_type, "Neil".to_string(), None), + SignedDocBody::new( + *doc.id(), + *doc.ver(), + doc_type, + vec!["Neil".to_string()], + None, + ), None, vec![], ); @@ -87,7 +93,7 @@ async fn queries_test() { assert!(res_docs.try_next().await.unwrap().is_none()); let mut res_docs = SignedDocBody::retrieve( - &DocsQueryFilter::Author(doc.author().clone()), + &DocsQueryFilter::Author(doc.authors().first().unwrap().clone()), &QueryLimits::ALL, ) .await diff --git a/catalyst-gateway/bin/src/service/api/documents/get_document.rs b/catalyst-gateway/bin/src/service/api/documents/get_document.rs index 00c5ba4b9ab..f654dfcd804 100644 --- a/catalyst-gateway/bin/src/service/api/documents/get_document.rs +++ b/catalyst-gateway/bin/src/service/api/documents/get_document.rs @@ -1,9 +1,11 @@ //! Implementation of the GET `/document` endpoint -use poem::Body; use poem_openapi::ApiResponse; -use crate::service::common::{responses::WithErrorResponses, types::payload::cbor::Cbor}; +use crate::{ + db::event::{error::NotFoundError, signed_docs::FullSignedDoc}, + service::common::{responses::WithErrorResponses, types::payload::cbor::Cbor}, +}; /// Endpoint responses. #[derive(ApiResponse)] @@ -13,7 +15,7 @@ pub(crate) enum Responses { /// /// The Document that was requested. #[oai(status = 200)] - Ok(Cbor), + Ok(Cbor>), /// ## Not Found /// /// The document could not be found. @@ -27,8 +29,9 @@ pub(crate) type AllResponses = WithErrorResponses; /// # GET `/document` #[allow(clippy::unused_async, clippy::no_effect_underscore_binding)] pub(crate) async fn endpoint(document_id: uuid::Uuid, version: Option) -> AllResponses { - let _doc = document_id; - let _ver = version; - - Responses::NotFound.into() + match FullSignedDoc::retrieve(&document_id, version.as_ref()).await { + Ok(doc) => Responses::Ok(Cbor(doc.raw().clone())).into(), + Err(err) if err.is::() => Responses::NotFound.into(), + Err(err) => AllResponses::handle_error(&err), + } } diff --git a/catalyst-gateway/bin/src/service/api/documents/mod.rs b/catalyst-gateway/bin/src/service/api/documents/mod.rs index 5714232ac31..3d005a06e60 100644 --- a/catalyst-gateway/bin/src/service/api/documents/mod.rs +++ b/catalyst-gateway/bin/src/service/api/documents/mod.rs @@ -12,10 +12,15 @@ use put_document::{bad_put_request::PutDocumentBadRequest, MAXIMUM_DOCUMENT_SIZE use crate::service::{ common::{ - self, - auth::{none_or_rbac::NoneOrRBAC, rbac::scheme::CatalystRBACSecurityScheme}, + auth::rbac::scheme::CatalystRBACSecurityScheme, tags::ApiTags, - types::{generic::uuidv7::UUIDv7, payload::cbor::Cbor}, + types::{ + generic::{ + query::pagination::{Limit, Page}, + uuidv7::UUIDv7, + }, + payload::cbor::Cbor, + }, }, utilities::middleware::schema_validation::schema_version_validation, }; @@ -46,7 +51,7 @@ impl DocumentApi { /// version. version: Query>, /// No Authorization required, but Token permitted. - _auth: NoneOrRBAC, + _auth: CatalystRBACSecurityScheme, ) -> get_document::AllResponses { let Ok(doc_id) = document_id.0.try_into() else { let err = anyhow!("Invalid UUIDv7"); // Should not happen as UUIDv7 is validating. @@ -76,9 +81,9 @@ impl DocumentApi { _auth: CatalystRBACSecurityScheme, ) -> put_document::AllResponses { match document.0.into_bytes_limit(MAXIMUM_DOCUMENT_SIZE).await { - Ok(document) => put_document::endpoint(document).await, + Ok(doc_bytes) => put_document::endpoint(doc_bytes.to_vec()).await, Err(ReadBodyError::PayloadTooLarge) => put_document::Responses::PayloadTooLarge.into(), - Err(_err) => { + Err(_) => { put_document::Responses::BadRequest(Json(PutDocumentBadRequest::new( "Failed to read document from the request", ))) @@ -103,8 +108,7 @@ impl DocumentApi { async fn post_document( &self, /// The Query Filter Specification query: Json, - page: Query>, - limit: Query>, + page: Query>, limit: Query>, /// Authorization required. _auth: CatalystRBACSecurityScheme, ) -> post_document_index_query::AllResponses { diff --git a/catalyst-gateway/bin/src/service/api/documents/post_document_index_query/mod.rs b/catalyst-gateway/bin/src/service/api/documents/post_document_index_query/mod.rs index 26ca8c004ac..7b70aa58eb4 100644 --- a/catalyst-gateway/bin/src/service/api/documents/post_document_index_query/mod.rs +++ b/catalyst-gateway/bin/src/service/api/documents/post_document_index_query/mod.rs @@ -4,7 +4,7 @@ use poem_openapi::{payload::Json, ApiResponse, Object}; use query_filter::DocumentIndexQueryFilter; use response::DocumentIndexListDocumented; -use super::common; +use super::{Limit, Page}; use crate::service::common::responses::WithErrorResponses; pub(crate) mod query_filter; @@ -39,9 +39,7 @@ pub(crate) struct QueryDocumentIndex { /// # POST `/document/index` #[allow(clippy::unused_async, clippy::no_effect_underscore_binding)] pub(crate) async fn endpoint( - filter: DocumentIndexQueryFilter, - page: Option, - limit: Option, + filter: DocumentIndexQueryFilter, page: Option, limit: Option, ) -> AllResponses { let _filter = filter; let _page = page; diff --git a/catalyst-gateway/bin/src/service/api/documents/post_document_index_query/query_filter.rs b/catalyst-gateway/bin/src/service/api/documents/post_document_index_query/query_filter.rs index 7c82ae2d859..5ad0f38cd36 100644 --- a/catalyst-gateway/bin/src/service/api/documents/post_document_index_query/query_filter.rs +++ b/catalyst-gateway/bin/src/service/api/documents/post_document_index_query/query_filter.rs @@ -5,9 +5,9 @@ use poem_openapi::{types::Example, NewType, Object}; -use super::common::types::document::ver::EqOrRangedVerDocumented; use crate::service::common::types::document::{ doc_ref::IdAndVerRefDocumented, doc_type::DocumentType, id::EqOrRangedIdDocumented, + ver::EqOrRangedVerDocumented, }; /// Query Filter for the generation of a signed document index. diff --git a/catalyst-gateway/bin/src/service/api/documents/put_document/bad_put_request.rs b/catalyst-gateway/bin/src/service/api/documents/put_document/bad_put_request.rs index 5d3bad1903a..99e0a5d79d5 100644 --- a/catalyst-gateway/bin/src/service/api/documents/put_document/bad_put_request.rs +++ b/catalyst-gateway/bin/src/service/api/documents/put_document/bad_put_request.rs @@ -14,9 +14,9 @@ pub(crate) struct PutDocumentBadRequest { impl PutDocumentBadRequest { /// Create a new instance of `ConfigBadRequest`. - pub(crate) fn new(error: &str) -> Self { + pub(crate) fn new(error: &(impl ToString + ?Sized)) -> Self { Self { - error: error.to_owned(), + error: error.to_string(), } } } diff --git a/catalyst-gateway/bin/src/service/api/documents/put_document/mod.rs b/catalyst-gateway/bin/src/service/api/documents/put_document/mod.rs index ddef4f7fa65..0102ec4cd87 100644 --- a/catalyst-gateway/bin/src/service/api/documents/put_document/mod.rs +++ b/catalyst-gateway/bin/src/service/api/documents/put_document/mod.rs @@ -1,10 +1,14 @@ //! Implementation of the PUT `/document` endpoint +use anyhow::anyhow; use bad_put_request::PutDocumentBadRequest; -use bytes::Bytes; +use catalyst_signed_doc::{CatalystSignedDocument, Decode, Decoder}; use poem_openapi::{payload::Json, ApiResponse}; -use crate::service::common::responses::WithErrorResponses; +use crate::{ + db::event::signed_docs::{FullSignedDoc, SignedDocBody}, + service::common::responses::WithErrorResponses, +}; pub(crate) mod bad_put_request; @@ -42,9 +46,61 @@ pub(crate) enum Responses { pub(crate) type AllResponses = WithErrorResponses; /// # PUT `/document` -#[allow(clippy::unused_async, clippy::no_effect_underscore_binding)] -pub(crate) async fn endpoint(document: Bytes) -> AllResponses { - let _doc = document; +#[allow(clippy::no_effect_underscore_binding)] +pub(crate) async fn endpoint(doc_bytes: Vec) -> AllResponses { + match CatalystSignedDocument::decode(&mut Decoder::new(&doc_bytes), &mut ()) { + Ok(doc) => { + let authors = doc + .signatures() + .kids() + .into_iter() + .map(|kid| kid.to_string()) + .collect(); - Responses::BadRequest(Json(PutDocumentBadRequest::new("unimplemented"))).into() + let doc_meta_json = match serde_json::to_value(doc.doc_meta()) { + Ok(json) => json, + Err(e) => { + return AllResponses::internal_error(&anyhow!( + "Cannot decode document metadata into JSON, err: {e}" + )) + }, + }; + + let doc_body = SignedDocBody::new( + doc.doc_id(), + doc.doc_ver(), + doc.doc_type(), + authors, + Some(doc_meta_json), + ); + + let payload = if doc.doc_content().is_json() { + match serde_json::from_slice(doc.doc_content().bytes()) { + Ok(payload) => Some(payload), + Err(e) => { + return AllResponses::internal_error(&anyhow!( + "Invalid Document Content, not Json encoded: {e}" + )) + }, + } + } else { + None + }; + + match FullSignedDoc::new(doc_body, payload, doc_bytes) + .store() + .await + { + Ok(true) => Responses::Created.into(), + Ok(false) => Responses::NoContent.into(), + Err(err) => AllResponses::handle_error(&err), + } + }, + Err(_) => { + Responses::BadRequest(Json(PutDocumentBadRequest::new( + "Invalid CBOR encoded document", + ))) + .into() + }, + } } diff --git a/catalyst-gateway/bin/src/service/common/auth/rbac/scheme.rs b/catalyst-gateway/bin/src/service/common/auth/rbac/scheme.rs index 0489a5b9721..154814be49b 100644 --- a/catalyst-gateway/bin/src/service/common/auth/rbac/scheme.rs +++ b/catalyst-gateway/bin/src/service/common/auth/rbac/scheme.rs @@ -48,8 +48,7 @@ static CERTS: LazyLock> = LazyLock::new bearer_format = "catalyst-rbac-token", checker = "checker_api_catalyst_auth" )] -#[allow(clippy::module_name_repetitions)] -#[allow(dead_code)] +#[allow(dead_code, clippy::module_name_repetitions)] pub struct CatalystRBACSecurityScheme(pub CatalystRBACTokenV1); /// Error with the Authorization Token diff --git a/catalyst-gateway/event-db/migrations/V2__signed_documents.sql b/catalyst-gateway/event-db/migrations/V2__signed_documents.sql index 28ff361d4a3..2dc073ec812 100644 --- a/catalyst-gateway/event-db/migrations/V2__signed_documents.sql +++ b/catalyst-gateway/event-db/migrations/V2__signed_documents.sql @@ -15,7 +15,7 @@ CREATE TABLE IF NOT EXISTS signed_docs ( id UUID NOT NULL, -- UUID v7 ver UUID NOT NULL, -- UUID v7 type UUID NOT NULL, -- UUID v4 - author TEXT NOT NULL, + authors TEXT [] NOT NULL, metadata JSONB NULL, payload JSONB NULL, raw BYTEA NOT NULL, @@ -32,8 +32,8 @@ COMMENT ON COLUMN signed_docs.ver IS 'The Signed Documents Document Version Number (ULID).'; COMMENT ON COLUMN signed_docs.type IS 'The Signed Document type identifier.'; -COMMENT ON COLUMN signed_docs.author IS -'The Primary Author of the Signed Document.'; +COMMENT ON COLUMN signed_docs.authors IS +'The Primary Author`s list of the Signed Document.'; COMMENT ON COLUMN signed_docs.metadata IS 'Extra metadata extracted from the Signed Document, and encoded as JSON.'; COMMENT ON COLUMN signed_docs.payload IS @@ -45,13 +45,13 @@ CREATE INDEX IF NOT EXISTS idx_signed_docs_type ON signed_docs (type); COMMENT ON INDEX idx_signed_docs_type IS 'Index to help finding documents by a known type faster.'; -CREATE INDEX IF NOT EXISTS idx_signed_docs_author ON signed_docs (author); -COMMENT ON INDEX idx_signed_docs_author IS -'Index to help finding documents by a known author faster.'; +CREATE INDEX IF NOT EXISTS idx_signed_docs_authors ON signed_docs (authors); +COMMENT ON INDEX idx_signed_docs_authors IS +'Index to help finding documents by a known authors faster.'; -CREATE INDEX IF NOT EXISTS idx_signed_docs_type_author ON signed_docs (type, author); -COMMENT ON INDEX idx_signed_docs_type_author IS -'Index to help finding documents by a known author for a specific document type faster.'; +CREATE INDEX IF NOT EXISTS idx_signed_docs_type_authors ON signed_docs (type, authors); +COMMENT ON INDEX idx_signed_docs_type_authors IS +'Index to help finding documents by a known authors for a specific document type faster.'; CREATE INDEX IF NOT EXISTS idx_signed_docs_metadata ON signed_docs USING gin (metadata); diff --git a/catalyst-gateway/tests/api_tests/api_tests/put_document.hurl b/catalyst-gateway/tests/api_tests/api_tests/put_document.hurl new file mode 100644 index 00000000000..6e058e87fc2 --- /dev/null +++ b/catalyst-gateway/tests/api_tests/api_tests/put_document.hurl @@ -0,0 +1,9 @@ +PUT http://localhost:3030/api/draft/document +Content-Type: application/cbor +hex, 84585da503183270436f6e74656e742d456e636f64696e676262726474797065d82550913c9265f9f944fcb3cf9d9516ae9baf626964d8255001946ea1818a7e0eb6b16169f02ffd4e63766572d82550913c9265f9f944fcb3cf9d9516ae9bafa0581c8b0b807b22616765223a32392c226e616d65223a22416c6578227d0380; +HTTP 201 + +PUT http://localhost:3030/api/draft/document +Content-Type: application/cbor +hex, 84585da503183270436f6e74656e742d456e636f64696e676262726474797065d82550913c9265f9f944fcb3cf9d9516ae9baf626964d8255001946ea1818a7e0eb6b16169f02ffd4e63766572d82550913c9265f9f944fcb3cf9d9516ae9bafa0581c8b0b807b22616765223a32392c226e616d65223a22416c6578227d0380; +HTTP 204 \ No newline at end of file