Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
feae79af1a
|
|||
|
ebef8605eb
|
|||
|
c5db0abf73
|
96
CLAUDE.md
96
CLAUDE.md
@@ -8,11 +8,12 @@ ORLY is a high-performance Nostr relay written in Go, designed for personal rela
|
||||
|
||||
**Key Technologies:**
|
||||
- **Language**: Go 1.25.3+
|
||||
- **Database**: Badger v4 (embedded key-value store) or DGraph (distributed graph database)
|
||||
- **Database**: Badger v4 (embedded), DGraph (distributed graph), or Neo4j (social graph)
|
||||
- **Cryptography**: Custom p8k library using purego for secp256k1 operations (no CGO)
|
||||
- **Web UI**: Svelte frontend embedded in the binary
|
||||
- **WebSocket**: gorilla/websocket for Nostr protocol
|
||||
- **Performance**: SIMD-accelerated SHA256 and hex encoding, query result caching with zstd compression
|
||||
- **Social Graph**: Neo4j backend with Web of Trust (WoT) extensions for trust metrics
|
||||
|
||||
## Build Commands
|
||||
|
||||
@@ -158,6 +159,19 @@ export ORLY_QUERY_CACHE_MAX_AGE=5m # Cache expiry time
|
||||
export ORLY_DB_BLOCK_CACHE_MB=512 # Block cache size
|
||||
export ORLY_DB_INDEX_CACHE_MB=256 # Index cache size
|
||||
export ORLY_INLINE_EVENT_THRESHOLD=1024 # Inline storage threshold (bytes)
|
||||
|
||||
# Directory Spider (metadata sync from other relays)
|
||||
export ORLY_DIRECTORY_SPIDER=true # Enable directory spider
|
||||
export ORLY_DIRECTORY_SPIDER_INTERVAL=24h # How often to run
|
||||
export ORLY_DIRECTORY_SPIDER_HOPS=3 # Max hops for relay discovery
|
||||
|
||||
# NIP-43 Relay Access Metadata
|
||||
export ORLY_NIP43_ENABLED=true # Enable invite system
|
||||
export ORLY_NIP43_INVITE_EXPIRY=24h # Invite code validity
|
||||
|
||||
# Authentication modes
|
||||
export ORLY_AUTH_REQUIRED=false # Require auth for all requests
|
||||
export ORLY_AUTH_TO_WRITE=false # Require auth only for writes
|
||||
```
|
||||
|
||||
## Code Architecture
|
||||
@@ -185,7 +199,7 @@ export ORLY_INLINE_EVENT_THRESHOLD=1024 # Inline storage threshold (bytes)
|
||||
|
||||
**`pkg/database/`** - Database abstraction layer with multiple backend support
|
||||
- `interface.go` - Database interface definition for pluggable backends
|
||||
- `factory.go` - Database backend selection (Badger or DGraph)
|
||||
- `factory.go` - Database backend selection (Badger, DGraph, or Neo4j)
|
||||
- `database.go` - Badger implementation with cache tuning and query cache
|
||||
- `save-event.go` - Event storage with index updates
|
||||
- `query-events.go` - Main query execution engine with filter normalization
|
||||
@@ -196,6 +210,15 @@ export ORLY_INLINE_EVENT_THRESHOLD=1024 # Inline storage threshold (bytes)
|
||||
- `identity.go` - Relay identity key management
|
||||
- `migrations.go` - Database schema migration runner
|
||||
|
||||
**`pkg/neo4j/`** - Neo4j graph database backend with social graph support
|
||||
- `neo4j.go` - Main database implementation
|
||||
- `schema.go` - Graph schema and index definitions (includes WoT extensions)
|
||||
- `query-events.go` - REQ filter to Cypher translation
|
||||
- `save-event.go` - Event storage with relationship creation
|
||||
- `social-event-processor.go` - Processes kinds 0, 3, 1984, 10000 for social graph
|
||||
- `WOT_SPEC.md` - Web of Trust data model specification (NostrUser nodes, trust metrics)
|
||||
- `MODIFYING_SCHEMA.md` - Guide for schema modifications
|
||||
|
||||
**`pkg/protocol/`** - Nostr protocol implementation
|
||||
- `ws/` - WebSocket message framing and parsing
|
||||
- `auth/` - NIP-42 authentication challenge/response
|
||||
@@ -231,6 +254,9 @@ export ORLY_INLINE_EVENT_THRESHOLD=1024 # Inline storage threshold (bytes)
|
||||
**`pkg/policy/`** - Event filtering and validation policies
|
||||
- Policy configuration loaded from `~/.config/ORLY/policy.json`
|
||||
- Per-kind size limits, age restrictions, custom scripts
|
||||
- **Write-Only Validation**: Size, age, tag, and expiry validations apply ONLY to write operations
|
||||
- **Read-Only Filtering**: `read_allow`, `read_deny`, `privileged` apply ONLY to read operations
|
||||
- See `docs/POLICY_CONFIGURATION_REFERENCE.md` for authoritative read vs write applicability
|
||||
- **Dynamic Policy Hot Reload via Kind 12345 Events:**
|
||||
- Policy admins can update policy configuration without relay restart
|
||||
- Kind 12345 events contain JSON policy in content field
|
||||
@@ -239,12 +265,16 @@ export ORLY_INLINE_EVENT_THRESHOLD=1024 # Inline storage threshold (bytes)
|
||||
- Policy admin follow lists (kind 3) trigger immediate cache refresh
|
||||
- `WriteAllowFollows` rule grants both read+write access to admin follows
|
||||
- Tag validation supports regex patterns per tag type
|
||||
- **New Policy Rule Fields:**
|
||||
- **Policy Rule Fields:**
|
||||
- `max_expiry_duration`: ISO-8601 duration format (e.g., "P7D", "PT1H30M") for event expiry limits
|
||||
- `protected_required`: Requires NIP-70 protected events (must have "-" tag)
|
||||
- `identifier_regex`: Regex pattern for validating "d" tag identifiers
|
||||
- `follows_whitelist_admins`: Per-rule admin pubkeys whose follows are whitelisted
|
||||
- `write_allow` / `write_deny`: Pubkey whitelist/blacklist for writing (write-only)
|
||||
- `read_allow` / `read_deny`: Pubkey whitelist/blacklist for reading (read-only)
|
||||
- `privileged`: Party-involved access control (read-only)
|
||||
- See `docs/POLICY_USAGE_GUIDE.md` for configuration examples
|
||||
- See `pkg/policy/README.md` for quick reference
|
||||
|
||||
**`pkg/sync/`** - Distributed synchronization
|
||||
- `cluster_manager.go` - Active replication between relay peers
|
||||
@@ -254,6 +284,12 @@ export ORLY_INLINE_EVENT_THRESHOLD=1024 # Inline storage threshold (bytes)
|
||||
**`pkg/spider/`** - Event syncing from other relays
|
||||
- `spider.go` - Spider manager for "follows" mode
|
||||
- Fetches events from admin relays for followed pubkeys
|
||||
- **Directory Spider** (`directory.go`):
|
||||
- Discovers relays by crawling kind 10002 (relay list) events
|
||||
- Expands outward from seed pubkeys (whitelisted users) via hop distance
|
||||
- Fetches metadata events (kinds 0, 3, 10000, 10002) from discovered relays
|
||||
- Self-detection prevents querying own relay
|
||||
- Configurable interval and max hops via `ORLY_DIRECTORY_SPIDER_*` env vars
|
||||
|
||||
**`pkg/utils/`** - Shared utilities
|
||||
- `atomic/` - Extended atomic operations
|
||||
@@ -287,6 +323,11 @@ export ORLY_INLINE_EVENT_THRESHOLD=1024 # Inline storage threshold (bytes)
|
||||
- Supports multiple backends via `ORLY_DB_TYPE` environment variable
|
||||
- **Badger** (default): Embedded key-value store with custom indexing, ideal for single-instance deployments
|
||||
- **DGraph**: Distributed graph database for larger, multi-node deployments
|
||||
- **Neo4j**: Graph database with social graph and Web of Trust (WoT) extensions
|
||||
- Processes kinds 0 (profile), 3 (contacts), 1984 (reports), 10000 (mute list) for social graph
|
||||
- NostrUser nodes with trust metrics (influence, PageRank)
|
||||
- FOLLOWS, MUTES, REPORTS relationships for WoT analysis
|
||||
- See `pkg/neo4j/WOT_SPEC.md` for full schema specification
|
||||
- Backend selected via factory pattern in `pkg/database/factory.go`
|
||||
- All backends implement the same `Database` interface defined in `pkg/database/interface.go`
|
||||
|
||||
@@ -538,3 +579,52 @@ Files modified:
|
||||
```
|
||||
3. GitHub Actions workflow builds binaries for multiple platforms
|
||||
4. Release created automatically with binaries and checksums
|
||||
|
||||
## Recent Features (v0.31.x)
|
||||
|
||||
### Directory Spider
|
||||
The directory spider (`pkg/spider/directory.go`) automatically discovers and syncs metadata from other relays:
|
||||
- Crawls kind 10002 (relay list) events to discover relays
|
||||
- Expands outward from seed pubkeys (whitelisted users) via configurable hop distance
|
||||
- Fetches essential metadata events (kinds 0, 3, 10000, 10002)
|
||||
- Self-detection prevents querying own relay
|
||||
- Enable with `ORLY_DIRECTORY_SPIDER=true`
|
||||
|
||||
### Neo4j Social Graph Backend
|
||||
The Neo4j backend (`pkg/neo4j/`) includes Web of Trust (WoT) extensions:
|
||||
- **Social Event Processor**: Handles kinds 0, 3, 1984, 10000 for social graph management
|
||||
- **NostrUser nodes**: Store profile data and trust metrics (influence, PageRank)
|
||||
- **Relationships**: FOLLOWS, MUTES, REPORTS for social graph analysis
|
||||
- **WoT Schema**: See `pkg/neo4j/WOT_SPEC.md` for full specification
|
||||
- **Schema Modifications**: See `pkg/neo4j/MODIFYING_SCHEMA.md` for how to update
|
||||
|
||||
### Policy System Enhancements
|
||||
- **Write-Only Validation**: Size, age, tag validations apply ONLY to writes
|
||||
- **Read-Only Filtering**: `read_allow`, `read_deny`, `privileged` apply ONLY to reads
|
||||
- **Scripts**: Policy scripts execute ONLY for write operations
|
||||
- **Reference Documentation**: `docs/POLICY_CONFIGURATION_REFERENCE.md` provides authoritative read vs write applicability
|
||||
- See also: `pkg/policy/README.md` for quick reference
|
||||
|
||||
### Authentication Modes
|
||||
- `ORLY_AUTH_REQUIRED=true`: Require authentication for ALL requests
|
||||
- `ORLY_AUTH_TO_WRITE=true`: Require authentication only for writes (allow anonymous reads)
|
||||
|
||||
### NIP-43 Relay Access Metadata
|
||||
Invite-based access control system:
|
||||
- `ORLY_NIP43_ENABLED=true`: Enable invite system
|
||||
- Publishes kind 8000/8001 events for member changes
|
||||
- Publishes kind 13534 membership list events
|
||||
- Configurable invite expiry via `ORLY_NIP43_INVITE_EXPIRY`
|
||||
|
||||
## Documentation Index
|
||||
|
||||
| Document | Purpose |
|
||||
|----------|---------|
|
||||
| `docs/POLICY_CONFIGURATION_REFERENCE.md` | Authoritative policy config reference with read/write applicability |
|
||||
| `docs/POLICY_USAGE_GUIDE.md` | Comprehensive policy system user guide |
|
||||
| `pkg/policy/README.md` | Policy system quick reference |
|
||||
| `pkg/neo4j/README.md` | Neo4j backend overview |
|
||||
| `pkg/neo4j/WOT_SPEC.md` | Web of Trust schema specification |
|
||||
| `pkg/neo4j/MODIFYING_SCHEMA.md` | How to modify Neo4j schema |
|
||||
| `pkg/neo4j/TESTING.md` | Neo4j testing guide |
|
||||
| `readme.adoc` | Project README with feature overview |
|
||||
|
||||
615
docs/POLICY_CONFIGURATION_REFERENCE.md
Normal file
615
docs/POLICY_CONFIGURATION_REFERENCE.md
Normal file
@@ -0,0 +1,615 @@
|
||||
# ORLY Policy Configuration Reference
|
||||
|
||||
This document provides a definitive reference for all policy configuration options and when each rule applies. Use this as the authoritative source for understanding policy behavior.
|
||||
|
||||
## Quick Reference: Read vs Write Applicability
|
||||
|
||||
| Rule Field | Write (EVENT) | Read (REQ) | Notes |
|
||||
|------------|:-------------:|:----------:|-------|
|
||||
| `size_limit` | ✅ | ❌ | Validates incoming events only |
|
||||
| `content_limit` | ✅ | ❌ | Validates incoming events only |
|
||||
| `max_age_of_event` | ✅ | ❌ | Prevents replay attacks |
|
||||
| `max_age_event_in_future` | ✅ | ❌ | Prevents future-dated events |
|
||||
| `max_expiry_duration` | ✅ | ❌ | Requires expiration tag |
|
||||
| `must_have_tags` | ✅ | ❌ | Validates required tags |
|
||||
| `protected_required` | ✅ | ❌ | Requires NIP-70 "-" tag |
|
||||
| `identifier_regex` | ✅ | ❌ | Validates "d" tag format |
|
||||
| `tag_validation` | ✅ | ❌ | Validates tag values with regex |
|
||||
| `write_allow` | ✅ | ❌ | Pubkey whitelist for writing |
|
||||
| `write_deny` | ✅ | ❌ | Pubkey blacklist for writing |
|
||||
| `read_allow` | ❌ | ✅ | Pubkey whitelist for reading |
|
||||
| `read_deny` | ❌ | ✅ | Pubkey blacklist for reading |
|
||||
| `privileged` | ❌ | ✅ | Party-involved access control |
|
||||
| `write_allow_follows` | ✅ | ✅ | Grants **both** read AND write |
|
||||
| `follows_whitelist_admins` | ✅ | ✅ | Grants **both** read AND write |
|
||||
| `script` | ✅ | ❌ | Scripts only run for writes |
|
||||
|
||||
---
|
||||
|
||||
## Core Principle: Validation vs Filtering
|
||||
|
||||
The policy system has two distinct modes of operation:
|
||||
|
||||
### Write Operations (EVENT messages)
|
||||
- **Purpose**: Validate and accept/reject incoming events
|
||||
- **All rules apply** except `read_allow`, `read_deny`, and `privileged`
|
||||
- Events are checked **before storage**
|
||||
- Rejected events are never stored
|
||||
|
||||
### Read Operations (REQ messages)
|
||||
- **Purpose**: Filter which stored events a user can retrieve
|
||||
- **Only access control rules apply**: `read_allow`, `read_deny`, `privileged`, `write_allow_follows`, `follows_whitelist_admins`
|
||||
- Validation rules (size, age, tags) do NOT apply
|
||||
- Scripts are NOT executed for reads
|
||||
- Filtering happens **after database query**
|
||||
|
||||
---
|
||||
|
||||
## Configuration Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"default_policy": "allow|deny",
|
||||
"kind": {
|
||||
"whitelist": [1, 3, 7],
|
||||
"blacklist": [4, 42]
|
||||
},
|
||||
"owners": ["hex_pubkey_64_chars"],
|
||||
"policy_admins": ["hex_pubkey_64_chars"],
|
||||
"policy_follow_whitelist_enabled": true,
|
||||
"global": { /* Rule object */ },
|
||||
"rules": {
|
||||
"1": { /* Rule object for kind 1 */ },
|
||||
"30023": { /* Rule object for kind 30023 */ }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Top-Level Configuration Fields
|
||||
|
||||
### `default_policy`
|
||||
**Type**: `string`
|
||||
**Values**: `"allow"` (default) or `"deny"`
|
||||
**Applies to**: Both read and write
|
||||
|
||||
The fallback behavior when no specific rule makes a decision.
|
||||
|
||||
```json
|
||||
{
|
||||
"default_policy": "deny"
|
||||
}
|
||||
```
|
||||
|
||||
### `kind.whitelist` and `kind.blacklist`
|
||||
**Type**: `[]int`
|
||||
**Applies to**: Both read and write
|
||||
|
||||
Controls which event kinds are processed at all.
|
||||
|
||||
- **Whitelist** takes precedence: If present, ONLY whitelisted kinds are allowed
|
||||
- **Blacklist**: If no whitelist, these kinds are denied
|
||||
- **Neither**: Behavior depends on `default_policy` and whether rules exist
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": {
|
||||
"whitelist": [0, 1, 3, 7, 30023]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `owners`
|
||||
**Type**: `[]string` (64-character hex pubkeys)
|
||||
**Applies to**: Policy administration
|
||||
|
||||
Relay owners with full control. Merged with `ORLY_OWNERS` environment variable.
|
||||
|
||||
```json
|
||||
{
|
||||
"owners": ["4a93c5ac0c6f49d2c7e7a5b8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8"]
|
||||
}
|
||||
```
|
||||
|
||||
### `policy_admins`
|
||||
**Type**: `[]string` (64-character hex pubkeys)
|
||||
**Applies to**: Policy administration
|
||||
|
||||
Pubkeys that can update policy via kind 12345 events (with restrictions).
|
||||
|
||||
### `policy_follow_whitelist_enabled`
|
||||
**Type**: `boolean`
|
||||
**Default**: `false`
|
||||
**Applies to**: Both read and write (when `write_allow_follows` is true)
|
||||
|
||||
When enabled, allows `write_allow_follows` rules to grant access to policy admin follows.
|
||||
|
||||
---
|
||||
|
||||
## Rule Object Fields
|
||||
|
||||
Rules can be defined in `global` (applies to all events) or `rules[kind]` (applies to specific kind).
|
||||
|
||||
### Access Control Fields
|
||||
|
||||
#### `write_allow`
|
||||
**Type**: `[]string` (hex pubkeys)
|
||||
**Applies to**: Write only
|
||||
**Behavior**: Exclusive whitelist
|
||||
|
||||
When present with entries, ONLY these pubkeys can write events of this kind. All others are denied.
|
||||
|
||||
```json
|
||||
{
|
||||
"rules": {
|
||||
"1": {
|
||||
"write_allow": ["pubkey1_hex", "pubkey2_hex"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Special case**: Empty array `[]` explicitly allows all writers.
|
||||
|
||||
#### `write_deny`
|
||||
**Type**: `[]string` (hex pubkeys)
|
||||
**Applies to**: Write only
|
||||
**Behavior**: Blacklist (highest priority)
|
||||
|
||||
These pubkeys cannot write events of this kind. **Checked before allow lists.**
|
||||
|
||||
```json
|
||||
{
|
||||
"rules": {
|
||||
"1": {
|
||||
"write_deny": ["banned_pubkey_hex"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `read_allow`
|
||||
**Type**: `[]string` (hex pubkeys)
|
||||
**Applies to**: Read only
|
||||
**Behavior**: Exclusive whitelist (with OR logic for privileged)
|
||||
|
||||
When present with entries:
|
||||
- If `privileged: false`: ONLY these pubkeys can read
|
||||
- If `privileged: true`: These pubkeys OR parties involved can read
|
||||
|
||||
```json
|
||||
{
|
||||
"rules": {
|
||||
"4": {
|
||||
"read_allow": ["trusted_pubkey_hex"],
|
||||
"privileged": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `read_deny`
|
||||
**Type**: `[]string` (hex pubkeys)
|
||||
**Applies to**: Read only
|
||||
**Behavior**: Blacklist (highest priority)
|
||||
|
||||
These pubkeys cannot read events of this kind. **Checked before allow lists.**
|
||||
|
||||
#### `privileged`
|
||||
**Type**: `boolean`
|
||||
**Default**: `false`
|
||||
**Applies to**: Read only
|
||||
|
||||
When `true`, events are only readable by "parties involved":
|
||||
- The event author (`event.pubkey`)
|
||||
- Users mentioned in `p` tags
|
||||
|
||||
**Interaction with `read_allow`**:
|
||||
- `read_allow` present + `privileged: true` = OR logic (in list OR party involved)
|
||||
- `read_allow` empty + `privileged: true` = Only parties involved
|
||||
- `privileged: true` alone = Only parties involved
|
||||
|
||||
```json
|
||||
{
|
||||
"rules": {
|
||||
"4": {
|
||||
"description": "DMs - only sender and recipient can read",
|
||||
"privileged": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `write_allow_follows`
|
||||
**Type**: `boolean`
|
||||
**Default**: `false`
|
||||
**Applies to**: Both read AND write
|
||||
**Requires**: `policy_follow_whitelist_enabled: true` at top level
|
||||
|
||||
Grants **both read and write access** to pubkeys followed by policy admins.
|
||||
|
||||
> **Important**: Despite the name, this grants BOTH read and write access.
|
||||
|
||||
```json
|
||||
{
|
||||
"policy_follow_whitelist_enabled": true,
|
||||
"rules": {
|
||||
"1": {
|
||||
"write_allow_follows": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `follows_whitelist_admins`
|
||||
**Type**: `[]string` (hex pubkeys)
|
||||
**Applies to**: Both read AND write
|
||||
|
||||
Alternative to `write_allow_follows` that specifies which admin pubkeys' follows are whitelisted for this specific rule.
|
||||
|
||||
```json
|
||||
{
|
||||
"rules": {
|
||||
"30023": {
|
||||
"follows_whitelist_admins": ["curator_pubkey_hex"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Validation Fields (Write-Only)
|
||||
|
||||
These fields validate incoming events and are **completely ignored for read operations**.
|
||||
|
||||
#### `size_limit`
|
||||
**Type**: `int64` (bytes)
|
||||
**Applies to**: Write only
|
||||
|
||||
Maximum total serialized event size.
|
||||
|
||||
```json
|
||||
{
|
||||
"global": {
|
||||
"size_limit": 100000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `content_limit`
|
||||
**Type**: `int64` (bytes)
|
||||
**Applies to**: Write only
|
||||
|
||||
Maximum size of the `content` field.
|
||||
|
||||
```json
|
||||
{
|
||||
"rules": {
|
||||
"1": {
|
||||
"content_limit": 10000
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `max_age_of_event`
|
||||
**Type**: `int64` (seconds)
|
||||
**Applies to**: Write only
|
||||
|
||||
Maximum age of events. Events with `created_at` older than `now - max_age_of_event` are rejected.
|
||||
|
||||
```json
|
||||
{
|
||||
"global": {
|
||||
"max_age_of_event": 86400
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `max_age_event_in_future`
|
||||
**Type**: `int64` (seconds)
|
||||
**Applies to**: Write only
|
||||
|
||||
Maximum time events can be dated in the future. Events with `created_at` later than `now + max_age_event_in_future` are rejected.
|
||||
|
||||
```json
|
||||
{
|
||||
"global": {
|
||||
"max_age_event_in_future": 300
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `max_expiry_duration`
|
||||
**Type**: `string` (ISO-8601 duration)
|
||||
**Applies to**: Write only
|
||||
|
||||
Maximum allowed expiry time from event creation. Events **must** have an `expiration` tag when this is set.
|
||||
|
||||
**Format**: `P[n]Y[n]M[n]W[n]DT[n]H[n]M[n]S`
|
||||
|
||||
**Examples**:
|
||||
- `P7D` = 7 days
|
||||
- `PT1H` = 1 hour
|
||||
- `P1DT12H` = 1 day 12 hours
|
||||
- `PT30M` = 30 minutes
|
||||
|
||||
```json
|
||||
{
|
||||
"rules": {
|
||||
"20": {
|
||||
"description": "Ephemeral events must expire within 24 hours",
|
||||
"max_expiry_duration": "P1D"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `must_have_tags`
|
||||
**Type**: `[]string` (tag names)
|
||||
**Applies to**: Write only
|
||||
|
||||
Required tags that must be present on the event.
|
||||
|
||||
```json
|
||||
{
|
||||
"rules": {
|
||||
"1": {
|
||||
"must_have_tags": ["p", "e"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `protected_required`
|
||||
**Type**: `boolean`
|
||||
**Default**: `false`
|
||||
**Applies to**: Write only
|
||||
|
||||
Requires events to have a `-` tag (NIP-70 protected events).
|
||||
|
||||
```json
|
||||
{
|
||||
"rules": {
|
||||
"4": {
|
||||
"protected_required": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `identifier_regex`
|
||||
**Type**: `string` (regex pattern)
|
||||
**Applies to**: Write only
|
||||
|
||||
Regex pattern that `d` tag values must match. Events **must** have a `d` tag when this is set.
|
||||
|
||||
```json
|
||||
{
|
||||
"rules": {
|
||||
"30023": {
|
||||
"identifier_regex": "^[a-z0-9-]{1,64}$"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `tag_validation`
|
||||
**Type**: `map[string]string` (tag name → regex pattern)
|
||||
**Applies to**: Write only
|
||||
|
||||
Regex patterns for validating specific tag values. Only validates tags that are **present** on the event.
|
||||
|
||||
> **Note**: To require a tag to exist, use `must_have_tags`. `tag_validation` only validates format.
|
||||
|
||||
```json
|
||||
{
|
||||
"rules": {
|
||||
"30023": {
|
||||
"tag_validation": {
|
||||
"t": "^[a-z0-9-]{1,32}$",
|
||||
"d": "^[a-z0-9-]+$"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Script Configuration
|
||||
|
||||
#### `script`
|
||||
**Type**: `string` (file path)
|
||||
**Applies to**: Write only
|
||||
|
||||
Path to a custom validation script. **Scripts are NOT executed for read operations.**
|
||||
|
||||
```json
|
||||
{
|
||||
"rules": {
|
||||
"1": {
|
||||
"script": "/etc/orly/scripts/spam-filter.py"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Policy Evaluation Order
|
||||
|
||||
### For Write Operations
|
||||
|
||||
```
|
||||
1. Global Rule Check (all fields apply)
|
||||
├─ Universal constraints (size, tags, age, etc.)
|
||||
├─ write_deny check
|
||||
├─ write_allow_follows / follows_whitelist_admins check
|
||||
└─ write_allow check
|
||||
|
||||
2. Kind Filtering (whitelist/blacklist)
|
||||
|
||||
3. Kind-Specific Rule Check (same as global)
|
||||
├─ Universal constraints
|
||||
├─ write_deny check
|
||||
├─ write_allow_follows / follows_whitelist_admins check
|
||||
├─ write_allow check
|
||||
└─ Script execution (if configured)
|
||||
|
||||
4. Default Policy (if no rules matched)
|
||||
```
|
||||
|
||||
### For Read Operations
|
||||
|
||||
```
|
||||
1. Global Rule Check (access control only)
|
||||
├─ read_deny check
|
||||
├─ write_allow_follows / follows_whitelist_admins check
|
||||
├─ read_allow check
|
||||
└─ privileged check (party involved)
|
||||
|
||||
2. Kind Filtering (whitelist/blacklist)
|
||||
|
||||
3. Kind-Specific Rule Check (access control only)
|
||||
├─ read_deny check
|
||||
├─ write_allow_follows / follows_whitelist_admins check
|
||||
├─ read_allow + privileged (OR logic)
|
||||
└─ privileged-only check
|
||||
|
||||
4. Default Policy (if no rules matched)
|
||||
|
||||
NOTE: Scripts are NOT executed for read operations
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Configuration Patterns
|
||||
|
||||
### Private Relay (Whitelist Only)
|
||||
|
||||
```json
|
||||
{
|
||||
"default_policy": "deny",
|
||||
"global": {
|
||||
"write_allow": ["trusted_pubkey_1", "trusted_pubkey_2"],
|
||||
"read_allow": ["trusted_pubkey_1", "trusted_pubkey_2"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Open Relay with Spam Protection
|
||||
|
||||
```json
|
||||
{
|
||||
"default_policy": "allow",
|
||||
"global": {
|
||||
"size_limit": 100000,
|
||||
"max_age_of_event": 86400,
|
||||
"max_age_event_in_future": 300
|
||||
},
|
||||
"rules": {
|
||||
"1": {
|
||||
"script": "/etc/orly/scripts/spam-filter.sh"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Community Relay (Follows-Based)
|
||||
|
||||
```json
|
||||
{
|
||||
"default_policy": "deny",
|
||||
"policy_admins": ["community_admin_pubkey"],
|
||||
"policy_follow_whitelist_enabled": true,
|
||||
"global": {
|
||||
"write_allow_follows": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Encrypted DMs (Privileged Access)
|
||||
|
||||
```json
|
||||
{
|
||||
"rules": {
|
||||
"4": {
|
||||
"description": "Encrypted DMs - only sender/recipient",
|
||||
"privileged": true,
|
||||
"protected_required": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Long-Form Content with Validation
|
||||
|
||||
```json
|
||||
{
|
||||
"rules": {
|
||||
"30023": {
|
||||
"description": "Long-form articles",
|
||||
"size_limit": 100000,
|
||||
"content_limit": 50000,
|
||||
"max_expiry_duration": "P30D",
|
||||
"identifier_regex": "^[a-z0-9-]{1,64}$",
|
||||
"tag_validation": {
|
||||
"t": "^[a-z0-9-]{1,32}$"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Important Behaviors
|
||||
|
||||
### Whitelist vs Blacklist Precedence
|
||||
|
||||
1. **Deny lists** (`write_deny`, `read_deny`) are checked **first** and have highest priority
|
||||
2. **Allow lists** are exclusive when populated - ONLY listed pubkeys are allowed
|
||||
3. **Deny-only configuration**: If only deny list exists (no allow list), all non-denied pubkeys are allowed
|
||||
|
||||
### Empty Arrays vs Null
|
||||
|
||||
- `[]` (empty array explicitly set) = Allow all
|
||||
- `null` or field omitted = No list configured, use other rules
|
||||
|
||||
### Global Rules Are Additive
|
||||
|
||||
Global rules are always evaluated **in addition to** kind-specific rules. They cannot be overridden at the kind level.
|
||||
|
||||
### Implicit Kind Whitelist
|
||||
|
||||
When rules are defined but no explicit `kind.whitelist`:
|
||||
- If `default_policy: "allow"`: All kinds allowed
|
||||
- If `default_policy: "deny"` or unset: Only kinds with rules allowed
|
||||
|
||||
---
|
||||
|
||||
## Debugging Policy Issues
|
||||
|
||||
Enable debug logging to see policy decisions:
|
||||
|
||||
```bash
|
||||
export ORLY_LOG_LEVEL=debug
|
||||
```
|
||||
|
||||
Log messages include:
|
||||
- Policy evaluation steps
|
||||
- Rule matching
|
||||
- Access decisions with reasons
|
||||
|
||||
---
|
||||
|
||||
## Source Code Reference
|
||||
|
||||
- Policy struct definition: `pkg/policy/policy.go:75-144` (Rule struct)
|
||||
- Policy struct definition: `pkg/policy/policy.go:380-412` (P struct)
|
||||
- Check evaluation: `pkg/policy/policy.go:1260-1595` (checkRulePolicy)
|
||||
- Write handler: `app/handle-event.go:114-138`
|
||||
- Read handler: `app/handle-req.go:420-438`
|
||||
@@ -134,6 +134,10 @@ CREATE (e)-[:AUTHORED_BY]->(a)
|
||||
eTagIndex := 0
|
||||
pTagIndex := 0
|
||||
|
||||
// Track if we need to add WITH clause before OPTIONAL MATCH
|
||||
// This is required because Cypher doesn't allow MATCH after CREATE without WITH
|
||||
needsWithClause := true
|
||||
|
||||
// Only process tags if they exist
|
||||
if ev.Tags != nil {
|
||||
for _, tagItem := range *ev.Tags {
|
||||
@@ -150,6 +154,15 @@ CREATE (e)-[:AUTHORED_BY]->(a)
|
||||
paramName := fmt.Sprintf("eTag_%d", eTagIndex)
|
||||
params[paramName] = tagValue
|
||||
|
||||
// Add WITH clause before first OPTIONAL MATCH to transition from CREATE to MATCH
|
||||
if needsWithClause {
|
||||
cypher += `
|
||||
// Carry forward event and author nodes for tag processing
|
||||
WITH e, a
|
||||
`
|
||||
needsWithClause = false
|
||||
}
|
||||
|
||||
cypher += fmt.Sprintf(`
|
||||
// Reference to event (e-tag)
|
||||
OPTIONAL MATCH (ref%d:Event {id: $%s})
|
||||
|
||||
@@ -1 +1 @@
|
||||
v0.31.8
|
||||
v0.31.9
|
||||
Reference in New Issue
Block a user