Production traffic containing unparseable client messages tore down the entire WebSocket connection. A single bad message - most visibly requests carrying invalid event IDs, failing deserialization with `Invalid input length 64` - propagated out of the relay's read loop and closed the session, so every affected client had to reconnect and re-subscribe. One malformed frame cost a client all of its live subscriptions, and clients that retried the same payload produced sustained connection churn. The defect was upstream in rust-nostr's local relay, not in ngit-grasp, and was reported as nostr:nevent1qqsqmmfv6fxk995yr5858eev7z6zmchchgmk90d4rffuuwe6ewcvx0cpz3mhxue69uhhyetvv9ujumn8d96zuer9wckedd82 and fixed by nostr:nevent1qqs24l0rv6qw3mdqdaj8nj4wlds2gy8a9wrqs3gj5kcmz27c8gm7kagpz3mhxue69uhhyetvv9ujumn8d96zuer9wc7gp6cp Deserialization failures are now contained inside the WebSocket loop and answered with a `NOTICE`, leaving the session and its subscriptions intact. Upgrade the NostrDevKit crates from 0.45.0-alpha.3 to 0.45.0-alpha.8, the first published release containing the fix. Inclusion was verified three ways rather than by release notes: upstream commit 7c0f3aa2cf2fbeb9f0067cb2349579754919eabd is an ancestor of the `Bump to v0.45.0-alpha.8` commit on upstream master; the alpha.8 crate downloaded from crates.io contains the repaired match arm in `src/local_relay/local/inner.rs`; and it also ships the upstream regression test `test_malformed_client_message_does_not_close_connection`, which drives a real WebSocket with a short-author REQ and asserts a subsequent valid REQ still reaches EOSE on the same socket. That upstream test covers the behaviour directly, so no equivalent test is duplicated here. All four rust-nostr crates move together to keep the tree off a mixture of alpha versions. `nostr-relay-builder` is gone: upstream merged it into `nostr-sdk` at alpha.4, so its imports become `nostr_sdk::local_relay` and `nostr_sdk::prelude`, and `nostr-sdk` now enables the `local-relay` feature. Three further alpha-to-alpha API removals are absorbed: `nostr::hashes` is no longer re-exported, so the `Sec-WebSocket-Accept` derivation depends on `bitcoin_hashes` directly - the same crate `nostr` still uses internally, so no second hash implementation enters the tree; `BoxedFuture` became crate-private, so `WritePolicy::admit_event` spells out its `Pin<Box<dyn Future<...>>>` return type; and `EventBuilder::text_note` was removed in favour of `EventBuilder::new(Kind::TextNote, ..)`, which affects test code only. These requirements are pinned exactly as `=0.45.0-alpha.n` rather than left as caret requirements. Cargo reads `"0.45.0-alpha.3"` as `^0.45.0-alpha.3`, which admits *any* later prerelease of 0.45.0 even though prereleases promise no compatibility - exactly the surprise reported against ngit in nostr:nevent1qqs0yvj4z302cjsnqx9jpp78jrfujhse0w47adjncwkxr922rjltk8spz3mhxue69uhhyetvv9ujumn8d96zuer9wcr42j8n where a declared alpha.2 resolved to alpha.7. This upgrade is direct evidence that the risk is real: alpha.4 deleted a whole crate this project depended on, which a caret requirement would have accepted silently. Pinning is safe because it only narrows resolution, and the committed `Cargo.lock` already selects these versions; what it adds is protection for builds that do not honour the lockfile - `cargo install`, and downstream consumers of the `ngit_grasp` library. The pins should be relaxed to caret requirements once 0.45.0 is released. Validated in the project Nix development shell with `CARGO_BUILD_JOBS=2`: `cargo fmt --all --check` clean, `cargo clippy --workspace --all-targets -D warnings` clean, and `cargo test --workspace --no-fail-fast` at 1937 passed / 32 failed. The 32 failures were confirmed pre-existing by running the identical suite on unmodified master in this environment: the same 1937/32 split and a byte-identical set of failing test names, so this upgrade introduces no regressions. All dependencies remain crates.io sources, so no `flake.nix` or `nix/module.nix` hashes required updating.
10 KiB
Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased
Fixed
- Fixed malformed client messages tearing down the whole WebSocket connection.
A single unparseable message - in production, requests carrying invalid event
IDs that fail with
Invalid input length 64- closed the session, forcing clients to reconnect and re-subscribe. Invalid messages are now answered with aNOTICEand the connection stays open. Fixed upstream in rust-nostr and picked up by upgrading to0.45.0-alpha.8. - Fixed proactive sync losing repository events when public relays cap active subscriptions. Compatible GRASP filters now share bounded NIP-01 REQs while retaining per-filter history pagination.
- Fixed repeated relay rate-limit notices extending the cooldown indefinitely. Notices during an active cooldown keep its original deadline, while a new rejection after recovery begins a fresh cooldown.
- Removed superseded same-author repository states from purgatory after their replacement is promoted when the locally available Git data cannot reconstruct them. Reconstructable rollback states and other maintainers' states are retained.
2.0.0 - 2026-07-27
Breaking changes
- Removed
--relay-owner-nsecbecause command-line secrets are exposed through process listings and service diagnostics. Use therelay_owner_nsecsystemd credential,NGIT_RELAY_OWNER_NSEC, or.relay-owner.nsec. The NixOSrelayOwnerNsecFileoption remains supported and now supplies a protected systemd credential. - Removed the hidden
repair-deletion-requestscommand and the publicngit_grasp::repair_deletion_requestsmodule. Deletion-request lifecycle reconciliation now runs automatically. - Added four deletion-request retention fields to the public
Configstruct. Rust consumers that constructConfigwith a struct literal must provide them.
Added
- Added configurable bounded retention for NIP-09 deletion requests and NIP-62 request-to-vanish events, with cleanup telemetry for operators.
Changed
- Deletion requests now move through a bounded served and gating lifecycle based on whether they affected accepted events. Retention catch-up runs in the background after the relay and synchronization workers start.
- Deletion-disrespector mode now applies to both NIP-09 deletion requests and NIP-62 request-to-vanish events.
Fixed
- Fixed maintainer invitation acceptance and synchronization across owner-only, invitee-only, shared, and temporarily unavailable GRASP servers. Acceptance now converges without an invitee state event or additional Git push, handles an invitee repository that already exists, and preserves the one-way authority granted by an invitation before reciprocal acceptance.
- Kept invitation recovery responsive during restarts, rate limits, large relay sets, and expired dependency caches by bounding actor work, retaining exact relay hints, and keeping connection and subscription retries scheduler-owned.
- Fixed promoted repositories remaining on state-only GRASP sync filters, which prevented remote issues, patches, and pull requests from being discovered after their Git data arrived.
- Delayed the terminal smart-HTTP receive-pack flush until matching repository events are promoted and queryable. Sideband clients receive progress during long Git processing and post-push repository alignment.
- Batched the one-time deletion-request lifecycle migration so large databases do not remain unavailable while historical requests are reconciled.
- Applied NIP-01's lowest-event-ID tie-break to same-second repository state replacements in purgatory.
Security
- Kept relay-owner private keys out of process arguments by loading NixOS
secret files through a protected systemd credential. Empty or invalid
configured keys now stop startup instead of rotating identity, generated
fallback keys use mode
0600, and existing fallback files are restricted before being read.
1.2.0 - 2026-07-06
Fixed
- Prevented redundant NIP-09 deletion requests from polluting relay storage and query results.
Added
- Implemented repository lifecycle handling for removals caused by NIP-09 deletion requests, NIP-62 requests-to-vanish, repository blacklist matches, and removal from whitelist. Removed repository scopes now archive git data and move data through holding/recovery flows with a 90-day default retention period, cascade-delete related events that lose their accepted-reference path, roll back deleted state-event versions where possible, and keep served nostr state aligned with git refs. This is an enabler for moderation features.
Changed
-
Stream git smart HTTP responses instead of buffering full responses before sending them to clients, improving behavior for long-running fetch and push operations.
-
Upgraded dependencies: rust-nostr to
0.45.0-alpha.3from a patched version to enable publishing ngit-grasp to crates.io, a Rust toolchain bump via Nix flake upgrade, and other Rust dependencies.
Fixed
- Wrapped
receive-packerror pkt-lines in sideband framing so git clients receive push rejection messages correctly during smart HTTP pushes.
1.1.0 - 2026-05-22
Added
- GRASP-06 contributor PR submission endpoint (
NGIT_GRASP06_ENABLE, default off). When enabled, the relay accepts unauthenticatedgit pushofrefs/nostr/<event-id>to/prs/<npub>/<identifier>.gitfrom any contributor, even for repositories this relay has no accepted announcement for. The corresponding PR (kind 1618) or PR Update (kind 1619) event is accepted into purgatory when itsclonetag names this relay's/prs/<signer>/<d>.gitendpoint and itsatag's d-tag matches the URL identifier. When the event and the push match (signer, d-tag, c-tag commit) the event is released from purgatory and the ref is mirrored into any accepted-announcement repos on this relay. Empty/prs/repos (probe pushes, mismatched events) are garbage-collected inline at the three runtime sites that can leave them empty (receive handler at end of push, PR-event policy when discarding a mismatched scoped placeholder, purgatory sweep when a scoped placeholder expires without a matching event) plus a one-shot startup scan that removes any zero-ref/prs/bare repos left behind by a previous run (crash, mid-cleanup failure, or shutdown with unresolved scoped placeholders). GRASP-06 is advertised in NIP-11supported_graspswhen enabled. See how-to/enable-grasp-06.md and explanation/grasp-06-contributor-pr-submission.md.
Fixed
- Handle
HEADrequests forinfo/refsendpoints (previously returned 405).
1.0.2 - 2026-04-10
Fixed
-
Replacement announcements (kind 30617) for a purgatory entry were being saved to the database immediately, bypassing the purgatory gate. When a second copy of the same announcement arrived (e.g. via sync from another relay) while the original was still in purgatory awaiting git data, the policy returned
Acceptinstead ofAcceptPurgatory, causing the event to be stored without the corresponding git data or state events ever arriving. The fix returnsAcceptPurgatoryfor replacements of purgatory entries so the updated event is held in purgatory until git data arrives. -
Repository identifiers containing characters that require percent-encoding in URLs (e.g. spaces, emoji) are now accepted and served correctly. NIP-01 places no restriction on
dtag values and NIP-34 only recommends kebab-case without mandating it, so rejecting non-kebab identifiers was overly strict. Identifiers are stored verbatim on disk and percent-encoded when used in URLs, per thenostr://clone URL spec formalised in NIP-34 PR #2312 and the GRASP-01 HTTP path spec. The landing page clone URL now also correctly percent-encodes the identifier. -
--git-diris now passed as a global git option (before the subcommand) incheck_repo_empty, fixing compatibility with git versions that require global options to precede the subcommand.
Changed
-
Remove arbitrary default max connections limit; when
NGIT_MAX_CONNECTIONSis unset the relay imposes no connection cap, deferring to OS fd limits and infrastructure controls -
Added
cleanup-empty-repossubcommand to remove stale events for empty git repositories
1.0.1 - 2026-02-27
Fixed
- Push authorization now correctly ignores
refs/tags/<name>^{}peeled-tag entries in state events (kind 30618). These entries are git's internal notation for the dereferenced commit behind an annotated tag and are never sent as part of a push. Previously, their presence in the state event causedcan_satisfy_stateto reject valid annotated-tag pushes because the would-be ref state after the push did not include the spurious^{}entry, making the exact-equality check fail.
Changed
- Push auth rejections now send the reason to the git client via ERR pkt-line (e.g. "authorisation failed: No state events in purgatory") instead of a generic HTTP 403, so users see actionable error messages directly in their terminal
1.0.0 - 2026-02-26
Initial release of ngit-grasp, a GRASP relay implementation in Rust.