From c1bd05fb046d296d3b355a8d2ced8137023c08b7 Mon Sep 17 00:00:00 2001 From: mleku Date: Fri, 5 Dec 2025 11:25:34 +0000 Subject: [PATCH] Adjust ACL behavior for "none" mode and make query cache optional This commit allows skipping authentication, permission checks, and certain filters (e.g., deletions, expirations) when the ACL mode is set to "none" (open relay mode). It also introduces a configuration option to disable query caching to reduce memory usage. These changes improve operational flexibility for open relay setups and resource-constrained environments. --- .claude/settings.local.json | 191 ++----------------------------- app/config/config.go | 9 +- app/handle-event.go | 21 ++-- app/handle-nip43_test.go | 4 +- app/handle_policy_config_test.go | 2 +- app/nip43_e2e_test.go | 2 +- app/server.go | 72 ++++++------ app/web/src/App.svelte | 45 +++++--- app/web/src/ExportView.svelte | 25 ++-- app/web/src/ImportView.svelte | 8 +- main.go | 4 +- pkg/acl/acl.go | 8 ++ pkg/database/database.go | 8 +- pkg/database/factory.go | 9 +- pkg/database/query-events.go | 38 +++--- pkg/database/save-event.go | 18 +-- pkg/mode/mode.go | 18 +++ pkg/run/run.go | 2 +- pkg/version/version | 2 +- 19 files changed, 201 insertions(+), 285 deletions(-) create mode 100644 pkg/mode/mode.go 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