src/grpc.rs
//! gRPC-web wiring — the one place that knows how the browser reaches holger,
//! and the one place a credential is attached.
//!
//! Generated stubs come from `proto/holger.proto` (vendored from the holger
//! main repo — that copy is the source of truth; refresh with
//! `cp ../holger/proto/holger.proto proto/`). Transport is
//! `tonic-web-wasm-client` over fetch; holger's server terminates gRPC-web
//! with its tonic-web layer.
pub mod pb {
tonic::include_proto!("holger.v1");
}
/// holger's room contract (`proto/holger-room.proto`, vendored like the main
/// one): where more room for a repository is bought — holger's word.
pub mod pb_room {
tonic::include_proto!("holger.room.v1");
}
/// holger's account contract (`proto/holger-account.proto`, lane P4): the
/// repositories a person has, the private ones an admin makes, their client
/// setup and plan — holger's word.
pub mod pb_account {
tonic::include_proto!("holger.account.v1");
}
/// holger's token contract (`proto/holger-token.proto`, lane P3): repository
/// tokens for package clients.
pub mod pb_token {
tonic::include_proto!("holger.token.v1");
}
use tonic_web_wasm_client::Client;
pub use pb::admin_service_client::AdminServiceClient;
pub use pb::admission_service_client::AdmissionServiceClient;
pub use pb::job_service_client::JobServiceClient;
#[cfg(feature = "metered")]
pub use pb_room::room_service_client::RoomServiceClient;
pub use pb_account::account_service_client::AccountServiceClient;
pub use pb_token::token_service_client::TokenServiceClient;
pub use pb::archive_service_client::ArchiveServiceClient;
pub use pb::audit_service_client::AuditServiceClient;
pub use pb::migration_service_client::MigrationServiceClient;
pub use pb::promotion_service_client::PromotionServiceClient;
pub use pb::property_service_client::PropertyServiceClient;
pub use pb::replication_service_client::ReplicationServiceClient;
pub use pb::repository_service_client::RepositoryServiceClient;
pub use pb::sbom_service_client::SbomServiceClient;
pub use pb::search_service_client::SearchServiceClient;
pub use pb::usage_service_client::UsageServiceClient;
/// Base URL of the holger gRPC-web door. Same-origin under `/grpc` by default:
/// the dev proxy (Trunk) and any reverse proxy in front of holger mount the
/// door there, so no CORS ceremony is needed. Override at build time with
/// `HOLGER_GRPC_WEB_URL` (e.g. a direct server origin — that path needs CORS
/// opened on the holger side instead).
pub fn base_url() -> String {
let origin = web_sys::window().and_then(|w| w.location().origin().ok());
crate::util::grpc_base_url(option_env!("HOLGER_GRPC_WEB_URL"), origin.as_deref())
}
// ─── The session ─────────────────────────────────────────────────────────────
//
// The person's session is the console server's HttpOnly cookie (A34-38,
// `holger-ui-server` `session`): the browser sends it with every same-origin
// call, the server turns it into `authorization: Bearer` for holger, and no
// script on this page can read it. So nothing here holds or attaches a bearer.
type Authed = Client;
fn channel() -> Authed {
Client::new(base_url())
}
pub fn admin() -> AdminServiceClient<Authed> {
AdminServiceClient::new(channel())
}
pub fn repositories() -> RepositoryServiceClient<Authed> {
RepositoryServiceClient::new(channel())
}
pub fn search() -> SearchServiceClient<Authed> {
SearchServiceClient::new(channel())
}
pub fn sboms() -> SbomServiceClient<Authed> {
SbomServiceClient::new(channel())
}
pub fn properties() -> PropertyServiceClient<Authed> {
PropertyServiceClient::new(channel())
}
pub fn promotion() -> PromotionServiceClient<Authed> {
PromotionServiceClient::new(channel())
}
/// The audit plane (`GetArtifactTrail`, Auditor or Admin) — the security
/// view's trail.
pub fn audit() -> AuditServiceClient<Authed> {
AuditServiceClient::new(channel())
}
/// The migration door: Discover · Start · Watch · Cancel. ADMIN-gated
/// server-wide and fail-closed on the server, and registered only in an
/// `online` build — a server without it answers `Unimplemented`, which the
/// console renders as "not in this build".
pub fn migration() -> MigrationServiceClient<Authed> {
MigrationServiceClient::new(channel())
}
pub fn replication() -> ReplicationServiceClient<Authed> {
ReplicationServiceClient::new(channel())
}
pub fn archives() -> ArchiveServiceClient<Authed> {
ArchiveServiceClient::new(channel())
}
/// The job queue (`ListJobs`, ADMIN) — the admin view's Queue pane.
pub fn jobs() -> JobServiceClient<Authed> {
JobServiceClient::new(channel())
}
/// Admission (`ListUsage` + `AdmitPlan`, ADMIN) — the admin view's Admission pane.
pub fn admission() -> AdmissionServiceClient<Authed> {
AdmissionServiceClient::new(channel())
}
/// Where more room is bought (`RoomService.Room`, READER on the repository). A metered
/// console's only (A28 M13): My plan, "More room" and the admin's order button ask it.
#[cfg(feature = "metered")]
pub fn room() -> RoomServiceClient<Authed> {
RoomServiceClient::new(channel())
}
/// The repositories this person has (`AccountService`) — the buyer's pages.
pub fn account() -> AccountServiceClient<Authed> {
AccountServiceClient::new(channel())
}
/// Repository tokens (`TokenService`: mint, list, revoke) — My tokens.
pub fn tokens() -> TokenServiceClient<Authed> {
TokenServiceClient::new(channel())
}
/// One repository's meters (`ReadUsage`, READER on it) — the Disk panel.
pub fn usage() -> UsageServiceClient<Authed> {
UsageServiceClient::new(channel())
}
/// The server's answer to "who am I here?" — subject, method, effective
/// global role. Discoverability only: every operation is still checked by
/// its own gate server-side.
/// **The security domain this Holger names** (its RON `domain:`, lane HOL-CDS): the
/// `x-holger-domain` holger puts on every answer, read off its WhoAmI answer. `None` when it
/// names none or does not answer.
pub async fn domain() -> Option<String> {
let r = admin().who_am_i(pb::WhoAmIRequest::default()).await.ok()?;
crate::util::domain_label(r.metadata().get("x-holger-domain").and_then(|v| v.to_str().ok()))
}
pub async fn who_am_i() -> Result<pb::WhoAmIResponse, tonic::Status> {
admin()
.who_am_i(pb::WhoAmIRequest::default())
.await
.map(|r| r.into_inner())
}
/// The server's refusal, verbatim, with the status-code name as fallback when
/// a transport-level failure carries no message — one rendering for every
/// gRPC error in the console.
pub fn status_text(s: &tonic::Status) -> String {
if s.code() == tonic::Code::Unimplemented {
// A gated service this holger was built without (supply-chain,
// mirror, migration, admission): said as such, whatever the transport
// put in the message.
return crate::util::NOT_IN_BUILD.to_owned();
}
let msg = s.message();
if msg.is_empty() {
format!("{:?}", s.code())
} else {
msg.to_string()
}
}
/// A unary answer, or holger's refusal in its own words.
pub fn answer<T>(r: Result<tonic::Response<T>, tonic::Status>) -> Result<T, String> {
r.map(tonic::Response::into_inner).map_err(|s| status_text(&s))
}
/// As [`answer`], and `Ok(None)` when this holger does not serve the door
/// (`Unimplemented`: a build without it) — the pane then says "not in this
/// build" ([`crate::util::not_in_build`]), never nothing.
pub fn answer_or_none<T>(r: Result<tonic::Response<T>, tonic::Status>) -> Result<Option<T>, String> {
match r {
Ok(r) => Ok(Some(r.into_inner())),
Err(s) if s.code() == tonic::Code::Unimplemented => Ok(None),
Err(s) => Err(status_text(&s)),
}
}
/// One failed call, as the console's unified state model sees it: the code
/// (classified for rendering), the server's verbatim message, and WHEN.
/// Never carries the credential — the interceptor owns that and it does not
/// appear in statuses.
#[derive(Clone, PartialEq)]
pub struct Fail {
pub code: tonic::Code,
pub message: String,
pub at: String,
}
pub fn fail(s: &tonic::Status) -> Fail {
Fail {
code: s.code(),
message: status_text(s),
at: crate::webio::now_hms(),
}
}