diff options
| author | bndw <ben@bdw.to> | 2026-02-13 17:35:32 -0800 |
|---|---|---|
| committer | bndw <ben@bdw.to> | 2026-02-13 17:35:32 -0800 |
| commit | 6c840f03524187d1f056fdaa70e5f1f9b24cf793 (patch) | |
| tree | 9b068d5125e79320321ac1a35df30f43482d4aba | |
| parent | 581ceecbf046f99b39885c74e2780a5320e5b15e (diff) | |
feat: add Protocol Buffer definitions and build tooling
| -rw-r--r-- | Makefile | 39 | ||||
| -rw-r--r-- | buf.gen.yaml | 15 | ||||
| -rw-r--r-- | buf.yaml | 9 | ||||
| -rw-r--r-- | proto/nostr/v1/nostr.proto | 183 |
4 files changed, 246 insertions, 0 deletions
diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d01b68b --- /dev/null +++ b/Makefile | |||
| @@ -0,0 +1,39 @@ | |||
| 1 | .PHONY: proto proto-lint proto-breaking test build clean | ||
| 2 | |||
| 3 | # Generate proto files | ||
| 4 | proto: | ||
| 5 | buf generate | ||
| 6 | |||
| 7 | # Lint proto files | ||
| 8 | proto-lint: | ||
| 9 | buf lint | ||
| 10 | |||
| 11 | # Check for breaking changes | ||
| 12 | proto-breaking: | ||
| 13 | buf breaking --against '.git#branch=main' | ||
| 14 | |||
| 15 | # Run tests | ||
| 16 | test: | ||
| 17 | go test ./... | ||
| 18 | |||
| 19 | # Build the relay | ||
| 20 | build: | ||
| 21 | go build -o bin/relay ./cmd/relay | ||
| 22 | |||
| 23 | # Clean generated files | ||
| 24 | clean: | ||
| 25 | rm -rf api/ | ||
| 26 | rm -f bin/relay | ||
| 27 | |||
| 28 | # Install buf (if not already installed) | ||
| 29 | install-buf: | ||
| 30 | @if ! command -v buf &> /dev/null; then \ | ||
| 31 | echo "Installing buf..."; \ | ||
| 32 | mkdir -p ~/.local/bin; \ | ||
| 33 | curl -sSL "https://github.com/bufbuild/buf/releases/latest/download/buf-$$(uname -s)-$$(uname -m)" -o ~/.local/bin/buf; \ | ||
| 34 | chmod +x ~/.local/bin/buf; \ | ||
| 35 | echo "buf installed to ~/.local/bin/buf"; \ | ||
| 36 | echo "Make sure ~/.local/bin is in your PATH"; \ | ||
| 37 | else \ | ||
| 38 | echo "buf is already installed"; \ | ||
| 39 | fi | ||
diff --git a/buf.gen.yaml b/buf.gen.yaml new file mode 100644 index 0000000..e6f485a --- /dev/null +++ b/buf.gen.yaml | |||
| @@ -0,0 +1,15 @@ | |||
| 1 | version: v2 | ||
| 2 | managed: | ||
| 3 | enabled: true | ||
| 4 | override: | ||
| 5 | - file_option: go_package_prefix | ||
| 6 | value: northwest.io/nostr-grpc/api | ||
| 7 | plugins: | ||
| 8 | - remote: buf.build/protocolbuffers/go | ||
| 9 | out: api | ||
| 10 | opt: | ||
| 11 | - paths=source_relative | ||
| 12 | - remote: buf.build/grpc/go | ||
| 13 | out: api | ||
| 14 | opt: | ||
| 15 | - paths=source_relative | ||
diff --git a/buf.yaml b/buf.yaml new file mode 100644 index 0000000..c7e30e3 --- /dev/null +++ b/buf.yaml | |||
| @@ -0,0 +1,9 @@ | |||
| 1 | version: v2 | ||
| 2 | modules: | ||
| 3 | - path: proto | ||
| 4 | lint: | ||
| 5 | use: | ||
| 6 | - STANDARD | ||
| 7 | breaking: | ||
| 8 | use: | ||
| 9 | - FILE | ||
diff --git a/proto/nostr/v1/nostr.proto b/proto/nostr/v1/nostr.proto new file mode 100644 index 0000000..7e8eacb --- /dev/null +++ b/proto/nostr/v1/nostr.proto | |||
| @@ -0,0 +1,183 @@ | |||
| 1 | syntax = "proto3"; | ||
| 2 | |||
| 3 | package nostr.v1; | ||
| 4 | |||
| 5 | option go_package = "northwest.io/nostr-grpc/api/nostr/v1;nostrv1"; | ||
| 6 | |||
| 7 | // Core Nostr event as defined in NIP-01 | ||
| 8 | message Event { | ||
| 9 | string id = 1; | ||
| 10 | string pubkey = 2; | ||
| 11 | int64 created_at = 3; | ||
| 12 | int32 kind = 4; | ||
| 13 | repeated Tag tags = 5; | ||
| 14 | string content = 6; | ||
| 15 | string sig = 7; | ||
| 16 | |||
| 17 | // Optional: only populated if client requests verification | ||
| 18 | // Contains exact canonical JSON bytes that were signed | ||
| 19 | optional bytes canonical_json = 8; | ||
| 20 | } | ||
| 21 | |||
| 22 | // Tag is an array of strings (e.g., ["e", "event_id", "relay_url"]) | ||
| 23 | message Tag { | ||
| 24 | repeated string values = 1; | ||
| 25 | } | ||
| 26 | |||
| 27 | // Nostr filter for querying events (REQ) | ||
| 28 | message Filter { | ||
| 29 | repeated string ids = 1; | ||
| 30 | repeated string authors = 2; // pubkeys | ||
| 31 | repeated int32 kinds = 3; | ||
| 32 | repeated string e_tags = 4; // #e tag values | ||
| 33 | repeated string p_tags = 5; // #p tag values | ||
| 34 | optional int64 since = 6; | ||
| 35 | optional int64 until = 7; | ||
| 36 | optional int32 limit = 8; | ||
| 37 | |||
| 38 | // Extension: support for arbitrary tag filters | ||
| 39 | map<string, TagFilter> tag_filters = 9; | ||
| 40 | } | ||
| 41 | |||
| 42 | // Filter for arbitrary tags | ||
| 43 | message TagFilter { | ||
| 44 | repeated string values = 1; | ||
| 45 | } | ||
| 46 | |||
| 47 | // Request to publish a single event | ||
| 48 | message PublishEventRequest { | ||
| 49 | Event event = 1; | ||
| 50 | } | ||
| 51 | |||
| 52 | // Response after publishing an event | ||
| 53 | message PublishEventResponse { | ||
| 54 | bool accepted = 1; | ||
| 55 | string message = 2; // Error message or "duplicate" or "success" | ||
| 56 | |||
| 57 | // Always include canonical JSON so client can verify | ||
| 58 | // what the relay stored | ||
| 59 | bytes canonical_json = 3; | ||
| 60 | } | ||
| 61 | |||
| 62 | // Request to subscribe to events matching filters | ||
| 63 | message SubscribeRequest { | ||
| 64 | repeated Filter filters = 1; | ||
| 65 | |||
| 66 | // If true, include canonical_json in streamed Event messages | ||
| 67 | // Allows client-side signature verification | ||
| 68 | // Default: false (most clients trust the relay) | ||
| 69 | bool include_canonical_json = 2; | ||
| 70 | |||
| 71 | // Optional client-provided subscription ID for tracking | ||
| 72 | string subscription_id = 3; | ||
| 73 | } | ||
| 74 | |||
| 75 | // Request to unsubscribe from an active subscription | ||
| 76 | message UnsubscribeRequest { | ||
| 77 | string subscription_id = 1; | ||
| 78 | } | ||
| 79 | |||
| 80 | // Batch publish request | ||
| 81 | message PublishBatchRequest { | ||
| 82 | repeated Event events = 1; | ||
| 83 | } | ||
| 84 | |||
| 85 | // Batch publish response | ||
| 86 | message PublishBatchResponse { | ||
| 87 | repeated PublishEventResponse results = 1; | ||
| 88 | } | ||
| 89 | |||
| 90 | // Paginated query request | ||
| 91 | message QueryRequest { | ||
| 92 | repeated Filter filters = 1; | ||
| 93 | bool include_canonical_json = 2; | ||
| 94 | |||
| 95 | // Pagination | ||
| 96 | string cursor = 3; // Opaque cursor from previous response | ||
| 97 | int32 page_size = 4; // Default: 100 | ||
| 98 | } | ||
| 99 | |||
| 100 | // Paginated query response | ||
| 101 | message QueryResponse { | ||
| 102 | repeated Event events = 1; | ||
| 103 | string next_cursor = 2; // Empty if no more results | ||
| 104 | int32 total_count = 3; // Optional: total matching events | ||
| 105 | } | ||
| 106 | |||
| 107 | // Event count request (NIP-45) | ||
| 108 | message CountRequest { | ||
| 109 | repeated Filter filters = 1; | ||
| 110 | } | ||
| 111 | |||
| 112 | // Event count response | ||
| 113 | message CountResponse { | ||
| 114 | int64 count = 1; | ||
| 115 | } | ||
| 116 | |||
| 117 | // Empty message | ||
| 118 | message Empty {} | ||
| 119 | |||
| 120 | // Main relay service | ||
| 121 | service NostrRelay { | ||
| 122 | // Publish a single event | ||
| 123 | rpc PublishEvent(PublishEventRequest) returns (PublishEventResponse); | ||
| 124 | |||
| 125 | // Subscribe to events matching filters (streaming) | ||
| 126 | rpc Subscribe(SubscribeRequest) returns (stream Event); | ||
| 127 | |||
| 128 | // Unsubscribe from an active subscription | ||
| 129 | rpc Unsubscribe(UnsubscribeRequest) returns (Empty); | ||
| 130 | |||
| 131 | // gRPC-specific: batch publish | ||
| 132 | rpc PublishBatch(PublishBatchRequest) returns (PublishBatchResponse); | ||
| 133 | |||
| 134 | // gRPC-specific: paginated query (non-streaming) | ||
| 135 | rpc QueryEvents(QueryRequest) returns (QueryResponse); | ||
| 136 | |||
| 137 | // Event counts (NIP-45) | ||
| 138 | rpc CountEvents(CountRequest) returns (CountResponse); | ||
| 139 | } | ||
| 140 | |||
| 141 | // Admin service (optional, secured separately) | ||
| 142 | service RelayAdmin { | ||
| 143 | rpc GetStats(Empty) returns (RelayStats); | ||
| 144 | rpc GetConnections(Empty) returns (ConnectionList); | ||
| 145 | rpc BanPublicKey(BanRequest) returns (Empty); | ||
| 146 | rpc GetStorageInfo(Empty) returns (StorageStats); | ||
| 147 | } | ||
| 148 | |||
| 149 | // Relay statistics | ||
| 150 | message RelayStats { | ||
| 151 | int64 total_events = 1; | ||
| 152 | int64 total_subscriptions = 2; | ||
| 153 | int64 connected_clients = 3; | ||
| 154 | int64 events_per_second = 4; | ||
| 155 | int64 uptime_seconds = 5; | ||
| 156 | } | ||
| 157 | |||
| 158 | // List of active connections | ||
| 159 | message ConnectionList { | ||
| 160 | repeated Connection connections = 1; | ||
| 161 | } | ||
| 162 | |||
| 163 | // Single connection info | ||
| 164 | message Connection { | ||
| 165 | string client_id = 1; | ||
| 166 | string protocol = 2; // "websocket" or "grpc" | ||
| 167 | int64 connected_at = 3; | ||
| 168 | int32 active_subscriptions = 4; | ||
| 169 | } | ||
| 170 | |||
| 171 | // Request to ban a public key | ||
| 172 | message BanRequest { | ||
| 173 | string pubkey = 1; | ||
| 174 | int64 until = 2; // Unix timestamp, 0 for permanent | ||
| 175 | string reason = 3; | ||
| 176 | } | ||
| 177 | |||
| 178 | // Storage statistics | ||
| 179 | message StorageStats { | ||
| 180 | int64 total_bytes = 1; | ||
| 181 | int64 total_events = 2; | ||
| 182 | int64 db_size_bytes = 3; | ||
| 183 | } | ||
