Browse Source

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.
main
mleku 1 month ago
parent
commit
c1bd05fb04
No known key found for this signature in database
  1. 191
      .claude/settings.local.json
  2. 3
      app/config/config.go
  3. 3
      app/handle-event.go
  4. 4
      app/handle-nip43_test.go
  5. 2
      app/handle_policy_config_test.go
  6. 2
      app/nip43_e2e_test.go
  7. 6
      app/server.go
  8. 33
      app/web/src/App.svelte
  9. 9
      app/web/src/ExportView.svelte
  10. 8
      app/web/src/ImportView.svelte
  11. 4
      main.go
  12. 8
      pkg/acl/acl.go
  13. 8
      pkg/database/database.go
  14. 1
      pkg/database/factory.go
  15. 22
      pkg/database/query-events.go
  16. 4
      pkg/database/save-event.go
  17. 18
      pkg/mode/mode.go
  18. 2
      pkg/run/run.go
  19. 2
      pkg/version/version

191
.claude/settings.local.json

@ -1,190 +1,21 @@
{ {
"permissions": { "permissions": {
"allow": [ "allow": [
"Skill(skill-creator)", "Bash:*",
"Bash(cat:*)", "Edit:*",
"Bash(python3:*)", "Glob:*",
"Bash(find:*)", "Grep:*",
"Skill(nostr-websocket)", "Read:*",
"Skill:*",
"WebFetch:*",
"WebSearch:*",
"Write:*",
"Bash(go build:*)", "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(go test:*)",
"Bash(timeout 180 go test:*)", "Bash(./scripts/test.sh:*)"
"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:*)"
], ],
"deny": [], "deny": [],
"ask": [] "ask": []
}, },
"outputStyle": "Explanatory" "outputStyle": "Default"
} }

3
app/config/config.go

