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. 9
      app/config/config.go
  3. 21
      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. 72
      app/server.go
  8. 45
      app/web/src/App.svelte
  9. 25
      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. 9
      pkg/database/factory.go
  15. 38
      pkg/database/query-events.go
  16. 18
      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"
} }

9
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"` NIP43InviteExpiry time.Duration `env:"ORLY_NIP43_INVITE_EXPIRY" default:"24h" usage:"how long invite codes remain valid"`
// 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"`
QueryCacheSizeMB int `env:"ORLY_QUERY_CACHE_SIZE_MB" default:"512" usage:"query cache size in MB (caches database query results for faster REQ responses)"` QueryCacheDisabled bool `env:"ORLY_QUERY_CACHE_DISABLED" default:"true" usage:"disable query cache to reduce memory usage (trades memory for query performance)"`
QueryCacheMaxAge string `env:"ORLY_QUERY_CACHE_MAX_AGE" default:"5m" usage:"maximum age for cached query results (e.g., 5m, 10m, 1h)"` 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) // 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)"` 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, 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

21
app/handle-event.go

@ -656,15 +656,18 @@ func (l *Listener) HandleEvent(msg []byte) (err error) {
return return
} else { } else {
// check if the event was deleted // check if the event was deleted
// Combine admins and owners for deletion checking // Skip deletion check when ACL is "none" (open relay mode)
adminOwners := append(l.Admins, l.Owners...) if acl.Registry.Active.Load() != "none" {
if err = l.DB.CheckForDeleted(env.E, adminOwners); err != nil { // Combine admins and owners for deletion checking
if strings.HasPrefix(err.Error(), "blocked:") { adminOwners := append(l.Admins, l.Owners...)
errStr := err.Error()[len("blocked: "):len(err.Error())] if err = l.DB.CheckForDeleted(env.E, adminOwners); err != nil {
if err = Ok.Error( if strings.HasPrefix(err.Error(), "blocked:") {
l, env, errStr, errStr := err.Error()[len("blocked: "):len(err.Error())]
); chk.E(err) { if err = Ok.Error(
return l, env, errStr,
); chk.E(err) {
return
}
} }
} }
} }

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)

72
app/server.go

