ngit-grasp

ngit-grasp
by DanConwayDev _@danconwaydev.com

A self-hostable Nostr relay and Git server for decentralized repositories.

ngit-grasp is a production-ready Rust implementation of GRASP (Git Relays Authorized via Signed-Nostr Proofs). It combines a Nostr relay with Git Smart HTTP, validates repository writes against signed Nostr state, and synchronizes repositories across decentralized Git hosting providers.

First release: Aug 20, 2026, 5 total releases.

Most recent release: Sep 25, 2026.

Website

Appears in 0 app stacks.

0 sats / 0 zaps received in the past year.

Sats Received

Underlying data available via MCP: app_zaps, app_releases.

Zap Count

Underlying data available via MCP: app_zaps, app_releases.

Releases

  • Sep 25, 2026 3.0.5
    Improve live sync startup, bound retries against unhealthy relays, fix Git push races and stalled uploads, and refresh compatible dependencies. ### Fixed - Let relays establish live sync while other relays reconcile history, by moving slow reconciliation and paced historic requests out of the shared sync actor into bounded background workers. Space connection starts at least 250 ms apart while retaining the eight-worker connection cap. - Apply independent exponential cooldowns to repeated rate-limit refusals and
    More…
    incomplete sync requests, from 65 seconds up to one hour. Queued requests respect active policy, rate-limit, and request-failure pauses; reconnects also enforce transport backoff. A successful connection alone does not reset recovery backoff. Preserve working live subscriptions and pending live repairs while history waits without occupying global worker capacity. - Back off failed participant mailbox fetches and NIP-65 identity queries independently per source, from five minutes up to one hour. Apply their deadlines to query admission and discovery-only reconnects, preserving pauses across reconnects and inventory growth, including newly discovered authors. Only a successful query of the corresponding type resets its failure delay; independent repository and live-sync work remains eligible. - Pause deterministic invalid or oversized filters instead of retrying unchanged requests. Regroup only explicit filter-count refusals, and ignore those for already-retired subscriptions so late responses cannot incorrectly pause replacement coverage for 24 hours. - Reconcile NIP-77 against the actual local event inventory on the intended relay, avoiding downloads of already-stored history. Keep reconciliation read-only and incoming payloads subject to the normal write policy. - Reuse installed core live filters during historic retries, preventing duplicate subscriptions from exhausting relay budgets. - Retire partially admitted or incomplete historic batches on admission failure, timeout, or closure before EOSE. Release their subscriptions, bound SDK retention with an auto-close deadline, and leave coverage unconfirmed for recovery instead of retaining stalled work or accepting partial history. Account for locally stored events and delivered events retained for dependency recovery before declaring history incomplete, avoiding repeated payload fetches and false remote-history failures. Preserve dependency IDs and relay hints for later maintainer changes without fetching Git data before state authorization. Bound recovery per event ID so progress on new events cannot renew older failures' retry budgets, rotate pending IDs fairly, and include bounded ID samples in background recovery logs. - Pace descendant fallback cycles with a one-minute refresh delay, retaining overlap and immediate baselines for changed frontiers. Report missing semantic fallback metadata as scheduled recovery rather than a subscription-creation error, and move routine batch-completion bookkeeping to debug logs. - Make concurrent thin-repository creation idempotent for the same identifier family and object format, preserving the winning view's refs and rejecting conflicting existing paths. - Accept Git pushes whose targets were already applied by background state promotion after ref advertisement, while retaining authorization for changed targets and Git's protection against concurrent ref updates. Validate PR refs independently so mixed pushes do not require state authorization for unchanged branches. Verify already-completed deletions without writing refs, including when mixed with new updates. Preserve per-ref outcomes for ordinary pushes and hold verified refs locked across atomic pushes. Keep rejected refs from being immediately overwritten by post-push state promotion. - Drain Git output while uploading push data, preventing pipe deadlocks when receive-pack produces progress or diagnostics before consuming the full pack. Require complete Git status reports and a successful subprocess exit before completing merged push responses, avoiding false success after interruption. - Reject PR and PR-update events missing commit metadata as terminal invalid events, avoiding repeated recovery attempts for immutable malformed data. - Omit raw rejected relay payloads from SDK diagnostics while retaining URLs and rejection reasons. Include repository, process exit status, and stream outcome in Git fetch failure diagnostics, and log client-cancelled upload-pack streams at debug level. ### Changed - Refresh compatible Cargo dependencies, including hyper-util 0.1.21 and transitive library and build-tool updates. Keep the Nostr SDK/database and WebSocket API versions unchanged.
  • Sep 21, 2026 3.0.4
    ### Fixed - Fix queued sync requests bypassing relay rate-limit backoff and continuing to send requests during the cooldown. Rate-limit signals now take effect even when sync event processing is blocked, and repeated signals no longer flood warning logs. - Preserve repository membership while a pending announcement is saved, so concurrent reconciliation cannot restore obsolete Git state. - Retain live roots received while their accepted repository announcement is waiting for batch processing, so their discovery work is not lost.
    More…
    ### Dependencies - Update compatible Cargo dependencies, including `nostr-sdk` 0.45.4. The embedded relay now closes affected live subscriptions when their event buffer overflows, allowing clients to detect gaps and resubscribe for stored events. It also checks the connection limit before the WebSocket handshake. - Update `negentropy` to 0.5.1, reducing allocations during reconciliation.
  • Sep 18, 2026 3.0.3
    This patch improves repository-state correctness, startup recovery, and relay responsiveness, and makes audit probes more accurate. It also includes released Nostr SDK fixes and more deterministic test fixtures. ### Fixed - Scope read-only audit state selection to the selected repository's confirmed maintainers, excluding unrelated repositories with the same identifier. Audit probes now also report unexpected branches and tags, including refs left behind when the winning state is empty.
    More…
    - Select one complete repository state in the read-only audit probe, using the lower event ID for same-second ties. Older states no longer contribute removed branches or tags to the expected Git refs. - Honor lower-ID same-second replacements in the public owner-repository sync helper while preserving replay suppression. - Restore relay-owner profiles and relay lists using the lower event ID when fetched versions share a timestamp. - Honor same-second lower-ID replacements when de-listing served repositories, capturing superseded history, and choosing rollback or post-deletion state. History capture time no longer overrides the original events' ID tie-break. - Apply the lower-event-ID tie-break to same-second purgatory replacements. Git push authorization now selects the preferred matching state regardless of arrival order, and a winning announcement that removes this service evicts its purgatory entry and associated repository data. - Preserve all persisted root events when rebuilding sync state at startup. Purgatory cleanup can no longer remove a partially reconstructed repository and leave its older threads untracked until another restart. - Upgrade the Nostr SDK and database dependencies to released 0.45 patch versions. NIP-77 database scans now run off async workers, avoiding runtime stalls that can delay unrelated relay connections. Reconciliation excludes expired events, and NIP-77 continuation messages no longer consume the query-start allowance. - Pick up upstream relay validation fixes that reject reposted protected events and verify the declared proof-of-work target as well as the event hash. - Require EOSE before accepting background discovery history, instead of treating a disconnected or timed-out partial response as a completed fetch. - Preserve background discovery relay backoff across idle-connection cleanup so unavailable mailbox and profile sources do not restart their retry history. - Render metrics on a blocking worker and serialize scrapes so repository counting does not occupy async workers serving relay and Git requests. - Keep periodic recovery checkpoints off async workers, release snapshot locks before disk I/O, and finish active checkpoints before the shutdown snapshot. - Avoid delayed-acknowledgement stalls in small WebSocket and Git responses by enabling `TCP_NODELAY` on accepted connections.
  • Sep 11, 2026 3.0.2
    This release contains no production runtime changes. It improves release and deployment plumbing, source-owned documentation exports, canonical project references, and build-time test portability. ### Added - Export a deterministic, side-effect-free command and configuration schema for pinned downstream documentation generation. ### Changed
    More…
    - Publish tagged static archives as signed NIP-82 releases, using the `main` channel for stable versions and a channel derived from the prerelease label for release candidates. - Publish release container images as Nostr kind-30624 repositories backed by redundant Blossom storage. Stable images receive version and `latest` tags; prereleases receive version and channel tags. Maintainers can backfill an exact prior-release tag through CI without moving a stable channel backwards. - Point GRASP implementation references and maintainer guidance at their canonical Nostr Git sources. ### Fixed - Make shallow-migration and bare-repository test fixtures deterministic across Git default-branch configurations, while keeping routine `git init` output out of test logs.
  • Aug 20, 2026 3.0.0
    ### Summary ngit-grasp 3.0.0 adds self-hosted private repositories through GRASP-08, with the protocol designed so hosting providers can offer managed private services later. Proactive Sync Plus (GRASP-03) enables maintainer-based moderation and simpler, more performant Nostr Git clients. This major release also includes significant security and performance fixes;
    More…
    an immediate upgrade is recommended. Its Git storage migration makes the v2-to-v3 upgrade one-way without restoring a pre-upgrade snapshot. Plan for downtime while the first v3 startup migrates Git data. The largest GRASP service tested so far, `relay.ngit.dev`, took approximately 51 minutes. ### Breaking changes - Version 3 performs an automatic, one-way migration of `NGIT_GIT_DATA_PATH` from complete per-owner and `/prs/` repositories to thin views backed by shared identifier families. Version 2 does not coordinate reads and writes through this layout and must not be run against migrated Git data. Rollback requires restoring the Git and relay-data snapshot taken before the v3 launch, not only downgrading the binary. - The v3 storage migration finishes before the HTTP server starts listening, so operators must plan for relay downtime during the upgrade. The largest GRASP service migrated so far spent 51 minutes migrating 2,294 repository views into 2,014 identifier families, retired 2,291 backups, and retained three for automatic integrity repair. - Maintainer authorization now uses reciprocal membership. A pubkey listed by a confirmed maintainer is only invited and cannot publish authoritative repository state until its own kind-30617 announcement for the same identifier lists back an existing confirmed maintainer. At release time, this changes the effective authorization state of zero live repositories; it is breaking for future invitations because they now require an explicit acceptance step. Repository announcements now support the NIP-34 `M` (lead maintainer), `m` (co-maintainer), and `o` (moderator) role tags. Active `M` and `m` entries define maintainers; `o` does not grant repository-state authority. The deprecated `maintainers` tag is used as a fallback only when none of those role tags are present. - Added `trusted_proxy_cidrs` to the public `Config` struct. Rust consumers that construct `Config` with a struct literal must provide it. - Added `base_path` to the public `Config` struct. Rust consumers that construct `Config` with a struct literal must provide it. - Added `startup_integrity_identifiers` to the public `Config` struct. Rust consumers that construct `Config` with a struct literal must provide it. ### Security - Reject unsuitable peer-advertised NIP-65 relay URLs before they can occupy mailbox selection slots, overlays, probe queues, or metrics: URLs failing the static outbound-target checks, `.onion` hosts (temporary mailbox policy), URLs matching nostr-watch's 52-word generated-path rule, 64-hex path segments, hostname-repeating path segments, and URLs over 2,048 bytes. These are single-URL rules; relay lists are never rejected wholesale, and legitimate path-based relays (`/inbox`, `/outbox`, language and versioned paths, semantic queries) remain accepted. - Canonicalize accepted NIP-65 relay URLs and bound every per-author selection to at most four relays per purpose, taken in published tag order after deduplication so trailing-root-slash and default-port variants of one relay consume a single slot. Live inbox coverage and owner/maintainer repository inboxes select `read`/unmarked relays, identity refresh selects `write`/unmarked relays, and participant history prefers `write`/unmarked relays with read-only relays filling remaining slots. The bound is a hard internal constant, so one hostile or machine-generated kind `10002` list cannot inflate mailbox inventory. Canonical keys now match the connection map, fixing trailing-root-slash mailbox relays that previously could never start their history probe. - Bound every sync-actor notification channel and require `auth-required` retry IDs to own a current subscription-ledger permit. Repeated terminals, forged subscription IDs, and delayed worker results can no longer create unbounded retained memory. - Added opt-in trusted-proxy CIDRs so WebSocket connection policy, per-IP metrics, abuse indicators, and logs can use the real client address without trusting spoofable forwarding headers from arbitrary direct peers. - Reject non-canonical and path-traversing repository coordinates after URL decoding across repository landing pages, Git Smart HTTP, and GRASP-06. All tagged releases through v2.1.2 could resolve crafted requests outside the requested repository namespace, allowing unauthenticated reads of Git repositories accessible to the service. With GRASP-06 enabled, crafted PR submissions could also write Git objects and PR refs into another hosted repository without its maintainer authorization. Any deployment that enabled GRASP-06 on a tagged build through v2.1.2 should treat its hosted Git repositories as potentially containing unauthorized objects or refs until it completes the v3 integrity checks. Conforming Nostr Git clients that resolve refs from valid signed State, PR, and PR Update events are not expected to accept this unauthorized data; direct Git consumers do not have that protection. Operators must upgrade to v3.0.0. By default, every v3 startup runs a non-blocking authorization-integrity pass that compares every served branch, tag, `HEAD`, and `refs/nostr/*` ref with the accepted State, PR, and PR Update events (including precisely scoped in-flight events). Owner-view PR refs require either confirmed-maintainer overlap with the target or an exact standard clone URL naming that owner and identifier on the service; similar, foreign, and `/prs/` URLs do not count. It repairs unambiguous differences and emits `manual_inspection=true` errors without deleting unexplained PR refs that may be evidence. GRASP-06 operators must check those logs and the terminal `Git authorization-integrity startup pass completed` summary; any non-zero `manual_inspection` or `failed` count means the named repository still requires review. `NGIT_STARTUP_INTEGRITY_IDENTIFIERS` can temporarily scope both startup integrity passes for release-candidate validation, but scoped logs establish integrity only for the named repositories. Operators must remove the scope and complete the default all-family sweep before treating the v3 upgrade as complete. The live `integrity-check --identifier` command now compares storage and event authorization together; it is check-only by default and applies safe fixes with `--repair`. ### Added - Build a reproducible, statically linked x86_64 Linux archive and checksum from every version tag. - Add `NGIT_BASE_PATH` / `--base-path` (NixOS `basePath`) so one GRASP server can be mounted below a shared domain path. WebSocket, Git Smart HTTP, GRASP-06, NIP-11, metrics, icons, landing pages, service matching, generated NIP-65 identity, and self-sync all use the configured prefix. Requests outside it are rejected. Path-mounted servers neither serve nor advertise the root-domain NIP-05 identity, and their generated kind-0 profile omits the `nip05` field. - Publish the relay-owner identity on startup as a minimal kind-0 profile with the scheme-less public URL as `name`, `bot: true`, and `_@domain` NIP-05 for domain-root deployments, plus a kind-10002 list naming this relay as its sole read/write relay. An operator-customized profile survives restarts, and no identity event — stored or generated — is published before the local database and at least one user-index relay have been checked for that kind. Every send is preceded by a per-relay re-check: an identity found on a user-index relay (for example after a local database wipe) is adopted locally and never overwritten, so the relay only fills identity gaps on the indexes, retrying transient failures with a capped backoff. In private mode the identity is seeded locally but never published, so a private relay's existence is not advertised. The relay also trusts events signed by its own key so that key can operate an `ngit-ci` coordinator, but only for kinds without a dedicated admission policy: owner-signed NIP-34 repository events pass the normal announcement, state, and PR policies, and NIP-09/NIP-62 tombstones still prevent replaying retracted owner events. - Serve the relay owner's public key as the NIP-05 `_@domain` identity from `/.well-known/nostr.json`. NIP-11 advertises NIP-05 only when requested at the domain root (`/`); relays mounted below a path do not claim support. - Add fixed-cardinality aggregate metrics for important long-lived sync state and document the producer, cleanup owner, bound, and terminal behavior of every peer-influenced transient subsystem. - Add bounded, operator-configurable inbox fallback coverage when a successful Sync+ user-index query finds no accepted NIP-65 relay list for a root author. - Add a default-on `NGIT_SYNC_PLUS_ENABLED` opt-out and advertise GRASP-03 in NIP-11 only when the Sync+ overlay is effective. - Recover repository-event descendants which reference a direct thread member but omit the repository and root-event tags. When the per-connection ledger can retain complete descendant coverage while preserving control and historic capacity, those filters stay live; otherwise bounded EOSE-closing history provides eventual coverage. Direct replaceable and addressable members contribute both their event ID and NIP-01 coordinate, covering descendants which use `a`, `A`, or coordinate-valued `q` tags. The frontier is deliberately non-recursive and connection sharding remains unnecessary. ### Changed - Retire identifier-family migration backups family by family. Once every view of a family is installed, each backup is verified (family contains every Git-readable backup object, thin views match their journal snapshot, family packs are indexed and valid), and the ordinary family integrity report must be healthy before deletion. Unhealthy families retain their backups for automatic repair. Healthy backups are deleted before the next family migrates, bounding peak migration disk overhead to the family in flight. Unindexed legacy packs are quarantined by content under `.grasp/migration/unindexed-packs/`, unverifiable backups are retained with a warning, backup-directory removals are fsynced before their journals are removed, and completed installations whose backups were already removed manually start unchanged. - Detect and reconcile incomplete object graphs inherited from legacy storage. The v3 migration preserves the legacy repository and its backup; the online integrity worker then requests missing objects from every accepted clone source and logs any family that remains incomplete without making it less available. A server-side `shallow` marker specifically identifies repositories created by the depth-one fallback in untagged development builds between 2026-01-05 (`623cae5`) and 2026-01-12 (`f25eea8`); no tagged v1 or v2 release shipped that behavior. Missing ancestors without a `shallow` marker are not attributed to that bug and can require a complete maintainer clone or bundle when every announced server inherited the same incomplete history. - Scope bare log levels to ngit-grasp while keeping dependencies at warnings; explicit tracing filter expressions remain unchanged. - Keep per-event discovery and validation details at debug, retain aggregate sync outcomes at info, and report unsupported NIP-77 once per connection instead of warning once per filter. - Treat routine client disconnects, incomplete HTTP/WebSocket sessions, and Git missing-object probes as debug diagnostics while preserving internal service and database failures as errors. - Metrics compatibility: removed the `ngit_sync_naughty_relay_info{relay,category,reason}` metric because its relay and raw reason labels were peer-controlled and unbounded. Use the unchanged `ngit_sync_naughty_relays_total{category}` metric for fixed-cardinality aggregate quarantine counts. - Minimise live-subscription churn as repository coverage grows: stable full core groups and descendant groups remain open. The mutable core tail is repacked in full when doing so releases slots; otherwise only the smallest useful subset is replaced to absorb new filters. This accounts for both filter-count and serialized-byte limits. Repository filter chunks are deterministic, and failed tail replacement restores the previous tail. ### Fixed - Restore cold rejected-maintainer recovery after policy admission pre-saves the newly authorized announcement for membership reconciliation. An exact duplicate from that intentional pre-save now still broadcasts the announcement and expands its state/Git dependencies, while a genuinely superseded replaceable event remains classified as a duplicate without downstream fan-out. - Discover historical repository roots and descendants from the NIP-65 inboxes of accepted repository owners and maintainers. Public Sync+ instances probe exact repository coordinates and every known root on those authors' bounded, sanitized read/unmarked inboxes through paced, byte-bounded history workers. This coverage is deliberately historical-only: owning or maintaining a repository never adds an author's inbox relays to ordinary persistent live repository targets, while accepted root authors retain the pre-existing live inbox coverage. Private instances continue to withhold repository coordinates, and the ordinary write policy and persistent deletion tombstones remain authoritative. - Retire peer-closed outbound subscriptions from rust-nostr's desired registry without interrupting NIP-42: the first `auth-required` response keeps the same subscription and ledger slot for one authenticated retry, while a repeat refusal becomes a 24-hour policy pause. This prevents authentication replay and live repair from multiplying rejected IDs without bound. Policy detection precedes descendant fallback, while an unclassified live closure waits for the ordinary cooldown before one repair attempt. - Bound duplicate repository Git acquisition to one primary and one delayed distinct-domain hedge, and recover from silent transports without imposing a total fetch deadline. Continuously active large transfers remain unbounded; a process group silent for five minutes is terminated gracefully then forcibly, after which remaining sources are tried sequentially. Concurrent object acquisition does not write `FETCH_HEAD`, run automatic maintenance, or infer source speed from a shared object database.