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

9
app/config/config.go

@ -91,9 +91,10 @@ type C struct { @@ -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() ( @@ -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() ( @@ -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

21
app/handle-event.go

@ -656,15 +656,18 @@ func (l *Listener) HandleEvent(msg []byte) (err error) { @@ -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
}
}
}
}

4
app/handle-nip43_test.go

@ -54,7 +54,7 @@ func setupTestListener(t *testing.T) (*Listener, *database.D, func()) { @@ -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) { @@ -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)
}

2
app/handle_policy_config_test.go

@ -88,7 +88,7 @@ func setupPolicyTestListener(t *testing.T, policyAdminHex string) (*Listener, *d @@ -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)

2
app/nip43_e2e_test.go

@ -105,7 +105,7 @@ func setupE2ETest(t *testing.T) (*Server, *httptest.Server, func()) { @@ -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)

72
app/server.go

@ -550,25 +550,28 @@ func (s *Server) handleExport(w http.ResponseWriter, r *http.Request) { @@ -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) { @@ -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")

45
app/web/src/App.svelte

@ -2276,13 +2276,16 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -2932,6 +2941,7 @@
<ExportView
{isLoggedIn}
{currentEffectiveRole}
{aclMode}
on:exportMyEvents={exportMyEvents}
on:exportAllEvents={exportAllEvents}
on:openLoginModal={openLoginModal}
@ -2941,6 +2951,7 @@ @@ -2941,6 +2951,7 @@
{isLoggedIn}
{currentEffectiveRole}
{selectedFile}
{aclMode}
on:fileSelect={handleFileSelect}
on:importEvents={importEvents}
on:openLoginModal={openLoginModal}

25
app/web/src/ExportView.svelte

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

8
app/web/src/ImportView.svelte

@ -2,10 +2,14 @@ @@ -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 @@ @@ -20,7 +24,7 @@
</script>
<div class="import-section">
{#if isLoggedIn && (currentEffectiveRole === "admin" || currentEffectiveRole === "owner")}
{#if canImport}
<h3>Import Events</h3>
<p>Upload a JSONL file to import events into the database.</p>
<div class="recovery-controls-card">
@ -42,7 +46,7 @@ @@ -42,7 +46,7 @@
<div class="permission-denied">
<h3 class="recovery-header">Import Events</h3>
<p class="recovery-description">
Admin or owner permission required for import functionality.
Admin or owner permission required for import functionality.
</p>
</div>
{:else}

4
main.go

@ -330,7 +330,7 @@ func main() { @@ -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 { @@ -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 { @@ -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,

8
pkg/acl/acl.go

@ -3,11 +3,19 @@ package acl @@ -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

8
pkg/database/database.go

@ -106,6 +106,12 @@ func NewWithConfig( @@ -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( @@ -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),
}

9
pkg/database/factory.go

@ -18,10 +18,11 @@ type DatabaseConfig struct { @@ -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)

38
pkg/database/query-events.go

@ -13,6 +13,7 @@ import ( @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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
}
}
}

18
pkg/database/save-event.go

@ -14,6 +14,7 @@ import ( @@ -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) ( @@ -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) {

18
pkg/mode/mode.go

@ -0,0 +1,18 @@ @@ -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) { @@ -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
}

2
pkg/version/version

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