@ -550,25 +550,28 @@ func (s *Server) handleExport(w http.ResponseWriter, r *http.Request) {
return return
} }
// Validate NIP-98 authentication // Skip authentication and permission checks when ACL is "none" (open relay mode)
valid, pubkey, err := httpauth.CheckAuth(r) if acl.Registry.Active.Load() != "none" {
if chk.E(err) || !valid { // Validate NIP-98 authentication
errorMsg := "NIP-98 authentication validation failed" valid, pubkey, err := httpauth.CheckAuth(r)
if err != nil { if chk.E(err) || !valid {
errorMsg = err.Error() 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 // Check permissions - require write, admin, or owner level
accessLevel := acl.Registry.GetAccessLevel(pubkey, r.RemoteAddr) accessLevel := acl.Registry.GetAccessLevel(pubkey, r.RemoteAddr)
if accessLevel != "write" && accessLevel != "admin" && accessLevel != "owner" { if accessLevel != "write" && accessLevel != "admin" && accessLevel != "owner" {
http.Error( http.Error(
w, "Write, admin, or owner permission required", w, "Write, admin, or owner permission required",
http.StatusForbidden, http.StatusForbidden,
) )
return return
}
} }
// Parse pubkeys from request // Parse pubkeys from request
@ -719,24 +722,27 @@ func (s *Server) handleImport(w http.ResponseWriter, r *http.Request) {
return return
} }
// Validate NIP-98 authentication // Skip authentication and permission checks when ACL is "none" (open relay mode)
valid, pubkey, err := httpauth.CheckAuth(r) if acl.Registry.Active.Load() != "none" {
if chk.E(err) || !valid { // Validate NIP-98 authentication
errorMsg := "NIP-98 authentication validation failed" valid, pubkey, err := httpauth.CheckAuth(r)
if err != nil { if chk.E(err) || !valid {
errorMsg = err.Error() 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 // Check permissions - require admin or owner level
accessLevel := acl.Registry.GetAccessLevel(pubkey, r.RemoteAddr) accessLevel := acl.Registry.GetAccessLevel(pubkey, r.RemoteAddr)
if accessLevel != "admin" && accessLevel != "owner" { if accessLevel != "admin" && accessLevel != "owner" {
http.Error( http.Error(
w, "Admin or owner permission required", http.StatusForbidden, w, "Admin or owner permission required", http.StatusForbidden,
) )
return return
}
} }
ct := r.Header.Get("Content-Type") ct := r.Header.Get("Content-Type")

45
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"
"/api/export", const headers = {
"POST", "Content-Type": "application/json",
); };
if (aclMode !== "none" && isLoggedIn) {
headers.Authorization = await createNIP98AuthHeader(
"/api/export",
"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"
"/api/import", const headers = {};
"POST", if (aclMode !== "none" && isLoggedIn) {
); headers.Authorization = await createNIP98AuthHeader(
"/api/import",
"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}

25
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,15 +23,17 @@
} }
</script> </script>
{#if isLoggedIn} {#if canAccess}
<div class="export-section"> {#if isLoggedIn}
<h3>Export My Events</h3> <div class="export-section">
<p>Download your personal events as a JSONL file.</p> <h3>Export My Events</h3>
<button class="export-btn" on:click={exportMyEvents}> <p>Download your personal events as a JSONL file.</p>
📤 Export My Events <button class="export-btn" on:click={exportMyEvents}>
</button> 📤 Export My Events
</div> </button>
{#if currentEffectiveRole === "admin" || currentEffectiveRole === "owner"} </div>
{/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),
} }

9
pkg/database/factory.go

@ -18,10 +18,11 @@ type DatabaseConfig struct {
LogLevel string LogLevel string
// 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
QueryCacheSizeMB int // ORLY_QUERY_CACHE_SIZE_MB QueryCacheDisabled bool // ORLY_QUERY_CACHE_DISABLED - disable query cache to reduce memory usage
QueryCacheMaxAge time.Duration // ORLY_QUERY_CACHE_MAX_AGE QueryCacheSizeMB int // ORLY_QUERY_CACHE_SIZE_MB
QueryCacheMaxAge time.Duration // ORLY_QUERY_CACHE_MAX_AGE
// Serial cache settings for compact event storage // Serial cache settings for compact event storage
SerialCachePubkeys int // ORLY_SERIAL_CACHE_PUBKEYS - max pubkeys to cache (default: 100000) SerialCachePubkeys int // ORLY_SERIAL_CACHE_PUBKEYS - max pubkeys to cache (default: 100000)

38
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,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 // skip events that have been deleted by a proper deletion event
if derr := d.CheckForDeleted(ev, nil); derr != nil { // Skip deletion check when ACL is "none" (open relay mode)
log.T.F("QueryEvents: id=%s filtered out due to deletion: %v", idHex, derr) if !mode.IsOpen() {
continue 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 // 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 // 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,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 // Check if this event has been deleted via an a-tag
if deletionMap, exists := deletionsByKindPubkeyDTag[key]; exists { // Skip deletion check when ACL is "none" (open relay mode)
// If there is a deletion timestamp and this event is older than the deletion, if aclActive {
// and this event is not specifically requested by ID, skip it if deletionMap, exists := deletionsByKindPubkeyDTag[key]; exists {
if delTs, ok := deletionMap[dValue]; ok && ev.CreatedAt < delTs && !isIdInFilter { // If there is a deletion timestamp and this event is older than the deletion,
continue // and this event is not specifically requested by ID, skip it
if delTs, ok := deletionMap[dValue]; ok && ev.CreatedAt < delTs && !isIdInFilter {
continue
}
} }
} }

18
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,13 +178,16 @@ 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
if err = d.CheckForDeleted(ev, nil); err != nil { // Skip deletion check when ACL is "none" (open relay mode)
// log.I.F( if !mode.IsOpen() {
// "SaveEvent: rejecting resubmission of deleted event ID=%s: %v", if err = d.CheckForDeleted(ev, nil); err != nil {
// hex.Enc(ev.ID), err, // log.I.F(
// ) // "SaveEvent: rejecting resubmission of deleted event ID=%s: %v",
err = fmt.Errorf("blocked: %s", err.Error()) // hex.Enc(ev.ID), err,
return // )
err = fmt.Errorf("blocked: %s", err.Error())
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) {

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