feat(grasp06): add NGIT_GRASP06_ENABLE feature flag and NIP-11 advertisement

Introduces the `grasp06_enable` config flag, default false, wired
through all four config-sync locations (src/config.rs, .env.example,
docs/reference/configuration.md, nix/module.nix), and advertises the
capability in NIP-11 when the flag is on.

Behaviour:
- When NGIT_GRASP06_ENABLE=true: NIP-11 supported_grasps includes
  "GRASP-06" (after GRASP-01 and GRASP-02). No /prs/ routing or
  event-acceptance changes yet — those land later.
- When disabled (default): unchanged.

Tests turning green:
- test_nip11_advertises_grasp_06_when_enabled (integration, with_grasp_06)
- test_nip11_advertises_grasp_06_when_enabled (unit, src/http/nip11.rs)
- test_nip11_grasp_06_disabled_by_default (unit)

The discovery-gate test
`test_prs_namespace_404_when_grasp06_not_advertised_no_grasp_06`
remains green and now exercises the real flag and real NIP-11 wiring.
`with_grasp_06` tests targeting /prs/ routing and event-acceptance
remain TDD-red against later work.

See: plans/grasp-06-implementation-plan.md (NIP-11 advertisement folded in)
This commit is contained in:
DanConwayDev
2026-05-15 15:48:33 +00:00
parent 96d3a1d9cd
commit 312840a885
5 changed files with 127 additions and 0 deletions
+19
View File
@@ -219,6 +219,25 @@
# Note: Cannot be used with NGIT_REPOSITORY_WHITELIST (mutually exclusive)
# NGIT_ARCHIVE_READ_ONLY=
# ============================================================================
# GRASP-06 CONTRIBUTOR PR SUBMISSION
# ============================================================================
# Enable GRASP-06 contributor PR submission endpoint at /prs/<npub>/<identifier>.git
#
# When enabled, the relay exposes an unauthenticated PR submission endpoint that
# accepts pushes of refs/nostr/<event-id> from any contributor. Security relies
# on the signed PR/PR-Update events the refs reference, not on HTTP-level auth.
#
# When disabled (default), /prs/* returns 404 and event-acceptance is unchanged.
#
# See: https://github.com/DanConwayDev/grasp/blob/main/06.md
# See: docs/explanation/grasp-06-contributor-pr-submission.md
#
# CLI: --grasp06-enable
# Default: false
# NGIT_GRASP06_ENABLE=false
# ============================================================================
# REPOSITORY WHITELIST
# ============================================================================
+45
View File
@@ -720,6 +720,51 @@ NGIT_ARCHIVE_READ_ONLY=true # Default
---
### GRASP-06 Contributor PR Submission
These options control the optional `/prs/<npub>/<identifier>.git` contributor pull-request submission endpoint per the [GRASP-06 specification](https://github.com/DanConwayDev/grasp/blob/main/06.md).
#### `NGIT_GRASP06_ENABLE`
**Description:** Enable the GRASP-06 contributor PR submission endpoint at `/prs/<npub>/<identifier>.git`
**Type:** Boolean
**Default:** `false`
**Required:** No
**Examples:**
```bash
# Enable GRASP-06 (opt-in)
NGIT_GRASP06_ENABLE=true
# Disable (default)
NGIT_GRASP06_ENABLE=false
```
**Behavior:**
- When `true`:
- `/prs/<npub>/<identifier>.git` accepts unauthenticated pushes of `refs/nostr/<event-id>`
- NIP-11 `supported_grasps` includes `"GRASP-06"`
- PR / PR-Update events naming this relay's `/prs/` endpoint in their `clone` tag are accepted to purgatory even without a matching accepted repository announcement
- Standard `<npub>/<identifier>.git` endpoint behaviour is unchanged
- When `false` (default):
- `/prs/*` returns HTTP 404
- Event-acceptance is unchanged (standard GRASP-01 rules apply)
**Security Model:**
The `/prs/` endpoint is intentionally unauthenticated — there is no NIP-98 auth, no allowlist, no quota, and no proof-of-work in v1. Validity is enforced by:
- The signed PR / PR-Update Nostr event the pushed `refs/nostr/<event-id>` references
- Per-submitter / per-identifier scoping enforced when the event arrives
- Inline ref-name validation (only `refs/nostr/<64-hex-event-id>` accepted)
- Periodic cleanup of orphan repositories with zero refs
Operators should review the design tradeoffs in [`docs/explanation/grasp-06-contributor-pr-submission.md`](../explanation/grasp-06-contributor-pr-submission.md) before enabling.
---
### Repository Whitelist
#### `NGIT_REPOSITORY_WHITELIST`
+20
View File
@@ -228,6 +228,25 @@ let
'';
};
grasp06Enable = mkOption {
type = types.bool;
default = false;
description = ''
Enable the GRASP-06 contributor PR submission endpoint at
/prs/<npub>/<identifier>.git.
When enabled, the relay exposes an unauthenticated PR submission
endpoint that accepts pushes of refs/nostr/<event-id> from any
contributor. Security relies on the signed PR/PR-Update events the
refs reference, not on HTTP-level auth.
When disabled (default), /prs/* returns 404 and event acceptance is
unchanged.
See: https://github.com/DanConwayDev/grasp/blob/main/06.md
'';
};
repositoryWhitelist = mkOption {
type = types.listOf types.str;
default = [ ];
@@ -339,6 +358,7 @@ let
NGIT_REPOSITORY_BLACKLIST = concatStringsSep "," cfg.repositoryBlacklist;
NGIT_EVENT_BLACKLIST = concatStringsSep "," cfg.eventBlacklist;
NGIT_LOG_LEVEL = cfg.logLevel;
NGIT_GRASP06_ENABLE = if cfg.grasp06Enable then "true" else "false";
} // optionalAttrs (cfg.maxConnections != null) {
NGIT_MAX_CONNECTIONS = toString cfg.maxConnections;
} // optionalAttrs (cfg.relayName != null) {
+15
View File
@@ -449,6 +449,20 @@ pub struct Config {
#[arg(long, env = "NGIT_ARCHIVE_READ_ONLY")]
pub archive_read_only: Option<bool>,
/// Enable GRASP-06 contributor PR submission endpoint at /prs/<npub>/<identifier>.git
///
/// When enabled, the relay exposes an unauthenticated PR submission endpoint
/// at `/prs/<npub>/<identifier>.git` that accepts pushes of `refs/nostr/<event-id>`
/// from any contributor. Security relies on the signed PR/PR-Update events
/// the refs reference, not on HTTP-level auth.
///
/// Default: false. This is an opt-in feature that adds an unauthenticated
/// write surface; operators must understand the tradeoffs (see GRASP-06 spec
/// and `docs/explanation/grasp-06-contributor-pr-submission.md`) before
/// enabling it.
#[arg(long, env = "NGIT_GRASP06_ENABLE", default_value_t = false)]
pub grasp06_enable: bool,
/// Repository whitelist: comma-separated list of npub/identifier/npub/identifier entries
/// Formats: "npub1...", "npub1.../identifier", "identifier"
/// When set, only announcements matching the whitelist AND listing the service are accepted
@@ -751,6 +765,7 @@ impl Config {
archive_whitelist: String::new(),
archive_grasp_services: String::new(),
archive_read_only: None,
grasp06_enable: false,
repository_whitelist: String::new(),
repository_blacklist: String::new(),
event_blacklist: String::new(),
+28
View File
@@ -67,6 +67,9 @@ impl RelayInformationDocument {
supported_grasps.push("GRASP-05".to_string());
}
supported_grasps.push("GRASP-02".to_string());
if config.grasp06_enable {
supported_grasps.push("GRASP-06".to_string());
}
// Build curation field for archive read-only mode or repository whitelist
let repository_config = config.repository_config();
@@ -281,4 +284,29 @@ mod tests {
assert!(curation.contains("whitelisted repositories"));
assert!(curation.contains("with or without service listing"));
}
#[test]
fn test_nip11_grasp_06_disabled_by_default() {
let config = Config::for_testing();
let doc = RelayInformationDocument::from_config(&config);
// GRASP-06 must not be advertised when the flag is off.
assert!(!doc.supported_grasps.iter().any(|g| g == "GRASP-06"));
}
#[test]
fn test_nip11_advertises_grasp_06_when_enabled() {
let mut config = Config::for_testing();
config.grasp06_enable = true;
let doc = RelayInformationDocument::from_config(&config);
// GRASP-06 must be advertised when the flag is on, alongside the
// standard GRASP-01 / GRASP-02 entries. Order is "GRASP-06 last"
// to match the insertion order in from_config().
assert_eq!(
doc.supported_grasps,
vec!["GRASP-01", "GRASP-02", "GRASP-06"]
);
}
}