Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,18 @@ The high-level API covers operations including:

The generic `invoke()` API remains available for compatible QKMS operations that do not yet have a dedicated facade function.

For MPC-backed key creation, prefer `create_key_async()` when the ceremony can outlive an HTTP proxy/CDN timeout. It sends the QKMS-compatible `CreateKey` request with the signed `?async=1` query so QKMS can acknowledge the task immediately while DKG continues asynchronously:

```cpp
const auto created = quilibrium::sync_wait(
sdk->kms().create_key_async(
R"({"KeySpec":"ECC_SECG_P256K1","KeyUsage":"SIGN_VERIFY"})"
)
);
```

This SDK intentionally covers the signed QKMS HTTP/KMS surface. The QNZM login flow and participant-side MPC sidecar/ceremony orchestration are a separate concern; applications that need browser/server sidecars participating in DKG should use the official Quilibrium QKMS SDK for that layer.

---

# Native Quilibrium Protocol
Expand Down
2 changes: 1 addition & 1 deletion docs/SERVICE_MATRIX.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
| HyperSnap casts | `quilibrium.sdk`, `quilibrium.hypersnap` | Typed get/search, conversation JSON + raw API | Farcaster writes use signed protocol messages |
| HyperSnap feeds | `quilibrium.sdk`, `quilibrium.hypersnap` | Following/trending/user-casts typed pages | Additional documented paths remain available through raw GET |
| QStorage | `quilibrium.sdk`, `quilibrium.qstorage` | Header SigV4, presigned PUT/GET/HEAD URLs, signed-header contracts, bucket/object common ops, copy/head/list, multipart create/upload/complete/abort/list, raw execute/presign | XML is returned raw; full presigned multipart orchestration can build on generic arbitrary-target presigning |
| QKMS | `quilibrium.sdk`, `quilibrium.qkms` | SigV4 generic invoke + named key/crypto/data-key/MAC/import/policy/alias/grant/tag/rotation/replication/deletion methods | QNZM/MPC sidecar login/session workflow is separate from the KMS-compatible API |
| QKMS | `quilibrium.sdk`, `quilibrium.qkms` | SigV4 generic invoke + named key/crypto/data-key/MAC/import/policy/alias/grant/tag/rotation/replication/deletion methods + async `CreateKey` (`?async=1`) | QNZM/MPC sidecar login/session and participant ceremony workflow are separate from the KMS-compatible API |
| NodeService | `quilibrium.sdk`, `quilibrium.protocol` | Unary raw-protobuf call + complete registry | Typed generated protobuf wrapper not bundled |
| ConnectivityService | same | Unary raw-protobuf + registry | — |
| GlobalService | same | Unary methods callable; stream method registered | Server streaming execution not implemented in v1.1 |
Expand Down
8 changes: 5 additions & 3 deletions src/qkms.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,16 @@ client::~client()=default;
client::client(client&&) noexcept=default;
client& client::operator=(client&&) noexcept=default;

