From f632ce7de7f609cae5c8d5932ec81b26fefce346 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Mon, 17 Aug 2026 10:29:50 +0000 Subject: [PATCH] docs: define administration and configuration vision Motivation: Operators need a coherent path from declarative production configuration to approachable single-binary administration. Maintainers also need scoped private analytics without service-wide authority. Approach: Define a NIP-98-authenticated, NIP-86-compatible control plane with namespaced extensions, an embedded React client, a provenance-aware settings registry, and a lower-priority SQLite override store. Use repository stats plus changerelaydescription as the first read/action vertical slice. Correctness assumptions: Command-line, credential, environment, NixOS-rendered, and dotenv sources remain authoritative over database overrides. Repository authorization reuses current recursive maintainer rules, and lifecycle mutations continue through existing serialization facades. Deliberately excluded: This documents direction only. It does not implement the API, UI, database, quota accounting, or live reconfiguration. It also does not promise that every setting will become API-writable. Validation: Ran git diff --cached --check and verified every local link introduced by the new vision and index entries resolves. A broader scan found an unrelated pre-existing broken documentation link, which is left outside this atomic change. --- docs/README.md | 2 + docs/explanation/README.md | 15 + docs/explanation/administration-vision.md | 434 ++++++++++++++++++++++ 3 files changed, 451 insertions(+) create mode 100644 docs/explanation/administration-vision.md diff --git a/docs/README.md b/docs/README.md index ab02cb9..af2e51a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -58,6 +58,8 @@ WORKING │ How-To │ Reference │ - **[Architecture Overview](explanation/architecture.md)** - System design and components - **[Inline Authorization](explanation/inline-authorization.md)** - Why we chose this approach +- **[Administration Vision](explanation/administration-vision.md)** - + Nostr-authenticated management, embedded UI, and runtime configuration - **[Comparison with ngit-relay](explanation/comparison.md)** - How we differ from reference - **[Design Decisions](explanation/decisions.md)** - Key architectural choices diff --git a/docs/explanation/README.md b/docs/explanation/README.md index fabb701..84570a2 100644 --- a/docs/explanation/README.md +++ b/docs/explanation/README.md @@ -172,6 +172,21 @@ Explanation documentation helps you **understand concepts** and design decisions --- +### [Administration, Private Analytics, and Runtime Configuration Vision](administration-vision.md) +**Nostr-authenticated management API, embedded UI, and layered configuration** + +**Topics:** +- NIP-98 authentication and NIP-86-compatible methods +- Scoped private analytics and future quota accounting +- Embedded static administration application +- Provenance-aware configuration and database overrides +- Incremental live reconfiguration + +**Read when:** You are designing administration APIs, operator tooling, +runtime settings, or quota features + +--- + ### [Defensive Measures & Rate Limiting](defensive-measures.md) **Protection against abuse, spam, and denial-of-service attacks** diff --git a/docs/explanation/administration-vision.md b/docs/explanation/administration-vision.md new file mode 100644 index 0000000..71d04d3 --- /dev/null +++ b/docs/explanation/administration-vision.md @@ -0,0 +1,434 @@ +# Administration, Private Analytics, and Runtime Configuration Vision + +**Status:** Proposed direction; not yet implemented + +**Related:** [Architecture](architecture.md), +[GRASP-08 private services](grasp-08-private-service.md), +[Monitoring](monitoring.md), and +[Configuration reference](../reference/configuration.md) + +## Vision + +ngit-grasp should be straightforward to operate without requiring every +operator to edit environment files or maintain a separate monitoring stack. +At the same time, declarative deployments must remain authoritative and +predictable. + +The service will provide a Nostr-authenticated HTTP management API and an +embedded static web application that uses it. The API will implement the +standard NIP-86 methods that fit ngit-grasp and advertise a longer tail of +namespaced GRASP and ngit-grasp extensions. It will return private operational +information to an authenticated caller as well as accept authorized actions. + +The intended operator experience is: + +- a hobbyist can start one binary, open `/admin/`, authenticate with a Nostr + signer, inspect the service, and configure settings that were not fixed by + the launch environment; +- a NixOS, container, or other declarative deployment keeps command-line, + credential, and environment-managed values read-only through the API; +- a repository maintainer can inspect private information and perform only + actions scoped to repositories they maintain; +- a service administrator can inspect service-wide analytics and change the + settings and operational state delegated to the control plane; and +- monitoring systems can continue scraping Prometheus without making + Prometheus the source of truth for accounting, quotas, or configuration. + +This is a control plane around the existing relay and Git data plane. It must +reuse the existing authorization and repository lifecycle rules rather than +creating a second interpretation of repository ownership. + +## Interface boundaries + +The same process serves several interfaces with different purposes: + +| Interface | Purpose | Data model | +| --- | --- | --- | +| Nostr WebSocket | Repository announcements, states, PRs, and related signed events | Nostr events | +| Git Smart HTTP | Repository fetch and push | Git protocol | +| NIP-86-compatible HTTP API | Private queries, analytics, settings, and actions | JSON request/response | +| `/admin/` | Human-facing management client | Embedded static application | +| `/metrics` | Machine-oriented monitoring and alerting | Prometheus exposition | + +Nostr data remains queryable over Nostr. Internal state that is not naturally a +Nostr event---effective configuration, resource usage, queue health, quota +state, and audit records---is returned through the management API. The web +application may use both interfaces and join their results in the browser. + +The management API is request/response. A private result is returned directly +to its authenticated caller over TLS; it is not published, persisted as a +public Nostr event, or broadcast to relay subscribers. Private responses use +`Cache-Control: no-store` and must not pass through a shared cache. Unsolicited +live updates are not part of the first design. Polling is sufficient initially; +an authenticated event stream can be designed later if a concrete need +appears. + +## Protocol foundation + +### NIP-98 authenticates HTTP requests + +[NIP-98](https://github.com/nostr-protocol/nips/blob/master/98.md) is the +authentication foundation, not the management API itself. A shared parser will +verify the kind-27235 event, signature, timestamp, and required tags. Explicit +validation profiles then apply different rules without accidentally weakening +one another: + +- **Standard NIP-98:** the `u` tag exactly matches the absolute request URL and + the `method` tag matches the HTTP method. +- **Management NIP-98:** standard validation plus `POST`, a mandatory payload + hash covering the exact request body, a short validity window, and replay + rejection for mutations. +- **GRASP-08 repository read:** the existing repository-root `GET` profile, + whose controlled short-window reuse is necessary for Git Smart HTTP. + +The GRASP-08 profile must not be accepted for management calls. Its reusable +credential and intentionally ignored payload are appropriate for a sequence of +Git fetch requests but not for an administrative mutation. + +### NIP-86 supplies the management envelope + +[NIP-86](https://github.com/nostr-protocol/nips/blob/master/86.md) defines a +JSON request/response protocol over HTTP at the same URI as the relay +WebSocket, selected by `Content-Type: application/nostr+json+rpc`. ngit-grasp +will: + +- implement applicable standard methods with their specified names and + parameter/result shapes; +- implement `supportedmethods` from the beginning; +- advertise extension methods through `supportedmethods`; and +- retain the standard `{ "result": ..., "error": ... }` response envelope. + +Extensions use collision-resistant names: + +- `grasp.*` for behavior intended to be portable across GRASP + implementations; and +- `ngit-grasp.*` for this implementation's settings and internal analytics. + +The exact extension names and schemas are part of the API contract and require +versioned compatibility once released. The embedded web application discovers +methods and capabilities instead of assuming that its server implements every +method known to the newest client. + +## Authentication is not authorization + +A valid NIP-98 event establishes the actor pubkey. A separate authorization +layer decides which method and scope that actor may use. + +Initial capabilities are: + +| Capability | Intended authority | +| --- | --- | +| Public discovery | Anyone | +| Own-account usage | The same pubkey and service administrators | +| Repository read | Maintainers authorized for that exact owner and identifier, and service administrators | +| Repository action | Method-specific subset of those repository maintainers, and service administrators | +| Service observation | Configured service observers and administrators | +| Service administration | Relay owner and explicitly configured service administrators | + +Repository authorization uses the existing recursive NIP-34 maintainer +calculation. Every repository-scoped request names both owner and identifier; +an identifier alone is ambiguous because several owners can announce versions +of the same project. + +On a GRASP-08 private service, membership is an additional outer admission +check. Membership grants access to the private trust domain but never implies +service-administrator authority. A valid caller who is not authorized for a +scope must receive a stable error that does not reveal whether an otherwise +private repository exists. + +The web application can hide controls that the caller cannot use, but the API +is always the security boundary. + +## Scoped analytics and future quotas + +Not every useful measurement belongs to a repository. API resources and quota +accounting have an explicit scope: + +| Scope | Examples | +| --- | --- | +| Service | Capacity, process health, connection totals, and global policy limits | +| Principal | Total storage, repository count, bandwidth allowance, and assigned tier | +| Repository | Storage, Git traffic, last accepted state, and sync health | +| Tier | Defaults, member count, and aggregate usage visible to administrators | + +The actor and charged principal are distinct. A maintainer who pushes to a +repository acts under their own signed identity, but resource usage is charged +to the repository owner or another explicitly modeled quota principal. + +Prometheus remains observational. Durable accounting counters, reservations, +period boundaries, tier assignments, and overrides belong in the control +database. This avoids high-cardinality private labels in `/metrics` and avoids +using resettable process metrics as enforcement state. + +When quotas are introduced, admission will resolve all applicable policies: + +1. a service hard ceiling; +2. the principal's assigned tier; +3. an optional principal override; and +4. an optional repository override. + +Operations must satisfy every applicable pool. Concurrent resource-consuming +operations will reserve capacity before beginning and commit actual usage or +release the reservation afterward, preventing individually valid operations +from collectively exceeding a limit. + +## Embedded management application + +A React application will be compiled to fingerprinted static assets and +included in release builds. The Rust process serves the application beneath +`/admin/`; production does not require Node.js or a separate web server. + +The application will: + +- use a NIP-07 browser signer initially and leave room for a NIP-46 remote + signer; +- never request, receive, or store an `nsec`; +- sign each management request with a body-bound NIP-98 event; +- use Nostr subscriptions for repository event data; +- use the management API for settings, analytics, accounting, and actions; +- discover supported methods and the caller's capabilities; and +- show the source, mutability, and application state of every setting. + +The bundled client and server are released together, but the API remains a +real external interface so alternative clients and automation can use it. +The client must tolerate older or newer servers through capability discovery. +Assets should be self-contained and served with a restrictive content security +policy; externally hosted scripts and fonts are unnecessary. + +## Configuration as layered, provenance-aware state + +Today `Config` is a startup snapshot populated by clap, environment variables, +`.env`, protected credentials, and defaults. Management requires an explicit +setting registry and preserved provenance rather than a plain struct whose +source has been forgotten. + +The proposed precedence, highest first, is: + +1. command-line arguments; +2. protected credentials for secret-bearing settings; +3. process environment, including values rendered by the NixOS module; +4. `.env` values; +5. control-database overrides; and +6. built-in defaults. + +NixOS is not a separate runtime source: the module renders options into +environment values or protected credentials. The API can still describe such +a setting as operator-managed and read-only. + +This ordering gives operators an unconditional escape hatch. Adding a CLI, +credential, environment, NixOS, or `.env` value immediately shadows a database +override. Removing the higher-priority value allows the stored override to +become effective again, so the API must display both the effective value and +any shadowed override and must allow an administrator to clear the override. +It must never silently pretend that a shadowed API write changed the running +service. + +Each registered setting describes at least: + +- stable API key and human description; +- value type and validation function; +- active value and configured value; +- effective source and optional stored override; +- whether the value is secret; +- whether the API may write an override; +- application mode; and +- revision for optimistic concurrency. + +Secrets are never returned. Their API representation says only whether they +are configured, their effective source, and whether rotation is possible by +another mechanism. Bootstrap settings needed to locate or secure the control +plane---relay owner secret, control-database location, database backend, and +initial listener binding---remain operator-managed initially. + +API writes use the same parser and validation logic as startup configuration. +A write includes the revision the client observed and fails on a conflicting +newer write instead of losing an update. + +## Control database + +A dedicated SQLite control database is the preferred implementation. It is +separate from the Nostr event database and repository holding/archive storage. +SQLite is a good fit for small transactional control-plane records, migrations, +auditing, and future quota accounting while preserving the single-binary +operator experience. A short implementation spike should confirm the Rust and +Nix packaging choice before this becomes permanent. + +The initial schema needs only a few concepts: + +- `setting_overrides`: key, typed JSON value, revision, actor, and timestamps; +- `audit_entries`: actor, authentication event, method, scope, before/after + summary, result, and timestamp; and +- `nip98_replays`: recently consumed authentication event IDs for mutations. + +Future migrations may add service roles, tier definitions, tier assignments, +quota overrides, usage counters, and reservations. Schema migrations run +transactionally and are versioned with the binary. + +The database is authoritative only for the control-plane layer. A row cannot +override an operator-managed source. It must also never become a second store +for Nostr repository state. + +## Applying configuration changes + +Every setting has one of three application modes: + +- **Live:** persist, validate, and update the running component atomically. +- **Restart required:** persist the configured value but leave the active + value unchanged until the next successful restart. +- **Operator only:** visible or redacted through the API but not writable there. + +A setting response distinguishes `active_value` from `configured_value` and +includes `pending_restart`. The UI must not describe a persisted restart-bound +change as active. + +The first implementation may classify most mutable settings as restart +required. Over time, individual components can consume watch channels or +explicit reconfiguration messages and graduate to live application. This is a +per-setting correctness decision, not a blanket promise: settings that change +storage identity, listener topology, or security boundaries may remain +restart-bound or operator-only indefinitely. + +A live update is successful only if persistence and runtime application agree. +If runtime application fails, the operation must report failure and leave an +unambiguous recoverable state; it must not claim success while silently running +the old value. + +## First vertical slice + +The first milestone deliberately implements one meaningful private read and +one safe action. `supportedmethods` is also required but does not count as the +read because it does not exercise resource authorization. + +### Read: `ngit-grasp.getrepositorystats` + +Parameters identify an owner and repository identifier. The initial result can +remain small: whether the repository is hosted, Git storage bytes, the latest +accepted state timestamp, and a coarse sync/health summary supported by +existing state. + +This method establishes: + +- strict management NIP-98 validation; +- private HTTP responses; +- repository scope parsing; +- recursive maintainer authorization; +- indistinguishable absent/unauthorized behavior; and +- a custom method and result schema discoverable by the UI. + +### Action: standard `changerelaydescription` + +Changing the relay description is a standard NIP-86 method, service-admin +only, non-destructive, and visibly testable through NIP-11 and the landing +page. It is the first setting backed by the override database. + +The action: + +1. authenticates and authorizes the service administrator; +2. rejects a change when a CLI, environment, NixOS-rendered, or `.env` value + owns the setting; +3. validates and transactionally stores the override otherwise; +4. updates the live NIP-11/landing-page view; +5. records a structured audit entry; and +6. returns the new value, source, revision, and application state. + +This method establishes standard NIP-86 interoperability, persistent mutation, +configuration precedence, audit behavior, optimistic concurrency, and a live +setting update without beginning with deletion, blacklist, or quota +enforcement. + +### Thin embedded UI + +The same milestone should include the smallest useful compiled application: + +- connect a Nostr signer; +- show the authenticated pubkey and discovered capabilities; +- query repository statistics for an owner and identifier; and +- show and, when authorized, edit the relay description. + +Completing the read, action, persistence, and bundled UI together proves the +whole delivery path before the API surface expands. + +## Evolution after the vertical slice + +### 1. Generalize configuration + +Move settings into the provenance-aware registry, expose read-only settings, +support validated overrides for eligible settings, and make restart state +visible. Keep bootstrap and secret settings operator-managed. + +### 2. Add operational reads and actions + +Expose service health, sync state, purgatory summaries, repository lifecycle +state, and safe retry/reconciliation actions. All lifecycle mutations must go +through the existing per-repository locking and deletion/recovery facades. + +### 3. Add roles and scoped analytics + +Introduce explicit observer and administrator assignments, self-usage views, +and consistent service/principal/repository authorization. Standard NIP-86 +roles should be implemented where their semantics fit; NIP-34 repository +maintainers remain a distinct repository-scoped authority. + +### 4. Add tiers, accounting, and quotas + +Persist tier definitions and assignments, build durable scoped usage +accounting, expose effective-limit explanations, and add reservation-based +enforcement at operation boundaries. API and UI views arrive before automatic +enforcement so operators can validate accounting. + +### 5. Graduate settings to live application + +Replace restart requirements one setting at a time with component-specific +reconfiguration mechanisms and tests proving that active and configured state +cannot diverge silently. + +## Security and correctness requirements + +- Invalid NIP-98 requests receive `401 Unauthorized`; valid but unauthorized + calls do not reveal private target existence. +- Management POST payload hashes are mandatory. +- Mutations reject authentication-event replay and are idempotent where the + operation permits it. +- The API never accepts the relaxed GRASP-08 Git credential profile. +- The API never returns secrets, raw private keys, or unrestricted process + environment data. +- All mutations are audited, including rejected attempts after authentication. +- High-cardinality private usage remains out of public Prometheus labels. +- Repository operations reuse current maintainer calculation and lifecycle + serialization. +- The UI never holds key material and is not trusted to enforce permissions. +- Database migration, configuration validation, and runtime application + failures are observable and fail without claiming a change succeeded. +- Tests wait on observable state with bounded deadlines; configuration tests do + not rely on fixed sleeps. + +## Deliberately deferred + +This vision does not initially promise: + +- every setting is API-writable; +- every setting applies live; +- server-initiated real-time management updates; +- a billing or payment system for tiers; +- quota enforcement before trustworthy accounting exists; +- replacement of Prometheus or normal application logs; or +- one control plane managing a fleet of ngit-grasp instances. + +Those can build on the same API, authorization, persistence, and capability +patterns once the single-instance path is proven. + +## Success criteria + +The direction is successful when: + +- generic NIP-86 clients can use the standard methods ngit-grasp advertises; +- maintainers can retrieve private repository information without gaining + service-wide visibility; +- declarative operators can see, but cannot accidentally overwrite, + operator-managed settings; +- hobbyists can persist eligible settings through the bundled interface; +- every response explains effective source and application state; +- every mutation has an authenticated audit record; +- the binary remains self-contained at runtime; and +- new reads, actions, settings, and quota dimensions can follow established + patterns instead of adding bespoke authentication and storage paths.