Files
ngit-grasp/Cargo.toml
T
DanConwayDev 36634dc326 fix(private-repos): restore self-subscription in private mode
The SelfSubscriber feeds the sync manager's repository index from the
service's own accepted events. It dialled our own public WebSocket
endpoint with an unauthenticated client, which a private instance's
NIP-42 gate correctly refused: the gate runs in the HTTP layer, ahead of
LocalRelay, and cannot distinguish our own process from any other
anonymous dialler. Announcements accepted at runtime therefore never
reached the sync index until a restart rebuilt it from the database,
stalling proactive sync and the dynamic membership derived from accepted
relay owners — worst on exactly the private-to-private mirroring GRASP-08
exists to enable.

Attach the subscriber to the embedded relay in-process instead. A custom
WebSocketTransport hands LocalRelay one end of an in-memory duplex pair
and keeps the other, so the client gets an ordinary relay session with
the same framing, subscription handling, and post-save broadcast, minus
the listener, the auth gate, and the network round trip. This replaces
the loopback dial in both modes: the public-mode feed is identical in
content and strictly more reliable, and it removes a self-directed
reconnect loop.

Chosen over attaching the relay owner key as a NIP-42 authenticator plus
adding the owner pubkey to the effective member set. That alternative
works, but widens the member set and the authenticated surface to solve a
problem that is not authentication: there is no remote party here. The
in-process route needs no key, no membership entry, and no configuration,
and nothing reaches the subscriber that the relay did not already accept
and persist.

Correctness assumptions: LocalRelay applies no NIP-42 or query policy of
its own — private-mode access control lives entirely in the HTTP layer —
so an in-process session is exactly a local session, not a bypassed
remote one. Both ends speak raw WebSocket framing over the duplex with no
HTTP upgrade, matching take_connection's Role::Server. The session
consumes one connection permit, as the loopback dial did.

The GRASP-08 regression test no longer restarts the relay over persistent
LMDB: it publishes an announcement at runtime and asserts sync
connections to both referenced relays, which is only possible if the live
feed reached the index. Verified to fail against the previous
implementation (60s deadline, no connections) and pass with this one.

req_concurrency's source relay now applies the production outbound target
policy. Its scenario lists a proxy URL in the announcement, and with a
reliable live feed the source discovers that URL — a distinct host:port
that happens to front itself — as an event-directed sync target and opens
its own REQ traffic through the proxy, contending for a budget the test
means to measure for the syncing relay alone. That behavior is
pre-existing and was already reachable after a restart; only its timing
changed. The policy keeps the source scenery without weakening the
assertion.

Deliberately excluded: neg_concurrency shares that topology but passes
unchanged, so its fixture is left alone; the duplicate NIP-11 fetch
between the pre-dial preflight probe and the post-connect hint fetch is
untouched.

Validation: cargo clippy --all-targets -D warnings; cargo test --lib (793
passed); cargo test --test private_mode --test sync --test
outbound_policy --test purgatory_sync (255 passed, including the full
sync suite under parallel load). req_concurrency's startup-burst test
passed 4/4 isolated runs after the fixture change.
2026-08-15 20:58:41 +00:00

166 lines
4.2 KiB
TOML

