HTTP bridge NIP-98: byte-identical same-second requests are rejected as replays — the per-request-uniqueness convention is undocumented
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 78/100
- Issue type
- Documentation
- Clarity
- Clearly specified
- Activity status
- Quiet
- Tech stack
- redis, rust
- Domain
- api, backend, documentation
Research direction
Start with NOSTR.md and compare its HTTP bridge coverage with crates/buzz-auth/src/nip98_replay.rs, crates/buzz-cli/src/client.rs, and crates/buzz-relay/src/api/git/transport.rs. Document per-request uniqueness for bridge NIP-98 events, recommend a random nonce tag, explain why changing created_at is unsuitable, and contrast this with reusable Git credentials.
Written by the indexing model from the issue text.
Description
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-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-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 /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). - 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 currentmain(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-clinonce 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).
- Dominant language
- Rust
- Stars
- 33.7k
- Forks
- 4.4k
- Avg merge
- 1d 21h
- Merged PRs (30d)
- 239
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from block/buzz
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
-
workflow_sink's mention parser never masks code regions — @name inside a code span wakes the agent Open
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
-
Difficulty 2/5 Half a day Newbie friendliness 88/100
-
Difficulty 1/5 Under an hour Newbie friendliness 92/100
Similar issues
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
gitbutlerapp/gitbutler#15998 · 1 comment ·
-
bug triage:deciding
Difficulty 1/5 Under an hour Newbie friendliness 88/100
open-telemetry/otel-arrow#4132 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
bitcoindevkit/bdk-ffi#1125 ·