floonet-strfry: add a NIP-98 header-minting example for the name authority

Operating a NIP-98-gated endpoint (register / unregister / quote) needs signed
kind-27235 Authorization headers, and there is no nak on the target hosts. This
example reuses the crate's existing nostr/base64/sha2 deps to mint a
"Nostr <base64-event>" header for curl/CI: generate a throwaway identity or
reuse one via NIP98_SK, sign over the method/path/body, print the header to
stdout. The u-tag is built from FLOONET_BASE_URL to match server verification.
This commit is contained in:
2ro
2026-07-03 03:15:38 -04:00
parent c5ca6860d7
commit fb62ed2bf2
+67
View File
@@ -0,0 +1,67 @@
//! Mint a NIP-98 `Authorization: Nostr <base64-event>` header for calling this
//! authority's authenticated endpoints (register / unregister / quote) with a
//! plain HTTP client like `curl`. Handy for operators and CI: no external
//! nostr tooling required, since the crate already depends on `nostr`.
//!
//! Usage:
//! # generate a throwaway identity (prints its secret+pubkey to stderr)
//! cargo run --example nip98 -- GET /api/v1/name/alice
//!
//! # reuse an identity and sign over a request body
//! FLOONET_BASE_URL=https://nm.floonet.dev NIP98_SK=<64-hex-secret> \
//! cargo run --example nip98 -- POST /api/v1/register '{"name":"alice","pubkey":"<hex>"}'
//!
//! The header value is printed to stdout (nothing else), so it can be captured
//! straight into a curl invocation:
//!
//! AUTH=$(NIP98_SK=$SK cargo run -q --example nip98 -- POST /api/v1/register "$BODY")
//! curl -H "Authorization: $AUTH" -d "$BODY" https://nm.floonet.dev/api/v1/register
//!
//! The `u` tag is built from FLOONET_BASE_URL (default https://nm.floonet.dev),
//! which MUST equal the authority's configured base URL — that is what the
//! server verifies the signature's `u` tag against.
use base64::Engine;
use nostr::{EventBuilder, JsonUtil, Keys, Kind, Tag, Timestamp};
use sha2::{Digest, Sha256};
fn main() {
let mut args = std::env::args().skip(1);
let method = args
.next()
.expect("usage: nip98 <METHOD> <PATH> [BODY] (e.g. POST /api/v1/register '{...}')");
let path = args
.next()
.expect("usage: nip98 <METHOD> <PATH> [BODY] (e.g. POST /api/v1/register '{...}')");
let body = args.next().unwrap_or_default();
let base_url =
std::env::var("FLOONET_BASE_URL").unwrap_or_else(|_| "https://nm.floonet.dev".to_string());
let keys = match std::env::var("NIP98_SK") {
Ok(sk) if !sk.trim().is_empty() => Keys::parse(sk.trim()).expect("invalid NIP98_SK"),
_ => {
let k = Keys::generate();
eprintln!("generated secret (hex): {}", k.secret_key().to_secret_hex());
k
}
};
eprintln!("pubkey (hex): {}", keys.public_key().to_hex());
let url = format!("{base_url}{path}");
let mut tags = vec![
Tag::parse(["u", &url]).unwrap(),
Tag::parse(["method", &method]).unwrap(),
];
if !body.is_empty() {
let payload = hex::encode(Sha256::digest(body.as_bytes()));
tags.push(Tag::parse(["payload", &payload]).unwrap());
}
let event = EventBuilder::new(Kind::HttpAuth, "")
.tags(tags)
.custom_created_at(Timestamp::now())
.sign_with_keys(&keys)
.expect("sign NIP-98 event");
let b64 = base64::engine::general_purpose::STANDARD.encode(event.as_json());
println!("Nostr {b64}");
}