block/buzz · error

nip98 mode always resolves principal

Error message

nip98 mode always resolves principal

What it means

Invariant panic in the admin API probe handler: when `config.auth` is `AdminAuth::Nip98`, the code assumes `authorize` already returned `Ok(Some(principal))` (nip98 always resolves a principal), so `principal.as_ref().expect("nip98 mode always resolves principal")` must be Some. It panics if the request reached the nip98 branch with `principal == None`, meaning the auth invariant was violated upstream.

Source

Thrown at crates/buzz-relay/src/api/admin/mod.rs:167

) -> Result<Json<ProbeResponse>, ApiError> {
    let principal = authorize(
        &state,
        &headers,
        uri.path_and_query()
            .map_or_else(|| uri.path(), |pq| pq.as_str()),
        "GET",
        None,
    )
    .await?;

    let (auth_mode, role, source, can_act, can_staff) = match &state.config.admin {
        Some(config) => match &config.auth {
            crate::config::AdminAuth::Disabled => ("disabled", None, None, false, false),
            crate::config::AdminAuth::Nip98 => {
                // principal is Some in nip98 mode (authorize returns Ok(Some(_)))
                let p = principal
                    .as_ref()
                    .expect("nip98 mode always resolves principal");
                let can_staff = p.role == AdminRole::Operator;
                (
                    "nip98",
                    Some(admin_role_str(p.role)),
                    Some(admin_source_str(&p.source)),
                    true, // both Operator and Moderator can act
                    can_staff,
                )
            }
        },
        None => return Err(ApiError::not_found()),
    };

    Ok(Json(ProbeResponse {
        status: "ok",
        auth_mode,
        role,
        source,

View on GitHub (pinned to dad5a33865)

Solutions

  1. Inspect how `principal` is populated before this match — ensure the nip98 authorize() path rejects (not passes) requests with no resolvable principal.
  2. Confirm the auth middleware runs before the probe handler and stores the principal for nip98 mode.
  3. Validate the NIP-98 Authorization header (base64 event, kind 27235, valid sig, correct URL/method tags) on the client side.
  4. Replace the expect with an explicit error response (401) if None is a legitimate runtime state rather than an invariant.

Example fix

// before
let p = principal.as_ref().expect("nip98 mode always resolves principal");
// after
let Some(p) = principal.as_ref() else {
    return StatusCode::UNAUTHORIZED.into_response(); // principal must exist in nip98 mode
};
Defensive patterns

Strategy: type-guard

Validate before calling

// client-side: send a valid NIP-98 header before calling admin probe
let event = EventBuilder::auth(Url::parse("https://relay/api/admin/probe")?)
    .sign_with_keys(&user_keys)?;
let header = format!("Nostr {}", URL_SAFE_NO_PAD.encode(event.as_json().to_string()));
assert!(!header.is_empty(), "NIP-98 Authorization header required for nip98 admin mode");

Type guard

fn nip98_principal(principal: Option<&AdminPrincipal>) -> Result<&AdminPrincipal, StatusCode> {
    principal.ok_or(StatusCode::UNAUTHORIZED)
}

Try / catch

let Some(p) = principal.as_ref() else {
    return StatusCode::UNAUTHORIZED.into_response();
};

Prevention

When it happens

Trigger: A request routed to the nip98 branch without a resolved principal — e.g. authorize() returned Ok(None) for a missing/invalid NIP-98 Authorization header but the caller treated it as authenticated, auth middleware ordering changed so probe runs before authorization, or an AdminAuth enum refactor altered which branch executes.

Common situations: Deployments where the relay's admin auth config changed from Disabled to Nip98 but requests carry no Authorization header; middleware refactors that make principal resolution conditional; misconfigured NIP-98 event payloads failing to parse silently upstream.

Related errors


AI-assisted analysis of block/buzz@dad5a33865 (2026-08-30). Data as JSON: /api/errors/a69ba2113050d4a5. Report an issue: GitHub.