Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger-ui

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(),
    }
}