@ -92,6 +92,7 @@ type C struct {
// Database configuration // Database configuration
DBType string `env:"ORLY_DB_TYPE" default:"badger" usage:"database backend to use: badger or neo4j"` 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)"` 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)"` QueryCacheMaxAge string `env:"ORLY_QUERY_CACHE_MAX_AGE" default:"5m" usage:"maximum age for cached query results (e.g., 5m, 10m, 1h)"`
@ -410,6 +411,7 @@ func (cfg *C) GetDatabaseConfigValues() (
dataDir, logLevel string, dataDir, logLevel string,
blockCacheMB, indexCacheMB, queryCacheSizeMB int, blockCacheMB, indexCacheMB, queryCacheSizeMB int,
queryCacheMaxAge time.Duration, queryCacheMaxAge time.Duration,
queryCacheDisabled bool,
serialCachePubkeys, serialCacheEventIds int, serialCachePubkeys, serialCacheEventIds int,
zstdLevel int, zstdLevel int,
neo4jURI, neo4jUser, neo4jPassword string, neo4jURI, neo4jUser, neo4jPassword string,
@ -425,6 +427,7 @@ func (cfg *C) GetDatabaseConfigValues() (
return cfg.DataDir, cfg.DBLogLevel, return cfg.DataDir, cfg.DBLogLevel,
cfg.DBBlockCacheMB, cfg.DBIndexCacheMB, cfg.QueryCacheSizeMB, cfg.DBBlockCacheMB, cfg.DBIndexCacheMB, cfg.QueryCacheSizeMB,
queryCacheMaxAge, queryCacheMaxAge,
cfg.QueryCacheDisabled,
cfg.SerialCachePubkeys, cfg.SerialCacheEventIds, cfg.SerialCachePubkeys, cfg.SerialCacheEventIds,
cfg.DBZSTDLevel, cfg.DBZSTDLevel,
cfg.Neo4jURI, cfg.Neo4jUser, cfg.Neo4jPassword cfg.Neo4jURI, cfg.Neo4jUser, cfg.Neo4jPassword

3
app/handle-event.go

@ -656,6 +656,8 @@ func (l *Listener) HandleEvent(msg []byte) (err error) {
return return
} else { } else {
// check if the event was deleted // check if the event was deleted
// Skip deletion check when ACL is "none" (open relay mode)
if acl.Registry.Active.Load() != "none" {
// Combine admins and owners for deletion checking // Combine admins and owners for deletion checking
adminOwners := append(l.Admins, l.Owners...) adminOwners := append(l.Admins, l.Owners...)
if err = l.DB.CheckForDeleted(env.E, adminOwners); err != nil { if err = l.DB.CheckForDeleted(env.E, adminOwners); err != nil {
@ -669,6 +671,7 @@ func (l *Listener) HandleEvent(msg []byte) (err error) {
} }
} }
} }
}
// store the event - use a separate context to prevent cancellation issues // store the event - use a separate context to prevent cancellation issues
saveCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) saveCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel() defer cancel()

4
app/handle-nip43_test.go

@ -54,7 +54,7 @@ func setupTestListener(t *testing.T) (*Listener, *database.D, func()) {
} }
// Configure ACL registry // Configure ACL registry
acl.Registry.Active.Store(cfg.ACLMode) acl.Registry.SetMode(cfg.ACLMode)
if err = acl.Registry.Configure(cfg, db, ctx); err != nil { if err = acl.Registry.Configure(cfg, db, ctx); err != nil {
db.Close() db.Close()
os.RemoveAll(tempDir) os.RemoveAll(tempDir)
@ -378,7 +378,7 @@ func TestHandleNIP43InviteRequest_ValidRequest(t *testing.T) {
// Add admin to config and reconfigure ACL // Add admin to config and reconfigure ACL
adminHex := hex.Enc(adminPubkey) adminHex := hex.Enc(adminPubkey)
listener.Server.Config.Admins = []string{adminHex} 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 { if err = acl.Registry.Configure(listener.Server.Config, listener.Server.DB, listener.ctx); err != nil {
t.Fatalf("failed to reconfigure ACL: %v", err) t.Fatalf("failed to reconfigure ACL: %v", err)
} }

2
app/handle_policy_config_test.go

@ -88,7 +88,7 @@ func setupPolicyTestListener(t *testing.T, policyAdminHex string) (*Listener, *d
} }
// Configure ACL registry // Configure ACL registry
acl.Registry.Active.Store(cfg.ACLMode) acl.Registry.SetMode(cfg.ACLMode)
if err = acl.Registry.Configure(cfg, db, ctx); err != nil { if err = acl.Registry.Configure(cfg, db, ctx); err != nil {
db.Close() db.Close()
os.RemoveAll(tempDir) os.RemoveAll(tempDir)

2
app/nip43_e2e_test.go

@ -105,7 +105,7 @@ func setupE2ETest(t *testing.T) (*Server, *httptest.Server, func()) {
} }
// Configure ACL registry // Configure ACL registry
acl.Registry.Active.Store(cfg.ACLMode) acl.Registry.SetMode(cfg.ACLMode)
if err = acl.Registry.Configure(cfg, db, ctx); err != nil { if err = acl.Registry.Configure(cfg, db, ctx); err != nil {
db.Close() db.Close()
os.RemoveAll(tempDir) os.RemoveAll(tempDir)

6
app/server.go

@ -550,6 +550,8 @@ func (s *Server) handleExport(w http.ResponseWriter, r *http.Request) {
return return
} }
// Skip authentication and permission checks when ACL is "none" (open relay mode)
if acl.Registry.Active.Load() != "none" {
// Validate NIP-98 authentication // Validate NIP-98 authentication
valid, pubkey, err := httpauth.CheckAuth(r) valid, pubkey, err := httpauth.CheckAuth(r)
if chk.E(err) || !valid { if chk.E(err) || !valid {
@ -570,6 +572,7 @@ func (s *Server) handleExport(w http.ResponseWriter, r *http.Request) {
) )
return return
} }
}
// Parse pubkeys from request // Parse pubkeys from request
var pks [][]byte var pks [][]byte
@ -719,6 +722,8 @@ func (s *Server) handleImport(w http.ResponseWriter, r *http.Request) {
return return
} }
// Skip authentication and permission checks when ACL is "none" (open relay mode)
if acl.Registry.Active.Load() != "none" {
// Validate NIP-98 authentication // Validate NIP-98 authentication
valid, pubkey, err := httpauth.CheckAuth(r) valid, pubkey, err := httpauth.CheckAuth(r)
if chk.E(err) || !valid { if chk.E(err) || !valid {
@ -738,6 +743,7 @@ func (s *Server) handleImport(w http.ResponseWriter, r *http.Request) {
) )
return return
} }
}
ct := r.Header.Get("Content-Type") ct := r.Header.Get("Content-Type")
if strings.HasPrefix(ct, "multipart/form-data") { if strings.HasPrefix(ct, "multipart/form-data") {

33
app/web/src/App.svelte

@ -2276,13 +2276,16 @@
// Export functionality // Export functionality
async function exportEvents(pubkeys = []) { 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"); alert("Please log in first");
return; return;
} }
// Check permissions for exporting all events using current effective role // Check permissions for exporting all events using current effective role
// Skip permission check when ACL is "none"
if ( if (
aclMode !== "none" &&
pubkeys.length === 0 && pubkeys.length === 0 &&
currentEffectiveRole !== "admin" && currentEffectiveRole !== "admin" &&
currentEffectiveRole !== "owner" currentEffectiveRole !== "owner"
@ -2292,16 +2295,19 @@
} }
try { try {
const authHeader = await createNIP98AuthHeader( // 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", "/api/export",
"POST", "POST",
); );
}
const response = await fetch("/api/export", { const response = await fetch("/api/export", {
method: "POST", method: "POST",
headers: { headers,
Authorization: authHeader,
"Content-Type": "application/json",
},
body: JSON.stringify({ pubkeys }), body: JSON.stringify({ pubkeys }),
}); });
@ -2354,7 +2360,8 @@
} }
async function importEvents() { 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"); alert("Admin or owner permission required");
return; return;
} }
@ -2365,18 +2372,20 @@
} }
try { try {
const authHeader = await createNIP98AuthHeader( // Build headers - only include auth when ACL is not "none"
const headers = {};
if (aclMode !== "none" && isLoggedIn) {
headers.Authorization = await createNIP98AuthHeader(
"/api/import", "/api/import",
"POST", "POST",
); );
}
const formData = new FormData(); const formData = new FormData();
formData.append("file", selectedFile); formData.append("file", selectedFile);
const response = await fetch("/api/import", { const response = await fetch("/api/import", {
method: "POST", method: "POST",
headers: { headers,
Authorization: authHeader,
},
body: formData, body: formData,
}); });
@ -2932,6 +2941,7 @@
<ExportView <ExportView
{isLoggedIn} {isLoggedIn}
{currentEffectiveRole} {currentEffectiveRole}
{aclMode}
on:exportMyEvents={exportMyEvents} on:exportMyEvents={exportMyEvents}
on:exportAllEvents={exportAllEvents} on:exportAllEvents={exportAllEvents}
on:openLoginModal={openLoginModal} on:openLoginModal={openLoginModal}
@ -2941,6 +2951,7 @@
{isLoggedIn} {isLoggedIn}
{currentEffectiveRole} {currentEffectiveRole}
{selectedFile} {selectedFile}
{aclMode}
on:fileSelect={handleFileSelect} on:fileSelect={handleFileSelect}
on:importEvents={importEvents} on:importEvents={importEvents}
on:openLoginModal={openLoginModal} on:openLoginModal={openLoginModal}

9
app/web/src/ExportView.svelte

@ -1,10 +1,15 @@
<script> <script>
export let isLoggedIn = false; export let isLoggedIn = false;
export let currentEffectiveRole = ""; export let currentEffectiveRole = "";
export let aclMode = "";
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
// When ACL is "none", allow access without login
$: canAccess = aclMode === "none" || isLoggedIn;
$: canExportAll = aclMode === "none" || currentEffectiveRole === "admin" || currentEffectiveRole === "owner";
function exportMyEvents() { function exportMyEvents() {
dispatch("exportMyEvents"); dispatch("exportMyEvents");
} }
@ -18,6 +23,7 @@
} }
</script> </script>
{#if canAccess}
{#if isLoggedIn} {#if isLoggedIn}
<div class="export-section"> <div class="export-section">
<h3>Export My Events</h3> <h3>Export My Events</h3>
@ -26,7 +32,8 @@
📤 Export My Events 📤 Export My Events
</button> </button>
</div> </div>
{#if currentEffectiveRole === "admin" || currentEffectiveRole === "owner"} {/if}
{#if canExportAll}
<div class="export-section"> <div class="export-section">
<h3>Export All Events</h3> <h3>Export All Events</h3>
<p> <p>

8
app/web/src/ImportView.svelte

@ -2,10 +2,14 @@
export let isLoggedIn = false; export let isLoggedIn = false;
export let currentEffectiveRole = ""; export let currentEffectiveRole = "";
export let selectedFile = null; export let selectedFile = null;
export let aclMode = "";
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
// When ACL is "none", allow access without login
$: canImport = aclMode === "none" || (isLoggedIn && (currentEffectiveRole === "admin" || currentEffectiveRole === "owner"));
function handleFileSelect(event) { function handleFileSelect(event) {
dispatch("fileSelect", event); dispatch("fileSelect", event);
} }
@ -20,7 +24,7 @@
</script> </script>
<div class="import-section"> <div class="import-section">
{#if isLoggedIn && (currentEffectiveRole === "admin" || currentEffectiveRole === "owner")} {#if canImport}
<h3>Import Events</h3> <h3>Import Events</h3>
<p>Upload a JSONL file to import events into the database.</p> <p>Upload a JSONL file to import events into the database.</p>
<div class="recovery-controls-card"> <div class="recovery-controls-card">
@ -42,7 +46,7 @@
<div class="permission-denied"> <div class="permission-denied">
<h3 class="recovery-header">Import Events</h3> <h3 class="recovery-header">Import Events</h3>
<p class="recovery-description"> <p class="recovery-description">
Admin or owner permission required for import functionality. Admin or owner permission required for import functionality.
</p> </p>
</div> </div>
{:else} {:else}

4
main.go

@ -330,7 +330,7 @@ func main() {
os.Exit(1) os.Exit(1)
} }
log.I.F("%s database initialized successfully", cfg.DBType) 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) { if err = acl.Registry.Configure(cfg, db, ctx); chk.E(err) {
os.Exit(1) os.Exit(1)
} }
@ -444,6 +444,7 @@ func makeDatabaseConfig(cfg *config.C) *database.DatabaseConfig {
dataDir, logLevel, dataDir, logLevel,
blockCacheMB, indexCacheMB, queryCacheSizeMB, blockCacheMB, indexCacheMB, queryCacheSizeMB,
queryCacheMaxAge, queryCacheMaxAge,
queryCacheDisabled,
serialCachePubkeys, serialCacheEventIds, serialCachePubkeys, serialCacheEventIds,
zstdLevel, zstdLevel,
neo4jURI, neo4jUser, neo4jPassword := cfg.GetDatabaseConfigValues() neo4jURI, neo4jUser, neo4jPassword := cfg.GetDatabaseConfigValues()
@ -455,6 +456,7 @@ func makeDatabaseConfig(cfg *config.C) *database.DatabaseConfig {
IndexCacheMB: indexCacheMB, IndexCacheMB: indexCacheMB,
QueryCacheSizeMB: queryCacheSizeMB, QueryCacheSizeMB: queryCacheSizeMB,
QueryCacheMaxAge: queryCacheMaxAge, QueryCacheMaxAge: queryCacheMaxAge,
QueryCacheDisabled: queryCacheDisabled,
SerialCachePubkeys: serialCachePubkeys, SerialCachePubkeys: serialCachePubkeys,
SerialCacheEventIds: serialCacheEventIds, SerialCacheEventIds: serialCacheEventIds,
ZSTDLevel: zstdLevel, ZSTDLevel: zstdLevel,

8
pkg/acl/acl.go

@ -3,11 +3,19 @@ package acl
import ( import (
"git.mleku.dev/mleku/nostr/encoders/event" "git.mleku.dev/mleku/nostr/encoders/event"
acliface "next.orly.dev/pkg/interfaces/acl" acliface "next.orly.dev/pkg/interfaces/acl"
"next.orly.dev/pkg/mode"
"next.orly.dev/pkg/utils/atomic" "next.orly.dev/pkg/utils/atomic"
) )
var Registry = &S{} 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 { type S struct {
ACL []acliface.I ACL []acliface.I
Active atomic.String Active atomic.String

8
pkg/database/database.go

@ -106,6 +106,12 @@ func NewWithConfig(
queryCacheSize := int64(queryCacheSizeMB * 1024 * 1024) 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{ d = &D{
ctx: ctx, ctx: ctx,
cancel: cancel, cancel: cancel,
@ -114,7 +120,7 @@ func NewWithConfig(
DB: nil, DB: nil,
seq: nil, seq: nil,
ready: make(chan struct{}), ready: make(chan struct{}),
queryCache: querycache.NewEventCache(queryCacheSize, queryCacheMaxAge), queryCache: qc,
serialCache: NewSerialCache(serialCachePubkeys, serialCacheEventIds), serialCache: NewSerialCache(serialCachePubkeys, serialCacheEventIds),
} }

1
pkg/database/factory.go

@ -20,6 +20,7 @@ type DatabaseConfig struct {
// Badger-specific settings // Badger-specific settings
BlockCacheMB int // ORLY_DB_BLOCK_CACHE_MB BlockCacheMB int // ORLY_DB_BLOCK_CACHE_MB
IndexCacheMB int // ORLY_DB_INDEX_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 QueryCacheSizeMB int // ORLY_QUERY_CACHE_SIZE_MB
QueryCacheMaxAge time.Duration // ORLY_QUERY_CACHE_MAX_AGE QueryCacheMaxAge time.Duration // ORLY_QUERY_CACHE_MAX_AGE

22
pkg/database/query-events.go

@ -13,6 +13,7 @@ import (
"lol.mleku.dev/log" "lol.mleku.dev/log"
"github.com/minio/sha256-simd" "github.com/minio/sha256-simd"
"next.orly.dev/pkg/database/indexes/types" "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/event"
"git.mleku.dev/mleku/nostr/encoders/filter" "git.mleku.dev/mleku/nostr/encoders/filter"
"git.mleku.dev/mleku/nostr/encoders/hex" "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 // 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( log.T.F(
"QueryEvents: id=%s filtered out due to expiration", idHex, "QueryEvents: id=%s filtered out due to expiration", idHex,
) )
@ -112,10 +114,13 @@ func (d *D) QueryEventsWithOptions(c context.Context, f *filter.F, includeDelete
} }
// skip events that have been deleted by a proper deletion event // skip events that have been deleted by a proper deletion event
// Skip deletion check when ACL is "none" (open relay mode)
if !mode.IsOpen() {
if derr := d.CheckForDeleted(ev, nil); derr != nil { if derr := d.CheckForDeleted(ev, nil); derr != nil {
log.T.F("QueryEvents: id=%s filtered out due to deletion: %v", idHex, derr) log.T.F("QueryEvents: id=%s filtered out due to deletion: %v", idHex, derr)
continue continue
} }
}
// Add the event to the results // Add the event to the results
evs = append(evs, ev) evs = append(evs, ev)
@ -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 // 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) expDeletes = append(expDeletes, ser)
expEvs = append(expEvs, ev) expEvs = append(expEvs, ev)
continue continue
} }
// Process deletion events to build our deletion maps // 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 // Check for 'e' tags that directly reference event IDs
eTags := ev.Tags.GetAll([]byte("e")) eTags := ev.Tags.GetAll([]byte("e"))
for _, eTag := range eTags { 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 // 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) eventIdHex := hex.Enc(ev.ID)
if deletedEventIds[eventIdHex] { if aclActive && deletedEventIds[eventIdHex] {
// Skip this event if it has been specifically deleted // Skip this event if it has been specifically deleted
continue 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 // 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 // kind/pubkey and is not in the filter AND there isn't a newer
// event with the same kind/pubkey // event with the same kind/pubkey
if deletionsByKindPubkey[key] && !isIdInFilter { if aclActive && deletionsByKindPubkey[key] && !isIdInFilter {
// This replaceable event has been deleted, skip it // This replaceable event has been deleted, skip it
continue continue
} else if wantMultipleVersions { } else if wantMultipleVersions {
@ -475,6 +484,8 @@ func (d *D) QueryEventsWithOptions(c context.Context, f *filter.F, includeDelete
} }
// Check if this event has been deleted via an a-tag // Check if this event has been deleted via an a-tag
// Skip deletion check when ACL is "none" (open relay mode)
if aclActive {
if deletionMap, exists := deletionsByKindPubkeyDTag[key]; exists { if deletionMap, exists := deletionsByKindPubkeyDTag[key]; exists {
// If there is a deletion timestamp and this event is older than the deletion, // 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 // and this event is not specifically requested by ID, skip it
@ -482,6 +493,7 @@ func (d *D) QueryEventsWithOptions(c context.Context, f *filter.F, includeDelete
continue continue
} }
} }
}
if wantMultipleVersions { if wantMultipleVersions {
// If wantMultipleVersions is true, collect all versions // If wantMultipleVersions is true, collect all versions

4
pkg/database/save-event.go

@ -14,6 +14,7 @@ import (
"lol.mleku.dev/log" "lol.mleku.dev/log"
"next.orly.dev/pkg/database/indexes" "next.orly.dev/pkg/database/indexes"
"next.orly.dev/pkg/database/indexes/types" "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/event"
"git.mleku.dev/mleku/nostr/encoders/filter" "git.mleku.dev/mleku/nostr/encoders/filter"
"git.mleku.dev/mleku/nostr/encoders/hex" "git.mleku.dev/mleku/nostr/encoders/hex"
@ -177,6 +178,8 @@ func (d *D) SaveEvent(c context.Context, ev *event.E) (
} }
// Check if the event has been deleted before allowing resubmission // Check if the event has been deleted before allowing resubmission
// Skip deletion check when ACL is "none" (open relay mode)
if !mode.IsOpen() {
if err = d.CheckForDeleted(ev, nil); err != nil { if err = d.CheckForDeleted(ev, nil); err != nil {
// log.I.F( // log.I.F(
// "SaveEvent: rejecting resubmission of deleted event ID=%s: %v", // "SaveEvent: rejecting resubmission of deleted event ID=%s: %v",
@ -185,6 +188,7 @@ func (d *D) SaveEvent(c context.Context, ev *event.E) (
err = fmt.Errorf("blocked: %s", err.Error()) err = fmt.Errorf("blocked: %s", err.Error())
return return
} }
}
// check for replacement - only validate, don't delete old events // check for replacement - only validate, don't delete old events
if kind.IsReplaceable(ev.Kind) || kind.IsParameterizedReplaceable(ev.Kind) { if kind.IsReplaceable(ev.Kind) || kind.IsParameterizedReplaceable(ev.Kind) {
var werr error var werr error

18
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"
}

2
pkg/run/run.go

@ -120,7 +120,7 @@ func Start(cfg *config.C, opts *Options) (relay *Relay, err error) {
} }
// Configure ACL // 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) { if err = acl.Registry.Configure(cfg, relay.db, relay.ctx); chk.E(err) {
return return
} }

2
pkg/version/version

@ -1 +1 @@
v0.34.2 v0.34.3
Loading…
Cancel
Save