photoncloud-monorepo/creditservice/creditservice-client/src/lib.rs
centra d2149b6249 fix(lightningstor): Fix SigV4 canonicalization for AWS S3 auth
- Replace form_urlencoded with RFC 3986 compliant URI encoding
- Implement aws_uri_encode() matching AWS SigV4 spec exactly
- Unreserved chars (A-Z,a-z,0-9,-,_,.,~) not encoded
- All other chars percent-encoded with uppercase hex
- Preserve slashes in paths, encode in query params
- Normalize empty paths to '/' per AWS spec
- Fix test expectations (body hash, HMAC values)
- Add comprehensive SigV4 signature determinism test

This fixes the canonicalization mismatch that caused signature
validation failures in T047. Auth can now be enabled for production.

Refs: T058.S1
2025-12-12 06:23:46 +09:00

130 lines
4.1 KiB
Rust

//! CreditService client library
//!
//! Provides a convenient client for interacting with CreditService.
use creditservice_proto::credit_service_client::CreditServiceClient;
use tonic::transport::Channel;
use tracing::debug;
pub use creditservice_proto::*;
/// CreditService client
pub struct Client {
inner: CreditServiceClient<Channel>,
}
impl Client {
/// Connect to a CreditService server
pub async fn connect(addr: impl AsRef<str>) -> Result<Self, tonic::transport::Error> {
let addr = addr.as_ref().to_string();
debug!("Connecting to CreditService at {}", addr);
let inner = CreditServiceClient::connect(addr).await?;
Ok(Self { inner })
}
/// Get wallet for a project
pub async fn get_wallet(
&mut self,
project_id: impl Into<String>,
) -> Result<Wallet, tonic::Status> {
let request = GetWalletRequest {
project_id: project_id.into(),
};
let response = self.inner.get_wallet(request).await?;
response
.into_inner()
.wallet
.ok_or_else(|| tonic::Status::not_found("Wallet not found"))
}
/// Create a new wallet
pub async fn create_wallet(
&mut self,
project_id: impl Into<String>,
org_id: impl Into<String>,
initial_balance: i64,
) -> Result<Wallet, tonic::Status> {
let request = CreateWalletRequest {
project_id: project_id.into(),
org_id: org_id.into(),
initial_balance,
};
let response = self.inner.create_wallet(request).await?;
response
.into_inner()
.wallet
.ok_or_else(|| tonic::Status::internal("Failed to create wallet"))
}
/// Check quota before resource creation
pub async fn check_quota(
&mut self,
project_id: impl Into<String>,
resource_type: ResourceType,
quantity: i32,
estimated_cost: i64,
) -> Result<CheckQuotaResponse, tonic::Status> {
let request = CheckQuotaRequest {
project_id: project_id.into(),
resource_type: resource_type as i32,
quantity,
estimated_cost,
};
self.inner.check_quota(request).await.map(|r| r.into_inner())
}
/// Reserve credits for a resource creation
pub async fn reserve_credits(
&mut self,
project_id: impl Into<String>,
amount: i64,
description: impl Into<String>,
resource_type: impl Into<String>,
ttl_seconds: i32,
) -> Result<Reservation, tonic::Status> {
let request = ReserveCreditsRequest {
project_id: project_id.into(),
amount,
description: description.into(),
resource_type: resource_type.into(),
ttl_seconds,
};
let response = self.inner.reserve_credits(request).await?;
response
.into_inner()
.reservation
.ok_or_else(|| tonic::Status::internal("Failed to create reservation"))
}
/// Commit a reservation after successful resource creation
pub async fn commit_reservation(
&mut self,
reservation_id: impl Into<String>,
actual_amount: i64,
resource_id: impl Into<String>,
) -> Result<CommitReservationResponse, tonic::Status> {
let request = CommitReservationRequest {
reservation_id: reservation_id.into(),
actual_amount,
resource_id: resource_id.into(),
};
self.inner
.commit_reservation(request)
.await
.map(|r| r.into_inner())
}
/// Release a reservation (e.g., if resource creation failed)
pub async fn release_reservation(
&mut self,
reservation_id: impl Into<String>,
reason: impl Into<String>,
) -> Result<bool, tonic::Status> {
let request = ReleaseReservationRequest {
reservation_id: reservation_id.into(),
reason: reason.into(),
};
let response = self.inner.release_reservation(request).await?;
Ok(response.into_inner().success)
}
}