[package]
name = "ngit-grasp"
version = "2.1.2"
edition = "2021"
authors = ["ngit-grasp contributors"]
license = "MIT"
description = "A GRASP (Git Relays Authorized via Signed-Nostr Proofs) implementation in Rust"
repository = "https://gitworkshop.dev/danconwaydev.com/ngit-grasp"
[dependencies]
# Async runtime
tokio = { version = "1.35", features = ["full"] }
# HTTP server (hyper for relay integration)
hyper = { version = "1.4", features = ["full"] }
hyper-util = { version = "0.1", features = ["tokio", "server", "http1", "http2"] }
http-body-util = "0.1"
# Nostr
#
# The stable 0.45 series contains the upstream NEG-OPEN handling fix used by
# the embedded relay. Caret requirements accept compatible patch releases.
nostr = "0.45.0"
# `local-relay` carries the embedded relay implementation, previously the
# separate `nostr-relay-builder` crate.
nostr-sdk = { version = "0.45.0", features = ["local-relay"] }
nostr-lmdb = "0.45.0"
nostr-memory = "0.45.0"
# Utilities
# SHA-1 for the `Sec-WebSocket-Accept` handshake. `nostr` uses this same crate
# internally but stopped re-exporting it as `nostr::hashes` in 0.45,
# so depend on it directly rather than pulling in a second hash implementation.
bitcoin_hashes = "0.14"
futures-util = "0.3"
tokio-tungstenite = { version = "0.28", default-features = false }
# Message type in `nostr-sdk`'s `WebSocketTransport` signatures, which the
# in-process self-subscription transport implements. `nostr-sdk` does not
# re-export it. No features requested: the build keeps whatever `nostr-sdk`
# already selects.
async-wsocket = { version = "0.17", default-features = false }
base64 = "0.22"
flate2 = "1.0"
tar = "0.4"
fs2 = "0.4"
ipnet = { version = "2", features = ["serde"] }
libc = "0.2"
# Metrics
prometheus = { version = "0.14", features = ["process"] }
dashmap = "6"
lazy_static = "1.4"
# Data structures
indexmap = "2"
# Random (for startup jitter)
rand = "0.10"
# Serialization
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
# Logging
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
# Configuration
dotenvy = "0.15"
clap = { version = "4.5", features = ["derive", "env"] }
# Error handling
anyhow = "1.0"
# Async traits
async-trait = "0.1"
# Temporary directories (used for GRASP-06 empty-repo synthesis)
tempfile = "3"
reqwest = { version = "0.13", default-features = false, features = ["native-tls"] }
# Git (for future use)
# git-http-backend = "0.3"
[dev-dependencies]
# Testing
grasp-audit = { path = "grasp-audit", version = "0.2.0" }
tempfile = "3"
tokio = { version = "1.35", features = ["full", "test-util"] }
tokio-tungstenite = "0.28.0"
[lib]
name = "ngit_grasp"
path = "src/lib.rs"
[[bin]]
name = "ngit-grasp"
path = "src/main.rs"
[[test]]
name = "nip09_announcement_cascade"
path = "tests/lifecycle/nip09_announcement_cascade.rs"
[[test]]
name = "nip09_blacklist_ops"
path = "tests/lifecycle/nip09_blacklist_ops.rs"
[[test]]
name = "nip09_cascade_event_types"
path = "tests/lifecycle/nip09_cascade_event_types.rs"
[[test]]
name = "nip09_disrespector"
path = "tests/lifecycle/nip09_disrespector.rs"
[[test]]
name = "nip09_git_archive_cleanup"
path = "tests/lifecycle/nip09_git_archive_cleanup.rs"
[[test]]
name = "nip09_holding_cleanup"
path = "tests/lifecycle/nip09_holding_cleanup.rs"
[[test]]
name = "nip09_holding_db"
path = "tests/lifecycle/nip09_holding_db.rs"
[[test]]
name = "nip09_multi_maintainer"
path = "tests/lifecycle/nip09_multi_maintainer.rs"
[[test]]
name = "nip09_recovery"
path = "tests/lifecycle/nip09_recovery.rs"
[[test]]
name = "nip09_state_cascade"
path = "tests/lifecycle/nip09_state_cascade.rs"
[[test]]
name = "nip09_state_multi_maintainer"
path = "tests/lifecycle/nip09_state_multi_maintainer.rs"
[[test]]
name = "nip09_validation"
path = "tests/lifecycle/nip09_validation.rs"
[[test]]
name = "deletion_request_retention"
path = "tests/lifecycle/deletion_request_retention.rs"
[[test]]
name = "nip62_lifecycle"
path = "tests/lifecycle/nip62_lifecycle.rs"
[[test]]
name = "nip62_validation"
path = "tests/lifecycle/nip62_validation.rs"
[[test]]
name = "replaceable_history"
path = "tests/lifecycle/replaceable_history.rs"
[workspace]
members = [".", "grasp-audit"]