Describe the bug
The HTTP bridge's NIP-98 replay guard keys on the auth event id (crates/buzz-auth/src/nip98_replay.rs — shared Redis seen-set, atomic set-if-absent, TTL ≥ 120s). That is the right design for actual replays, but it has a side effect for legitimate clients: a NIP-98 auth event is fully deterministic — kind 27235, second-granularity created_at, u + method (+ optional payload) tags — so two legitimate, distinct requests to the same URL within the same second serialize to the same event, produce the same id, and the second one gets 401 NIP-98: replay detected. For headless clients (bots, agents, integration daemons) that batch or burst requests, this is routine, not exotic.
Buzz's own bridge clients already handle this — but the obligation is documented nowhere:
crates/buzz-cli/src/client.rs:93-95 adds a random nonce tag to every per-request auth event, with the comment "Nonce prevents replay rejection for rapid-fire requests with identical bodies."
crates/buzz-acp/src/relay.rs — same nonce approach.
NOSTR.md — zero mentions of nonce or of any per-request-uniqueness requirement for NIP-98.
So a third-party integrator implementing NIP-98 exactly per the NIP (which does not require a nonce) gets intermittent 401s under load with no documented explanation.
For clarity, this report is about the bridge routes only. The Git HTTP transport is a different, deliberate regime: git-credential-nostr signs one repo-root-scoped credential that Git intentionally reuses across the requests of a session (info/refs GET → pack POST), and the transport therefore applies no event-id dedup by design — crates/buzz-relay/src/api/git/transport.rs:193-198 documents that rejecting reused ids would break normal clone/push, with the ±60s window + URL scoping + HTTPS as the replay bound. That regime is coherent as-is and needs no change; it's mentioned only because the contrast is exactly what the docs should state: per-request events with per-request uniqueness on the bridge; one intentionally reusable session credential on Git routes.
Steps to reproduce
- Run the relay with the HTTP bridge and Redis (standard dev setup).
- From one keypair, issue two
POST /query requests with the same body in the same wall-clock second, each with a freshly signed NIP-98 auth event containing only the standard u/method/payload tags (no nonce).
- First request: 200. Second request:
401 NIP-98: replay detected — the two auth events are byte-identical, hence id-identical.
We hit this with a headless integration daemon (Buzz rooms → external knowledge store: https://github.com/OriginTrail/buzz-dkg-integration) and reproduced it deterministically. One second-order finding worth documenting alongside the convention: the "obvious" workaround of monotonically bumping created_at to force uniqueness drifts out of the verifier's ±60s freshness window under sustained bursts — we shipped that first and it broke; a random nonce tag (exactly what buzz-cli does) is the correct scheme because it keeps timestamps honest.
Expected behavior
Document the client obligation: NIP-98 auth events sent to the bridge must be unique per request, and the recommended convention is an ignored random nonce tag (as buzz-cli and buzz-acp already do). A short note wherever the HTTP bridge gets documented — NOSTR.md currently doesn't cover it, so either a small bridge subsection there or the module docs — turns an intermittent mystery 401 into a documented contract, and can also state the bridge-vs-Git contrast above so nobody "fixes" the Git regime by mistake.
Happy to submit the docs PR (DCO-signed, Conventional Commits) if maintainers agree with the direction.
Version and platform
- Buzz version: observed live at
dd222a5; code paths re-verified unchanged at current main (485d03a, 2026-07-29)
- OS: macOS 15 (relay built from source, host process; Postgres/Redis/MinIO in Docker)
Logs / additional context
HTTP/1.1 401 Unauthorized
NIP-98: replay detected
Relevant code, verified at 485d03a:
- Replay guard design + id-keying:
crates/buzz-auth/src/nip98_replay.rs:1-31 (module doc: "Verify first, then mark", TTL floor 120s = 2×60s skew window)
buzz-cli nonce convention + comment: crates/buzz-cli/src/client.rs:90-96
- Git transport's deliberate no-dedup regime:
crates/buzz-relay/src/api/git/transport.rs:144-157 (repo-root credential scope), :193-198 ("NIP-98 event-ID dedup intentionally NOT implemented here… reuses one signed token across multiple requests")
- Duplicate search: no existing issue found for
nip-98 replay, replay detected, nonce (searched 2026-07-29).
Describe the bug
The HTTP bridge's NIP-98 replay guard keys on the auth event id (
crates/buzz-auth/src/nip98_replay.rs— shared Redis seen-set, atomic set-if-absent, TTL ≥ 120s). That is the right design for actual replays, but it has a side effect for legitimate clients: a NIP-98 auth event is fully deterministic — kind27235, second-granularitycreated_at,u+method(+ optionalpayload) tags — so two legitimate, distinct requests to the same URL within the same second serialize to the same event, produce the same id, and the second one gets401 NIP-98: replay detected. For headless clients (bots, agents, integration daemons) that batch or burst requests, this is routine, not exotic.Buzz's own bridge clients already handle this — but the obligation is documented nowhere:
crates/buzz-cli/src/client.rs:93-95adds a randomnoncetag to every per-request auth event, with the comment "Nonce prevents replay rejection for rapid-fire requests with identical bodies."crates/buzz-acp/src/relay.rs— same nonce approach.NOSTR.md— zero mentions ofnonceor of any per-request-uniqueness requirement for NIP-98.So a third-party integrator implementing NIP-98 exactly per the NIP (which does not require a nonce) gets intermittent 401s under load with no documented explanation.
For clarity, this report is about the bridge routes only. The Git HTTP transport is a different, deliberate regime:
git-credential-nostrsigns one repo-root-scoped credential that Git intentionally reuses across the requests of a session (info/refsGET → pack POST), and the transport therefore applies no event-id dedup by design —crates/buzz-relay/src/api/git/transport.rs:193-198documents that rejecting reused ids would break normal clone/push, with the ±60s window + URL scoping + HTTPS as the replay bound. That regime is coherent as-is and needs no change; it's mentioned only because the contrast is exactly what the docs should state: per-request events with per-request uniqueness on the bridge; one intentionally reusable session credential on Git routes.Steps to reproduce
POST /queryrequests with the same body in the same wall-clock second, each with a freshly signed NIP-98 auth event containing only the standardu/method/payloadtags (no nonce).401 NIP-98: replay detected— the two auth events are byte-identical, hence id-identical.We hit this with a headless integration daemon (Buzz rooms → external knowledge store: https://github.com/OriginTrail/buzz-dkg-integration) and reproduced it deterministically. One second-order finding worth documenting alongside the convention: the "obvious" workaround of monotonically bumping
created_atto force uniqueness drifts out of the verifier's ±60s freshness window under sustained bursts — we shipped that first and it broke; a randomnoncetag (exactly whatbuzz-clidoes) is the correct scheme because it keeps timestamps honest.Expected behavior
Document the client obligation: NIP-98 auth events sent to the bridge must be unique per request, and the recommended convention is an ignored random
noncetag (asbuzz-cliandbuzz-acpalready do). A short note wherever the HTTP bridge gets documented —NOSTR.mdcurrently doesn't cover it, so either a small bridge subsection there or the module docs — turns an intermittent mystery 401 into a documented contract, and can also state the bridge-vs-Git contrast above so nobody "fixes" the Git regime by mistake.Happy to submit the docs PR (DCO-signed, Conventional Commits) if maintainers agree with the direction.
Version and platform
dd222a5; code paths re-verified unchanged at currentmain(485d03a, 2026-07-29)Logs / additional context
Relevant code, verified at
485d03a:crates/buzz-auth/src/nip98_replay.rs:1-31(module doc: "Verify first, then mark", TTL floor 120s = 2×60s skew window)buzz-clinonce convention + comment:crates/buzz-cli/src/client.rs:90-96crates/buzz-relay/src/api/git/transport.rs:144-157(repo-root credential scope),:193-198("NIP-98 event-ID dedup intentionally NOT implemented here… reuses one signed token across multiple requests")nip-98 replay,replay detected,nonce(searched 2026-07-29).