task<result<response>> client::invoke(std::string operation,bytes payload,call_options options) {
task<result<response>> client::invoke(std::string operation,bytes payload,call_options options,std::string query) {
if(!impl_->transport) co_return std::unexpected(error{.domain=error_domain::configuration,.code=400,.message="HTTP transport is not configured"});
if(impl_->endpoints.empty()) co_return std::unexpected(error{.domain=error_domain::configuration,.code=401,.message="QKMS endpoint list is empty"});

const auto attempts = std::max<std::uint32_t>(1U, options.max_attempts);
error last_error{.domain=error_domain::transport,.code=402,.message="QKMS request failed",.retryable=true};
const std::string target=query.empty()?"/":"/?"+query;
for(std::uint32_t attempt=0; attempt<attempts; ++attempt) {
const auto& selected=impl_->endpoints[attempt % impl_->endpoints.size()];
http_request request{.verb=http_method::post,.target_endpoint=selected,.target="/",.header_fields={{"content-type","application/x-amz-json-1.1"},{"x-amz-target",impl_->target_prefix+"."+operation}},.body=payload};
http_request request{.verb=http_method::post,.target_endpoint=selected,.target=target,.header_fields={{"content-type","application/json"},{"x-amz-target",impl_->target_prefix+"."+operation}},.body=payload};
if(auto status=impl_->signer.sign(request);!status) co_return std::unexpected(status.error());
auto raw=impl_->transport->send_now(std::move(request),options);
if(!raw) {
Expand All @@ -53,7 +54,8 @@ task<result<response>> client::invoke(std::string operation,bytes payload,call_o
}

#define QL_KMS_FORWARD(name,operation) task<result<response>> client::name(bytes p,call_options o){co_return sync_wait(invoke(operation,std::move(p),o));}
QL_KMS_FORWARD(create_key,"CreateKey")
task<result<response>> client::create_key(bytes p,call_options o){o.idempotent=false;co_return sync_wait(invoke("CreateKey",std::move(p),o));}
task<result<response>> client::create_key_async(bytes p,call_options o){o.idempotent=false;co_return sync_wait(invoke("CreateKey",std::move(p),o,"async=1"));}
QL_KMS_FORWARD(describe_key,"DescribeKey")
QL_KMS_FORWARD(enable_key,"EnableKey")
QL_KMS_FORWARD(disable_key,"DisableKey")
Expand Down
6 changes: 5 additions & 1 deletion src/qkms.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@ class client final {
public:
client(config configuration, http_transport_ptr transport);
~client(); client(client&&) noexcept; client& operator=(client&&) noexcept; client(const client&)=delete; client& operator=(const client&)=delete;
[[nodiscard]] task<result<response>> invoke(std::string operation, bytes json_payload, call_options options = {});
/** Invokes a QKMS operation. `query` is the raw query string without a leading `?` and is included in SigV4 canonicalization. */
[[nodiscard]] task<result<response>> invoke(std::string operation, bytes json_payload, call_options options = {}, std::string query = {});
/** Blocking CreateKey; waits for QKMS to complete the MPC operation. */
[[nodiscard]] task<result<response>> create_key(bytes payload, call_options options = {});
/** Non-blocking CreateKey (`?async=1`); avoids proxy/CDN 504s while MPC/DKG continues asynchronously. */
[[nodiscard]] task<result<response>> create_key_async(bytes payload, call_options options = {});
[[nodiscard]] task<result<response>> describe_key(bytes payload, call_options options = {});
[[nodiscard]] task<result<response>> list_keys(bytes payload = {}, call_options options = {});
[[nodiscard]] task<result<response>> enable_key(bytes payload, call_options options = {});
Expand Down
23 changes: 21 additions & 2 deletions src/sdk_facade.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -636,13 +636,32 @@ task<result<service_response>> kms_api::invoke(std::string operation,std::string
if (!state) co_return std::unexpected(error{.domain=error_domain::configuration,.code=850,.message="SDK state is unavailable"});
auto view=as_bytes(json_payload);
bytes payload(view.begin(),view.end());
auto response=send_with_failover(*state,state->kms,http_method::post,"/",{{"content-type","application/x-amz-json-1.1"},{"x-amz-target","TrentService."+operation}},std::move(payload),options,&state->kms_credentials,state->kms_region,"kms");
auto response=send_with_failover(*state,state->kms,http_method::post,"/",{{"content-type","application/json"},{"x-amz-target","TrentService."+operation}},std::move(payload),options,&state->kms_credentials,state->kms_region,"kms");
if (!response) co_return std::unexpected(response.error());
co_return convert(std::move(*response));
}

#define QL_SDK_KMS_FORWARD(name,operation) task<result<service_response>> kms_api::name(std::string payload,call_options options) const { co_return sync_wait(invoke(operation,std::move(payload),options)); }
QL_SDK_KMS_FORWARD(create_key,"CreateKey")
task<result<service_response>> kms_api::create_key(std::string json_payload,call_options options) const {
auto* state=state_cast(state_);
if (!state) co_return std::unexpected(error{.domain=error_domain::configuration,.code=850,.message="SDK state is unavailable"});
options.idempotent=false;
auto view=as_bytes(json_payload);
bytes payload(view.begin(),view.end());
auto response=send_with_failover(*state,state->kms,http_method::post,"/",{{"content-type","application/json"},{"x-amz-target","TrentService.CreateKey"}},std::move(payload),options,&state->kms_credentials,state->kms_region,"kms");
if (!response) co_return std::unexpected(response.error());
co_return convert(std::move(*response));
}
task<result<service_response>> kms_api::create_key_async(std::string json_payload,call_options options) const {
auto* state=state_cast(state_);
if (!state) co_return std::unexpected(error{.domain=error_domain::configuration,.code=850,.message="SDK state is unavailable"});
options.idempotent=false;
auto view=as_bytes(json_payload);
bytes payload(view.begin(),view.end());
auto response=send_with_failover(*state,state->kms,http_method::post,"/?async=1",{{"content-type","application/json"},{"x-amz-target","TrentService.CreateKey"}},std::move(payload),options,&state->kms_credentials,state->kms_region,"kms");
if (!response) co_return std::unexpected(response.error());
co_return convert(std::move(*response));
}
QL_SDK_KMS_FORWARD(describe_key,"DescribeKey")
QL_SDK_KMS_FORWARD(encrypt,"Encrypt")
QL_SDK_KMS_FORWARD(decrypt,"Decrypt")
Expand Down
2 changes: 2 additions & 0 deletions src/sdk_facade.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,8 @@ public:
/** Invokes any compatible `TrentService.<operation>` QKMS operation. */
[[nodiscard]] task<result<service_response>> invoke(std::string operation,std::string json_payload="{}",call_options options={}) const;
[[nodiscard]] task<result<service_response>> create_key(std::string json_payload="{}",call_options options={}) const;
/** Non-blocking CreateKey (`?async=1`) for MPC/DKG operations that may outlive proxy timeouts. */
[[nodiscard]] task<result<service_response>> create_key_async(std::string json_payload="{}",call_options options={}) const;
[[nodiscard]] task<result<service_response>> describe_key(std::string json_payload,call_options options={}) const;
[[nodiscard]] task<result<service_response>> encrypt(std::string json_payload,call_options options={}) const;
[[nodiscard]] task<result<service_response>> decrypt(std::string json_payload,call_options options={}) const;
Expand Down
2 changes: 1 addition & 1 deletion tests/c_api_tests.c
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
#include <string.h>

int main(void) {
assert(strcmp(ql_version(),"1.1.0")==0);
assert(strcmp(ql_version(),"1.2.0")==0);
ql_error error={0};
ql_sdk* sdk=ql_sdk_create(NULL,&error);
assert(sdk!=NULL);
Expand Down
16 changes: 14 additions & 2 deletions tests/facade_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@ import quilibrium.sdk;
struct mock_state final {
bool saw_storage_auth{false};
bool saw_kms_auth{false};
bool saw_kms_async_create{false};
bool saw_kms_json_content_type{false};
bool saw_kms_create_non_idempotent{false};
bool saw_native_grpc{false};
};

quilibrium::result<quilibrium::http_response> mock_send(void* opaque,quilibrium::http_request request,quilibrium::call_options) {
quilibrium::result<quilibrium::http_response> mock_send(void* opaque,quilibrium::http_request request,quilibrium::call_options options) {
auto* state=static_cast<mock_state*>(opaque);
std::string body;
quilibrium::http_headers headers{{"content-type","application/json"}};
Expand All @@ -24,8 +27,13 @@ quilibrium::result<quilibrium::http_response> mock_send(void* opaque,quilibrium:
} else if(request.target=="/bucket/object.bin") {
state->saw_storage_auth=request.header_fields.contains("authorization");
body="storage-ok";
} else if(request.target=="/"&&request.header_fields.contains("x-amz-target")) {
} else if((request.target=="/"||request.target=="/?async=1")&&request.header_fields.contains("x-amz-target")) {
state->saw_kms_auth=request.header_fields.contains("authorization");
if(request.header_fields.at("x-amz-target")=="TrentService.CreateKey") {
if(request.target=="/?async=1") state->saw_kms_async_create=true;
if(!options.idempotent) state->saw_kms_create_non_idempotent=true;
}
if(const auto ct=request.header_fields.find("content-type");ct!=request.header_fields.end()&&ct->second=="application/json") state->saw_kms_json_content_type=true;
body=R"({"Signature":"AA=="})";
} else if(request.target=="/quilibrium.node.node.pb.NodeService/GetNodeInfo") {
state->saw_native_grpc=true;
Expand Down Expand Up @@ -63,6 +71,10 @@ int main(){

auto signed_value=quilibrium::sync_wait(q->kms().invoke("Sign",R"({"KeyId":"test"})"));
assert(signed_value&&signed_value->status_code==200&&state->saw_kms_auth);
auto blocking_key=quilibrium::sync_wait(q->kms().create_key(R"({"KeySpec":"ECC_SECG_P256K1"})"));
assert(blocking_key&&blocking_key->status_code==200&&state->saw_kms_create_non_idempotent);
auto async_key=quilibrium::sync_wait(q->kms().create_key_async(R"({"KeySpec":"ECC_SECG_P256K1"})"));
assert(async_key&&async_key->status_code==200&&state->saw_kms_async_create&&state->saw_kms_json_content_type&&state->saw_kms_create_non_idempotent);

auto node_info=quilibrium::sync_wait(q->native().call(quilibrium::native_service::node,"GetNodeInfo",{}));
assert(node_info&&node_info->size()==2&&state->saw_native_grpc);
Expand Down
17 changes: 16 additions & 1 deletion tests/service_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,23 @@ struct mock_state final {
bool saw_kms_target{false};
bool saw_multipart{false};
bool saw_rotation_target{false};
bool saw_async_create{false};
bool saw_qkms_json_content_type{false};
bool saw_create_non_idempotent{false};
};

quilibrium::result<quilibrium::http_response> service_send(void* opaque,quilibrium::http_request request,quilibrium::call_options) {
quilibrium::result<quilibrium::http_response> service_send(void* opaque,quilibrium::http_request request,quilibrium::call_options options) {
auto* state=static_cast<mock_state*>(opaque);
if (request.header_fields.contains("authorization")) state->saw_authorization=true;
if (request.target.find("?uploads") != std::string::npos) state->saw_multipart=true;
if (const auto it=request.header_fields.find("x-amz-target");it!=request.header_fields.end()) {
if (it->second=="TrentService.Sign") state->saw_kms_target=true;
if (it->second=="TrentService.EnableKeyRotation") state->saw_rotation_target=true;
if (it->second=="TrentService.CreateKey") {
if (request.target=="/?async=1") state->saw_async_create=true;
if (!options.idempotent) state->saw_create_non_idempotent=true;
}
if (const auto ct=request.header_fields.find("content-type");ct!=request.header_fields.end() && ct->second=="application/json") state->saw_qkms_json_content_type=true;
}
std::string body=request.target.starts_with("/v2/farcaster/user")?R"({"user":{"fid":3,"username":"dwr.eth"}})":"{}";
const auto view=quilibrium::as_bytes(body);
Expand Down Expand Up @@ -66,8 +74,15 @@ int main() {
assert(response&&response->status_code==200);
auto rotation=quilibrium::sync_wait(client.enable_key_rotation({}));
assert(rotation&&rotation->status_code==200);
auto blocking_create=quilibrium::sync_wait(client.create_key({}));
assert(blocking_create&&blocking_create->status_code==200);
auto async_create=quilibrium::sync_wait(client.create_key_async({}));
assert(async_create&&async_create->status_code==200);
assert(state->saw_kms_target);
assert(state->saw_rotation_target);
assert(state->saw_async_create);
assert(state->saw_qkms_json_content_type);
assert(state->saw_create_non_idempotent);
}

{
Expand Down
Loading