{"record":{"id":"a69ba2113050d4a5","repo":"block/buzz","slug":"nip98-mode-always-resolves-principal","errorCode":null,"errorMessage":"nip98 mode always resolves principal","messagePattern":"nip98 mode always resolves principal","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/buzz-relay/src/api/admin/mod.rs","lineNumber":167,"sourceCode":") -> Result<Json<ProbeResponse>, ApiError> {\n    let principal = authorize(\n        &state,\n        &headers,\n        uri.path_and_query()\n            .map_or_else(|| uri.path(), |pq| pq.as_str()),\n        \"GET\",\n        None,\n    )\n    .await?;\n\n    let (auth_mode, role, source, can_act, can_staff) = match &state.config.admin {\n        Some(config) => match &config.auth {\n            crate::config::AdminAuth::Disabled => (\"disabled\", None, None, false, false),\n            crate::config::AdminAuth::Nip98 => {\n                // principal is Some in nip98 mode (authorize returns Ok(Some(_)))\n                let p = principal\n                    .as_ref()\n                    .expect(\"nip98 mode always resolves principal\");\n                let can_staff = p.role == AdminRole::Operator;\n                (\n                    \"nip98\",\n                    Some(admin_role_str(p.role)),\n                    Some(admin_source_str(&p.source)),\n                    true, // both Operator and Moderator can act\n                    can_staff,\n                )\n            }\n        },\n        None => return Err(ApiError::not_found()),\n    };\n\n    Ok(Json(ProbeResponse {\n        status: \"ok\",\n        auth_mode,\n        role,\n        source,","sourceCodeStart":149,"sourceCodeEnd":185,"githubUrl":"https://github.com/block/buzz/blob/dad5a33865fc81a2e55b3b60746632f615ec1e3a/crates/buzz-relay/src/api/admin/mod.rs#L149-L185","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Inspect how `principal` is populated before this match — ensure the nip98 authorize() path rejects (not passes) requests with no resolvable principal.","Confirm the auth middleware runs before the probe handler and stores the principal for nip98 mode.","Validate the NIP-98 Authorization header (base64 event, kind 27235, valid sig, correct URL/method tags) on the client side.","Replace the expect with an explicit error response (401) if None is a legitimate runtime state rather than an invariant."],"exampleFix":"// before\nlet p = principal.as_ref().expect(\"nip98 mode always resolves principal\");\n// after\nlet Some(p) = principal.as_ref() else {\n    return StatusCode::UNAUTHORIZED.into_response(); // principal must exist in nip98 mode\n};","handlingStrategy":"type-guard","validationCode":"// client-side: send a valid NIP-98 header before calling admin probe\nlet event = EventBuilder::auth(Url::parse(\"https://relay/api/admin/probe\")?)\n    .sign_with_keys(&user_keys)?;\nlet header = format!(\"Nostr {}\", URL_SAFE_NO_PAD.encode(event.as_json().to_string()));\nassert!(!header.is_empty(), \"NIP-98 Authorization header required for nip98 admin mode\");","typeGuard":"fn nip98_principal(principal: Option<&AdminPrincipal>) -> Result<&AdminPrincipal, StatusCode> {\n    principal.ok_or(StatusCode::UNAUTHORIZED)\n}","tryCatchPattern":"let Some(p) = principal.as_ref() else {\n    return StatusCode::UNAUTHORIZED.into_response();\n};","preventionTips":["Always send a valid NIP-98 Authorization header in nip98 admin mode.","Run authorize() before any handler that assumes a principal exists.","Prefer explicit 401 over expect() when None is a reachable runtime state.","Cover the 'no header' path with a test so the invariant stays true."],"tags":["rust","panic","auth","nip98","invariant"],"backgroundTag":"missing-auth-principal","analyzedSha":"dad5a33865fc81a2e55b3b60746632f615ec1e3a","analyzedAt":"2026-08-30T13:49:18.474Z","contentChangedAt":"2026-08-30T13:49:18.474Z","schemaVersion":2},"datasetVersion":"2026-09-08T15:18:49.778Z"}