diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 3c63531..4205c61 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,190 +1,21 @@ { "permissions": { "allow": [ - "Skill(skill-creator)", - "Bash(cat:*)", - "Bash(python3:*)", - "Bash(find:*)", - "Skill(nostr-websocket)", + "Bash:*", + "Edit:*", + "Glob:*", + "Grep:*", + "Read:*", + "Skill:*", + "WebFetch:*", + "WebSearch:*", + "Write:*", "Bash(go build:*)", - "Bash(chmod:*)", - "Bash(journalctl:*)", - "Bash(timeout 5 bash -c 'echo [\"\"REQ\"\",\"\"test123\"\",{\"\"kinds\"\":[1],\"\"limit\"\":1}] | websocat ws://localhost:3334':*)", - "Bash(pkill:*)", - "Bash(timeout 5 bash:*)", - "Bash(md5sum:*)", - "Bash(timeout 3 bash -c 'echo [\\\"\"REQ\\\"\",\\\"\"test456\\\"\",{\\\"\"kinds\\\"\":[1],\\\"\"limit\\\"\":10}] | websocat ws://localhost:3334')", - "Bash(printf:*)", - "Bash(websocat:*)", "Bash(go test:*)", - "Bash(timeout 180 go test:*)", - "WebFetch(domain:github.com)", - "WebFetch(domain:raw.githubusercontent.com)", - "Bash(/tmp/find help)", - "Bash(/tmp/find verify-name example.com)", - "Skill(golang)", - "Bash(/tmp/find verify-name Bitcoin.Nostr)", - "Bash(/tmp/find generate-key)", - "Bash(git ls-tree:*)", - "Bash(CGO_ENABLED=0 go build:*)", - "Bash(CGO_ENABLED=0 go test:*)", - "Bash(app/web/dist/index.html)", - "Bash(export CGO_ENABLED=0)", - "Bash(bash:*)", - "Bash(CGO_ENABLED=0 ORLY_LOG_LEVEL=debug go test:*)", - "Bash(/tmp/test-policy-script.sh)", - "Bash(docker --version:*)", - "Bash(mkdir:*)", - "Bash(./test-docker-policy/test-policy.sh:*)", - "Bash(docker-compose:*)", - "Bash(tee:*)", - "Bash(docker logs:*)", - "Bash(timeout 5 websocat:*)", - "Bash(docker exec:*)", - "Bash(TESTSIG=\"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\":*)", - "Bash(echo:*)", - "Bash(git rm:*)", - "Bash(git add:*)", - "Bash(./test-policy.sh:*)", - "Bash(docker rm:*)", - "Bash(./scripts/docker-policy/test-policy.sh:*)", - "Bash(./policytest:*)", - "WebSearch", - "WebFetch(domain:blog.scottlogic.com)", - "WebFetch(domain:eli.thegreenplace.net)", - "WebFetch(domain:learn-wasm.dev)", - "Bash(curl:*)", - "Bash(./build.sh)", - "Bash(./pkg/wasm/shell/run.sh:*)", - "Bash(./run.sh echo.wasm)", - "Bash(./test.sh)", - "Bash(ORLY_PPROF=cpu ORLY_LOG_LEVEL=info ORLY_LISTEN=0.0.0.0 ORLY_PORT=3334 ORLY_ADMINS=npub1fjqqy4a93z5zsjwsfxqhc2764kvykfdyttvldkkkdera8dr78vhsmmleku ORLY_OWNERS=npub1fjqqy4a93z5zsjwsfxqhc2764kvykfdyttvldkkkdera8dr78vhsmmleku ORLY_ACL_MODE=follows ORLY_SPIDER_MODE=follows timeout 120 go run:*)", - "Bash(go tool pprof:*)", - "Bash(go get:*)", - "Bash(go mod tidy:*)", - "Bash(go list:*)", - "Bash(timeout 180 go build:*)", - "Bash(timeout 240 go build:*)", - "Bash(timeout 300 go build:*)", - "Bash(/tmp/orly:*)", - "Bash(./orly version:*)", - "Bash(git checkout:*)", - "Bash(docker ps:*)", - "Bash(./run-profile.sh:*)", - "Bash(sudo rm:*)", - "Bash(docker compose:*)", - "Bash(./run-benchmark.sh:*)", - "Bash(docker run:*)", - "Bash(docker inspect:*)", - "Bash(./run-benchmark-clean.sh:*)", - "Bash(cd:*)", - "Bash(CGO_ENABLED=0 timeout 180 go build:*)", - "Bash(/home/mleku/src/next.orly.dev/pkg/dgraph/dgraph.go)", - "Bash(ORLY_LOG_LEVEL=debug timeout 60 ./orly:*)", - "Bash(ORLY_LOG_LEVEL=debug timeout 30 ./orly:*)", - "Bash(killall:*)", - "Bash(kill:*)", - "Bash(gh repo list:*)", - "Bash(gh auth:*)", - "Bash(/tmp/backup-github-repos.sh)", - "Bash(./benchmark:*)", - "Bash(env)", - "Bash(./run-badger-benchmark.sh:*)", - "Bash(./update-github-vpn.sh:*)", - "Bash(dmesg:*)", - "Bash(export:*)", - "Bash(timeout 60 /tmp/benchmark-fixed:*)", - "Bash(/tmp/test-auth-event.sh)", - "Bash(CGO_ENABLED=0 timeout 180 go test:*)", - "Bash(/tmp/benchmark-real-events:*)", - "Bash(CGO_ENABLED=0 timeout 240 go build:*)", - "Bash(/tmp/benchmark-final --events 500 --workers 2 --datadir /tmp/test-real-final)", - "Bash(timeout 60 /tmp/benchmark-final:*)", - "Bash(timeout 120 ./benchmark:*)", - "Bash(timeout 60 ./benchmark:*)", - "Bash(timeout 30 ./benchmark:*)", - "Bash(timeout 15 ./benchmark:*)", - "Bash(docker build:*)", - "Bash(xargs:*)", - "Bash(timeout 30 sh:*)", - "Bash(timeout 60 go test:*)", - "Bash(timeout 120 go test:*)", - "Bash(timeout 180 ./scripts/test.sh:*)", - "Bash(CGO_ENABLED=0 timeout 60 go test:*)", - "Bash(CGO_ENABLED=1 go build:*)", - "Bash(lynx:*)", - "Bash(sed:*)", - "Bash(docker stop:*)", - "Bash(grep:*)", - "Bash(timeout 30 go test:*)", - "Bash(tree:*)", - "Bash(timeout 180 ./migrate-imports.sh:*)", - "Bash(./migrate-fast.sh:*)", - "Bash(git restore:*)", - "Bash(go mod download:*)", - "Bash(go clean:*)", - "Bash(GOSUMDB=off CGO_ENABLED=0 timeout 240 go build:*)", - "Bash(CGO_ENABLED=0 GOFLAGS=-mod=mod timeout 240 go build:*)", - "Bash(CGO_ENABLED=0 timeout 120 go test:*)", - "Bash(./cmd/blossomtest/blossomtest:*)", - "Bash(sudo journalctl:*)", - "Bash(systemctl:*)", - "Bash(systemctl show:*)", - "Bash(ssh relay1:*)", - "Bash(done)", - "Bash(go run:*)", - "Bash(go doc:*)", - "Bash(/tmp/orly-test help:*)", - "Bash(go version:*)", - "Bash(ss:*)", - "Bash(CGO_ENABLED=0 go clean:*)", - "Bash(CGO_ENABLED=0 timeout 30 go test:*)", - "Bash(~/.local/bin/tea issue 6 --repo mleku/next.orly.dev --remote https://git.nostrdev.com)", - "Bash(tea issue:*)", - "Bash(tea issues view:*)", - "Bash(tea issue view:*)", - "Bash(tea issues:*)", - "Bash(bun run build:*)", - "Bash(git tag:*)", - "Bash(/tmp/orly-test version:*)", - "Bash(git log:*)", - "Bash(git show:*)", - "Bash(git config:*)", - "Bash(git check-ignore:*)", - "Bash(git commit:*)", - "WebFetch(domain:www.npmjs.com)", - "Bash(git stash:*)", - "WebFetch(domain:arxiv.org)", - "WebFetch(domain:hal.science)", - "WebFetch(domain:pkg.go.dev)", - "Bash(GOOS=js GOARCH=wasm CGO_ENABLED=0 go build:*)", - "Bash(GOOS=js GOARCH=wasm go doc:*)", - "Bash(GOOS=js GOARCH=wasm CGO_ENABLED=0 go test:*)", - "Bash(node --version:*)", - "Bash(npm install)", - "Bash(node run_wasm_tests.mjs:*)", - "Bash(go env:*)", - "Bash(GOROOT=/home/mleku/go node run_wasm_tests.mjs:*)", - "Bash(./orly:*)", - "Bash(./orly -version:*)", - "Bash(./orly --version:*)", - "Bash(GOOS=js GOARCH=wasm go test:*)", - "Bash(ls:*)", - "Bash(GOROOT=/home/mleku/go node:*)", - "Bash(GOOS=js GOARCH=wasm go build:*)", - "Bash(go mod graph:*)", - "Bash(xxd:*)", - "Bash(CGO_ENABLED=0 go mod tidy:*)", - "WebFetch(domain:git.mleku.dev)", - "Bash(CGO_ENABLED=0 LOG_LEVEL=trace go test:*)", - "Bash(go vet:*)", - "Bash(gofmt:*)", - "Skill(cypher)", - "Bash(git mv:*)", - "Bash(CGO_ENABLED=0 go run:*)" + "Bash(./scripts/test.sh:*)" ], "deny": [], "ask": [] }, - "outputStyle": "Explanatory" + "outputStyle": "Default" } diff --git a/app/config/config.go b/app/config/config.go index 2b29d05..32c5fa6 100644 --- a/app/config/config.go +++ b/app/config/config.go @@ -91,9 +91,10 @@ type C struct { NIP43InviteExpiry time.Duration `env:"ORLY_NIP43_INVITE_EXPIRY" default:"24h" usage:"how long invite codes remain valid"` // Database configuration - DBType string `env:"ORLY_DB_TYPE" default:"badger" usage:"database backend to use: badger or neo4j"` - QueryCacheSizeMB int `env:"ORLY_QUERY_CACHE_SIZE_MB" default:"512" usage:"query cache size in MB (caches database query results for faster REQ responses)"` - QueryCacheMaxAge string `env:"ORLY_QUERY_CACHE_MAX_AGE" default:"5m" usage:"maximum age for cached query results (e.g., 5m, 10m, 1h)"` + DBType string `env:"ORLY_DB_TYPE" default:"badger" usage:"database backend to use: badger or neo4j"` + QueryCacheDisabled bool `env:"ORLY_QUERY_CACHE_DISABLED" default:"true" usage:"disable query cache to reduce memory usage (trades memory for query performance)"` + QueryCacheSizeMB int `env:"ORLY_QUERY_CACHE_SIZE_MB" default:"512" usage:"query cache size in MB (caches database query results for faster REQ responses)"` + QueryCacheMaxAge string `env:"ORLY_QUERY_CACHE_MAX_AGE" default:"5m" usage:"maximum age for cached query results (e.g., 5m, 10m, 1h)"` // Neo4j configuration (only used when ORLY_DB_TYPE=neo4j) Neo4jURI string `env:"ORLY_NEO4J_URI" default:"bolt://localhost:7687" usage:"Neo4j bolt URI (only used when ORLY_DB_TYPE=neo4j)"` @@ -410,6 +411,7 @@ func (cfg *C) GetDatabaseConfigValues() ( dataDir, logLevel string, blockCacheMB, indexCacheMB, queryCacheSizeMB int, queryCacheMaxAge time.Duration, + queryCacheDisabled bool, serialCachePubkeys, serialCacheEventIds int, zstdLevel int, neo4jURI, neo4jUser, neo4jPassword string, @@ -425,6 +427,7 @@ func (cfg *C) GetDatabaseConfigValues() ( return cfg.DataDir, cfg.DBLogLevel, cfg.DBBlockCacheMB, cfg.DBIndexCacheMB, cfg.QueryCacheSizeMB, queryCacheMaxAge, + cfg.QueryCacheDisabled, cfg.SerialCachePubkeys, cfg.SerialCacheEventIds, cfg.DBZSTDLevel, cfg.Neo4jURI, cfg.Neo4jUser, cfg.Neo4jPassword diff --git a/app/handle-event.go b/app/handle-event.go index 728cc51..1e323e4 100644 --- a/app/handle-event.go +++ b/app/handle-event.go @@ -656,15 +656,18 @@ func (l *Listener) HandleEvent(msg []byte) (err error) { return } else { // check if the event was deleted - // Combine admins and owners for deletion checking - adminOwners := append(l.Admins, l.Owners...) - if err = l.DB.CheckForDeleted(env.E, adminOwners); err != nil { - if strings.HasPrefix(err.Error(), "blocked:") { - errStr := err.Error()[len("blocked: "):len(err.Error())] - if err = Ok.Error( - l, env, errStr, - ); chk.E(err) { - return + // Skip deletion check when ACL is "none" (open relay mode) + if acl.Registry.Active.Load() != "none" { + // Combine admins and owners for deletion checking + adminOwners := append(l.Admins, l.Owners...) + if err = l.DB.CheckForDeleted(env.E, adminOwners); err != nil { + if strings.HasPrefix(err.Error(), "blocked:") { + errStr := err.Error()[len("blocked: "):len(err.Error())] + if err = Ok.Error( + l, env, errStr, + ); chk.E(err) { + return + } } } } diff --git a/app/handle-nip43_test.go b/app/handle-nip43_test.go index d20ea0a..2fe0a2c 100644 --- a/app/handle-nip43_test.go +++ b/app/handle-nip43_test.go @@ -54,7 +54,7 @@ func setupTestListener(t *testing.T) (*Listener, *database.D, func()) { } // Configure ACL registry - acl.Registry.Active.Store(cfg.ACLMode) + acl.Registry.SetMode(cfg.ACLMode) if err = acl.Registry.Configure(cfg, db, ctx); err != nil { db.Close() os.RemoveAll(tempDir) @@ -378,7 +378,7 @@ func TestHandleNIP43InviteRequest_ValidRequest(t *testing.T) { // Add admin to config and reconfigure ACL adminHex := hex.Enc(adminPubkey) listener.Server.Config.Admins = []string{adminHex} - acl.Registry.Active.Store("none") + acl.Registry.SetMode("none") if err = acl.Registry.Configure(listener.Server.Config, listener.Server.DB, listener.ctx); err != nil { t.Fatalf("failed to reconfigure ACL: %v", err) } diff --git a/app/handle_policy_config_test.go b/app/handle_policy_config_test.go index 3532872..7bfd537 100644 --- a/app/handle_policy_config_test.go +++ b/app/handle_policy_config_test.go @@ -88,7 +88,7 @@ func setupPolicyTestListener(t *testing.T, policyAdminHex string) (*Listener, *d } // Configure ACL registry - acl.Registry.Active.Store(cfg.ACLMode) + acl.Registry.SetMode(cfg.ACLMode) if err = acl.Registry.Configure(cfg, db, ctx); err != nil { db.Close() os.RemoveAll(tempDir) diff --git a/app/nip43_e2e_test.go b/app/nip43_e2e_test.go index 0a77483..9bb2dab 100644 --- a/app/nip43_e2e_test.go +++ b/app/nip43_e2e_test.go @@ -105,7 +105,7 @@ func setupE2ETest(t *testing.T) (*Server, *httptest.Server, func()) { } // Configure ACL registry - acl.Registry.Active.Store(cfg.ACLMode) + acl.Registry.SetMode(cfg.ACLMode) if err = acl.Registry.Configure(cfg, db, ctx); err != nil { db.Close() os.RemoveAll(tempDir) diff --git a/app/server.go b/app/server.go index 0e6ee4c..97490cc 100644 --- a/app/server.go +++ b/app/server.go @@ -550,25 +550,28 @@ func (s *Server) handleExport(w http.ResponseWriter, r *http.Request) { return } - // Validate NIP-98 authentication - valid, pubkey, err := httpauth.CheckAuth(r) - if chk.E(err) || !valid { - errorMsg := "NIP-98 authentication validation failed" - if err != nil { - errorMsg = err.Error() + // Skip authentication and permission checks when ACL is "none" (open relay mode) + if acl.Registry.Active.Load() != "none" { + // Validate NIP-98 authentication + valid, pubkey, err := httpauth.CheckAuth(r) + if chk.E(err) || !valid { + errorMsg := "NIP-98 authentication validation failed" + if err != nil { + errorMsg = err.Error() + } + http.Error(w, errorMsg, http.StatusUnauthorized) + return } - http.Error(w, errorMsg, http.StatusUnauthorized) - return - } - // Check permissions - require write, admin, or owner level - accessLevel := acl.Registry.GetAccessLevel(pubkey, r.RemoteAddr) - if accessLevel != "write" && accessLevel != "admin" && accessLevel != "owner" { - http.Error( - w, "Write, admin, or owner permission required", - http.StatusForbidden, - ) - return + // Check permissions - require write, admin, or owner level + accessLevel := acl.Registry.GetAccessLevel(pubkey, r.RemoteAddr) + if accessLevel != "write" && accessLevel != "admin" && accessLevel != "owner" { + http.Error( + w, "Write, admin, or owner permission required", + http.StatusForbidden, + ) + return + } } // Parse pubkeys from request @@ -719,24 +722,27 @@ func (s *Server) handleImport(w http.ResponseWriter, r *http.Request) { return } - // Validate NIP-98 authentication - valid, pubkey, err := httpauth.CheckAuth(r) - if chk.E(err) || !valid { - errorMsg := "NIP-98 authentication validation failed" - if err != nil { - errorMsg = err.Error() + // Skip authentication and permission checks when ACL is "none" (open relay mode) + if acl.Registry.Active.Load() != "none" { + // Validate NIP-98 authentication + valid, pubkey, err := httpauth.CheckAuth(r) + if chk.E(err) || !valid { + errorMsg := "NIP-98 authentication validation failed" + if err != nil { + errorMsg = err.Error() + } + http.Error(w, errorMsg, http.StatusUnauthorized) + return } - http.Error(w, errorMsg, http.StatusUnauthorized) - return - } - // Check permissions - require admin or owner level - accessLevel := acl.Registry.GetAccessLevel(pubkey, r.RemoteAddr) - if accessLevel != "admin" && accessLevel != "owner" { - http.Error( - w, "Admin or owner permission required", http.StatusForbidden, - ) - return + // Check permissions - require admin or owner level + accessLevel := acl.Registry.GetAccessLevel(pubkey, r.RemoteAddr) + if accessLevel != "admin" && accessLevel != "owner" { + http.Error( + w, "Admin or owner permission required", http.StatusForbidden, + ) + return + } } ct := r.Header.Get("Content-Type") diff --git a/app/web/src/App.svelte b/app/web/src/App.svelte index 8fc759b..ce6534d 100644 --- a/app/web/src/App.svelte +++ b/app/web/src/App.svelte @@ -2276,13 +2276,16 @@ // Export functionality async function exportEvents(pubkeys = []) { - if (!isLoggedIn) { + // Skip login check when ACL is "none" (open relay mode) + if (aclMode !== "none" && !isLoggedIn) { alert("Please log in first"); return; } // Check permissions for exporting all events using current effective role + // Skip permission check when ACL is "none" if ( + aclMode !== "none" && pubkeys.length === 0 && currentEffectiveRole !== "admin" && currentEffectiveRole !== "owner" @@ -2292,16 +2295,19 @@ } try { - const authHeader = await createNIP98AuthHeader( - "/api/export", - "POST", - ); + // Build headers - only include auth when ACL is not "none" + const headers = { + "Content-Type": "application/json", + }; + if (aclMode !== "none" && isLoggedIn) { + headers.Authorization = await createNIP98AuthHeader( + "/api/export", + "POST", + ); + } const response = await fetch("/api/export", { method: "POST", - headers: { - Authorization: authHeader, - "Content-Type": "application/json", - }, + headers, body: JSON.stringify({ pubkeys }), }); @@ -2354,7 +2360,8 @@ } async function importEvents() { - if (!isLoggedIn || (userRole !== "admin" && userRole !== "owner")) { + // Skip login/permission check when ACL is "none" (open relay mode) + if (aclMode !== "none" && (!isLoggedIn || (userRole !== "admin" && userRole !== "owner"))) { alert("Admin or owner permission required"); return; } @@ -2365,18 +2372,20 @@ } try { - const authHeader = await createNIP98AuthHeader( - "/api/import", - "POST", - ); + // Build headers - only include auth when ACL is not "none" + const headers = {}; + if (aclMode !== "none" && isLoggedIn) { + headers.Authorization = await createNIP98AuthHeader( + "/api/import", + "POST", + ); + } const formData = new FormData(); formData.append("file", selectedFile); const response = await fetch("/api/import", { method: "POST", - headers: { - Authorization: authHeader, - }, + headers, body: formData, }); @@ -2932,6 +2941,7 @@ export let isLoggedIn = false; export let currentEffectiveRole = ""; + export let aclMode = ""; import { createEventDispatcher } from "svelte"; const dispatch = createEventDispatcher(); + // When ACL is "none", allow access without login + $: canAccess = aclMode === "none" || isLoggedIn; + $: canExportAll = aclMode === "none" || currentEffectiveRole === "admin" || currentEffectiveRole === "owner"; + function exportMyEvents() { dispatch("exportMyEvents"); } @@ -18,15 +23,17 @@ } -{#if isLoggedIn} -
-

Export My Events

-

Download your personal events as a JSONL file.

- -
- {#if currentEffectiveRole === "admin" || currentEffectiveRole === "owner"} +{#if canAccess} + {#if isLoggedIn} +
+

Export My Events

+

Download your personal events as a JSONL file.

+ +
+ {/if} + {#if canExportAll}

Export All Events

diff --git a/app/web/src/ImportView.svelte b/app/web/src/ImportView.svelte index 6c19182..48a191d 100644 --- a/app/web/src/ImportView.svelte +++ b/app/web/src/ImportView.svelte @@ -2,10 +2,14 @@ export let isLoggedIn = false; export let currentEffectiveRole = ""; export let selectedFile = null; + export let aclMode = ""; import { createEventDispatcher } from "svelte"; const dispatch = createEventDispatcher(); + // When ACL is "none", allow access without login + $: canImport = aclMode === "none" || (isLoggedIn && (currentEffectiveRole === "admin" || currentEffectiveRole === "owner")); + function handleFileSelect(event) { dispatch("fileSelect", event); } @@ -20,7 +24,7 @@

- {#if isLoggedIn && (currentEffectiveRole === "admin" || currentEffectiveRole === "owner")} + {#if canImport}

Import Events

Upload a JSONL file to import events into the database.

@@ -42,7 +46,7 @@

Import Events

- ❌ Admin or owner permission required for import functionality. + Admin or owner permission required for import functionality.

{:else} diff --git a/main.go b/main.go index 4c7e5e0..deb993f 100644 --- a/main.go +++ b/main.go @@ -330,7 +330,7 @@ func main() { os.Exit(1) } log.I.F("%s database initialized successfully", cfg.DBType) - acl.Registry.Active.Store(cfg.ACLMode) + acl.Registry.SetMode(cfg.ACLMode) if err = acl.Registry.Configure(cfg, db, ctx); chk.E(err) { os.Exit(1) } @@ -444,6 +444,7 @@ func makeDatabaseConfig(cfg *config.C) *database.DatabaseConfig { dataDir, logLevel, blockCacheMB, indexCacheMB, queryCacheSizeMB, queryCacheMaxAge, + queryCacheDisabled, serialCachePubkeys, serialCacheEventIds, zstdLevel, neo4jURI, neo4jUser, neo4jPassword := cfg.GetDatabaseConfigValues() @@ -455,6 +456,7 @@ func makeDatabaseConfig(cfg *config.C) *database.DatabaseConfig { IndexCacheMB: indexCacheMB, QueryCacheSizeMB: queryCacheSizeMB, QueryCacheMaxAge: queryCacheMaxAge, + QueryCacheDisabled: queryCacheDisabled, SerialCachePubkeys: serialCachePubkeys, SerialCacheEventIds: serialCacheEventIds, ZSTDLevel: zstdLevel, diff --git a/pkg/acl/acl.go b/pkg/acl/acl.go index f2c070c..f685e0d 100644 --- a/pkg/acl/acl.go +++ b/pkg/acl/acl.go @@ -3,11 +3,19 @@ package acl import ( "git.mleku.dev/mleku/nostr/encoders/event" acliface "next.orly.dev/pkg/interfaces/acl" + "next.orly.dev/pkg/mode" "next.orly.dev/pkg/utils/atomic" ) var Registry = &S{} +// SetMode sets the active ACL mode and syncs it to the mode package for +// packages that need to check the mode without importing acl (to avoid cycles). +func (s *S) SetMode(m string) { + s.Active.Store(m) + mode.ACLMode.Store(m) +} + type S struct { ACL []acliface.I Active atomic.String diff --git a/pkg/database/database.go b/pkg/database/database.go index 012a0e4..1f106a9 100644 --- a/pkg/database/database.go +++ b/pkg/database/database.go @@ -106,6 +106,12 @@ func NewWithConfig( queryCacheSize := int64(queryCacheSizeMB * 1024 * 1024) + // Create query cache only if not disabled + var qc *querycache.EventCache + if !cfg.QueryCacheDisabled { + qc = querycache.NewEventCache(queryCacheSize, queryCacheMaxAge) + } + d = &D{ ctx: ctx, cancel: cancel, @@ -114,7 +120,7 @@ func NewWithConfig( DB: nil, seq: nil, ready: make(chan struct{}), - queryCache: querycache.NewEventCache(queryCacheSize, queryCacheMaxAge), + queryCache: qc, serialCache: NewSerialCache(serialCachePubkeys, serialCacheEventIds), } diff --git a/pkg/database/factory.go b/pkg/database/factory.go index 7276b75..936127c 100644 --- a/pkg/database/factory.go +++ b/pkg/database/factory.go @@ -18,10 +18,11 @@ type DatabaseConfig struct { LogLevel string // Badger-specific settings - BlockCacheMB int // ORLY_DB_BLOCK_CACHE_MB - IndexCacheMB int // ORLY_DB_INDEX_CACHE_MB - QueryCacheSizeMB int // ORLY_QUERY_CACHE_SIZE_MB - QueryCacheMaxAge time.Duration // ORLY_QUERY_CACHE_MAX_AGE + BlockCacheMB int // ORLY_DB_BLOCK_CACHE_MB + IndexCacheMB int // ORLY_DB_INDEX_CACHE_MB + QueryCacheDisabled bool // ORLY_QUERY_CACHE_DISABLED - disable query cache to reduce memory usage + QueryCacheSizeMB int // ORLY_QUERY_CACHE_SIZE_MB + QueryCacheMaxAge time.Duration // ORLY_QUERY_CACHE_MAX_AGE // Serial cache settings for compact event storage SerialCachePubkeys int // ORLY_SERIAL_CACHE_PUBKEYS - max pubkeys to cache (default: 100000) diff --git a/pkg/database/query-events.go b/pkg/database/query-events.go index d29163c..4548189 100644 --- a/pkg/database/query-events.go +++ b/pkg/database/query-events.go @@ -13,6 +13,7 @@ import ( "lol.mleku.dev/log" "github.com/minio/sha256-simd" "next.orly.dev/pkg/database/indexes/types" + "next.orly.dev/pkg/mode" "git.mleku.dev/mleku/nostr/encoders/event" "git.mleku.dev/mleku/nostr/encoders/filter" "git.mleku.dev/mleku/nostr/encoders/hex" @@ -102,7 +103,8 @@ func (d *D) QueryEventsWithOptions(c context.Context, f *filter.F, includeDelete } // check for an expiration tag and delete after returning the result - if CheckExpiration(ev) { + // Skip expiration check when ACL is "none" (open relay mode) + if !mode.IsOpen() && CheckExpiration(ev) { log.T.F( "QueryEvents: id=%s filtered out due to expiration", idHex, ) @@ -112,9 +114,12 @@ func (d *D) QueryEventsWithOptions(c context.Context, f *filter.F, includeDelete } // skip events that have been deleted by a proper deletion event - if derr := d.CheckForDeleted(ev, nil); derr != nil { - log.T.F("QueryEvents: id=%s filtered out due to deletion: %v", idHex, derr) - continue + // Skip deletion check when ACL is "none" (open relay mode) + if !mode.IsOpen() { + if derr := d.CheckForDeleted(ev, nil); derr != nil { + log.T.F("QueryEvents: id=%s filtered out due to deletion: %v", idHex, derr) + continue + } } // Add the event to the results @@ -210,13 +215,15 @@ func (d *D) QueryEventsWithOptions(c context.Context, f *filter.F, includeDelete } // check for an expiration tag and delete after returning the result - if CheckExpiration(ev) { + // Skip expiration check when ACL is "none" (open relay mode) + if !mode.IsOpen() && CheckExpiration(ev) { expDeletes = append(expDeletes, ser) expEvs = append(expEvs, ev) continue } // Process deletion events to build our deletion maps - if ev.Kind == kind.Deletion.K { + // Skip deletion processing when ACL is "none" (open relay mode) + if !mode.IsOpen() && ev.Kind == kind.Deletion.K { // Check for 'e' tags that directly reference event IDs eTags := ev.Tags.GetAll([]byte("e")) for _, eTag := range eTags { @@ -433,8 +440,10 @@ func (d *D) QueryEventsWithOptions(c context.Context, f *filter.F, includeDelete } } // Check if this specific event has been deleted + // Skip deletion checks when ACL is "none" (open relay mode) + aclActive := !mode.IsOpen() eventIdHex := hex.Enc(ev.ID) - if deletedEventIds[eventIdHex] { + if aclActive && deletedEventIds[eventIdHex] { // Skip this event if it has been specifically deleted continue } @@ -446,7 +455,7 @@ func (d *D) QueryEventsWithOptions(c context.Context, f *filter.F, includeDelete // deletion Only skip this event if it has been deleted by // kind/pubkey and is not in the filter AND there isn't a newer // event with the same kind/pubkey - if deletionsByKindPubkey[key] && !isIdInFilter { + if aclActive && deletionsByKindPubkey[key] && !isIdInFilter { // This replaceable event has been deleted, skip it continue } else if wantMultipleVersions { @@ -475,11 +484,14 @@ func (d *D) QueryEventsWithOptions(c context.Context, f *filter.F, includeDelete } // Check if this event has been deleted via an a-tag - if deletionMap, exists := deletionsByKindPubkeyDTag[key]; exists { - // If there is a deletion timestamp and this event is older than the deletion, - // and this event is not specifically requested by ID, skip it - if delTs, ok := deletionMap[dValue]; ok && ev.CreatedAt < delTs && !isIdInFilter { - continue + // Skip deletion check when ACL is "none" (open relay mode) + if aclActive { + if deletionMap, exists := deletionsByKindPubkeyDTag[key]; exists { + // If there is a deletion timestamp and this event is older than the deletion, + // and this event is not specifically requested by ID, skip it + if delTs, ok := deletionMap[dValue]; ok && ev.CreatedAt < delTs && !isIdInFilter { + continue + } } } diff --git a/pkg/database/save-event.go b/pkg/database/save-event.go index b9aaceb..c138ffa 100644 --- a/pkg/database/save-event.go +++ b/pkg/database/save-event.go @@ -14,6 +14,7 @@ import ( "lol.mleku.dev/log" "next.orly.dev/pkg/database/indexes" "next.orly.dev/pkg/database/indexes/types" + "next.orly.dev/pkg/mode" "git.mleku.dev/mleku/nostr/encoders/event" "git.mleku.dev/mleku/nostr/encoders/filter" "git.mleku.dev/mleku/nostr/encoders/hex" @@ -177,13 +178,16 @@ func (d *D) SaveEvent(c context.Context, ev *event.E) ( } // Check if the event has been deleted before allowing resubmission - if err = d.CheckForDeleted(ev, nil); err != nil { - // log.I.F( - // "SaveEvent: rejecting resubmission of deleted event ID=%s: %v", - // hex.Enc(ev.ID), err, - // ) - err = fmt.Errorf("blocked: %s", err.Error()) - return + // Skip deletion check when ACL is "none" (open relay mode) + if !mode.IsOpen() { + if err = d.CheckForDeleted(ev, nil); err != nil { + // log.I.F( + // "SaveEvent: rejecting resubmission of deleted event ID=%s: %v", + // hex.Enc(ev.ID), err, + // ) + err = fmt.Errorf("blocked: %s", err.Error()) + return + } } // check for replacement - only validate, don't delete old events if kind.IsReplaceable(ev.Kind) || kind.IsParameterizedReplaceable(ev.Kind) { diff --git a/pkg/mode/mode.go b/pkg/mode/mode.go new file mode 100644 index 0000000..673afd9 --- /dev/null +++ b/pkg/mode/mode.go @@ -0,0 +1,18 @@ +// Package mode provides a global ACL mode indicator that can be read by +// packages that need to know the current access control mode without creating +// circular dependencies. +package mode + +import "next.orly.dev/pkg/utils/atomic" + +// ACLMode holds the current ACL mode as a string. +// This is set by the ACL package when configured and can be read by other +// packages (like database) to adjust their behavior. +var ACLMode atomic.String + +// IsOpen returns true if the ACL mode is "none" (open relay mode). +// In open mode, security filtering (expiration, deletion, privileged events) +// should be disabled. +func IsOpen() bool { + return ACLMode.Load() == "none" +} diff --git a/pkg/run/run.go b/pkg/run/run.go index 699239b..934b2ae 100644 --- a/pkg/run/run.go +++ b/pkg/run/run.go @@ -120,7 +120,7 @@ func Start(cfg *config.C, opts *Options) (relay *Relay, err error) { } // Configure ACL - acl.Registry.Active.Store(cfg.ACLMode) + acl.Registry.SetMode(cfg.ACLMode) if err = acl.Registry.Configure(cfg, relay.db, relay.ctx); chk.E(err) { return } diff --git a/pkg/version/version b/pkg/version/version index 05d724a..9d31093 100644 --- a/pkg/version/version +++ b/pkg/version/version @@ -1 +1 @@ -v0.34.2 \ No newline at end of file +v0.34.3 \ No newline at end of file