Skip to content

Commit adce4f9

Browse files
committed
fix(bolt): honor configured bind address and refresh deps
- resolve Bolt and HTTP bind address from CLI, config, and env precedence - add Bolt host binding support and regression coverage - bump yzma to v1.12.0 - bump ui dev deps: postcss to 8.5.10 and typescript to 6.0.3
1 parent 8968fe9 commit adce4f9

8 files changed

Lines changed: 194 additions & 26 deletions

File tree

cmd/nornicdb/main.go

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,7 @@ func runServe(cmd *cobra.Command, args []string) error {
265265
// Apply memory configuration FIRST (before heavy allocations)
266266
// First, try to load from config file, then fall back to environment variables
267267
var cfg *config.Config
268+
loadedConfigFile := false
268269
explicitConfigPath, _ := cmd.Flags().GetString("config") // persistent
269270
configPath := strings.TrimSpace(explicitConfigPath)
270271
if configPath == "" {
@@ -285,10 +286,15 @@ func runServe(cmd *cobra.Command, args []string) error {
285286
fmt.Printf("⚠️ Warning: failed to load config from %s: %v\n", configPath, err)
286287
cfg = config.LoadFromEnv()
287288
} else {
289+
loadedConfigFile = true
288290
fmt.Printf("📄 Loaded config from: %s\n", configPath)
289291
}
290292
}
291293

294+
resolvedAddress := resolveBindAddress(cmd, cfg, address, loadedConfigFile)
295+
cfg.Server.HTTPAddress = resolvedAddress
296+
cfg.Server.BoltAddress = resolvedAddress
297+
292298
// YAML config file is the source of truth for embedding settings
293299
// Always use config file values if they are set (non-zero/non-empty)
294300
if cfg.Memory.EmbeddingDimensions > 0 {
@@ -558,7 +564,7 @@ func runServe(cmd *cobra.Command, args []string) error {
558564
// Create and start HTTP server
559565
serverConfig := server.DefaultConfig()
560566
serverConfig.Port = httpPort
561-
serverConfig.Address = address
567+
serverConfig.Address = resolvedAddress
562568
// MCP server configuration
563569
serverConfig.MCPEnabled = mcpEnabled
564570
// Pass embedding settings to server (from loaded config)
@@ -598,6 +604,7 @@ func runServe(cmd *cobra.Command, args []string) error {
598604

599605
// Create and start Bolt server for Neo4j driver compatibility
600606
boltConfig := bolt.DefaultConfig()
607+
boltConfig.Host = resolvedAddress
601608
boltConfig.Port = boltPort
602609
boltConfig.LogQueries = logQueries
603610
boltConfig.ServerAnnouncement = cfg.Server.BoltServerAnnouncement
@@ -629,8 +636,8 @@ func runServe(cmd *cobra.Command, args []string) error {
629636
fmt.Println("✅ NornicDB is ready!")
630637
fmt.Println()
631638
// Determine the display address for user-friendly output
632-
displayAddr := address
633-
if address == "0.0.0.0" {
639+
displayAddr := resolvedAddress
640+
if resolvedAddress == "0.0.0.0" || resolvedAddress == "::" {
634641
displayAddr = "localhost" // 0.0.0.0 is all interfaces, show localhost for convenience
635642
}
636643
fmt.Println("Endpoints:")
@@ -688,6 +695,41 @@ func runServe(cmd *cobra.Command, args []string) error {
688695
return nil
689696
}
690697

698+
func resolveBindAddress(cmd *cobra.Command, cfg *config.Config, cliAddress string, loadedConfigFile bool) string {
699+
resolvedAddress := strings.TrimSpace(cliAddress)
700+
if cmd != nil && !cmd.Flags().Changed("address") && cfg != nil {
701+
if loadedConfigFile && cfg.Server.HTTPAddress != "" {
702+
resolvedAddress = cfg.Server.HTTPAddress
703+
} else if loadedConfigFile && cfg.Server.BoltAddress != "" {
704+
resolvedAddress = cfg.Server.BoltAddress
705+
} else if hasExplicitProtocolBindEnv() {
706+
if cfg.Server.HTTPAddress != "" {
707+
resolvedAddress = cfg.Server.HTTPAddress
708+
} else if cfg.Server.BoltAddress != "" {
709+
resolvedAddress = cfg.Server.BoltAddress
710+
}
711+
}
712+
}
713+
if strings.TrimSpace(resolvedAddress) == "" {
714+
return "127.0.0.1"
715+
}
716+
return strings.TrimSpace(resolvedAddress)
717+
}
718+
719+
func hasExplicitProtocolBindEnv() bool {
720+
for _, envName := range []string{
721+
"NORNICDB_BOLT_ADDRESS",
722+
"NORNICDB_HTTP_ADDRESS",
723+
"NEO4J_dbms_connector_bolt_listen__address",
724+
"NEO4J_dbms_connector_http_listen__address",
725+
} {
726+
if strings.TrimSpace(os.Getenv(envName)) != "" {
727+
return true
728+
}
729+
}
730+
return false
731+
}
732+
691733
func startStdioLogCompactor(maxKB int, interval time.Duration) func() {
692734
if maxKB <= 0 {
693735
return func() {}

cmd/nornicdb/main_address_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package main
2+
3+
import (
4+
"testing"
5+
6+
"github.com/orneryd/nornicdb/pkg/config"
7+
"github.com/spf13/cobra"
8+
)
9+
10+
func TestResolveBindAddress(t *testing.T) {
11+
t.Run("uses_cli_address_when_flag_changed", func(t *testing.T) {
12+
cfg := config.LoadDefaults()
13+
cfg.Server.BoltAddress = "0.0.0.0"
14+
cfg.Server.HTTPAddress = "0.0.0.0"
15+
16+
cmd := &cobra.Command{Use: "test"}
17+
cmd.Flags().String("address", "127.0.0.1", "")
18+
if err := cmd.Flags().Set("address", "127.0.0.1"); err != nil {
19+
t.Fatalf("set address flag: %v", err)
20+
}
21+
22+
resolved := resolveBindAddress(cmd, cfg, "127.0.0.1", false)
23+
if resolved != "127.0.0.1" {
24+
t.Fatalf("expected CLI address to win, got %q", resolved)
25+
}
26+
})
27+
28+
t.Run("uses_loaded_server_address_when_config_file_sets_host", func(t *testing.T) {
29+
cfg := config.LoadDefaults()
30+
cfg.Server.BoltAddress = "127.0.0.2"
31+
cfg.Server.HTTPAddress = "127.0.0.2"
32+
33+
cmd := &cobra.Command{Use: "test"}
34+
cmd.Flags().String("address", "127.0.0.1", "")
35+
36+
resolved := resolveBindAddress(cmd, cfg, "127.0.0.1", true)
37+
if resolved != "127.0.0.2" {
38+
t.Fatalf("expected loaded config address, got %q", resolved)
39+
}
40+
})
41+
42+
t.Run("keeps_secure_default_when_no_explicit_config_exists", func(t *testing.T) {
43+
cfg := config.LoadDefaults()
44+
45+
cmd := &cobra.Command{Use: "test"}
46+
cmd.Flags().String("address", "127.0.0.1", "")
47+
48+
resolved := resolveBindAddress(cmd, cfg, "127.0.0.1", false)
49+
if resolved != "127.0.0.1" {
50+
t.Fatalf("expected loopback CLI default, got %q", resolved)
51+
}
52+
})
53+
54+
t.Run("falls_back_to_protocol_address_when_env_explicitly_sets_it", func(t *testing.T) {
55+
cfg := config.LoadDefaults()
56+
cfg.Server.HTTPAddress = ""
57+
cfg.Server.BoltAddress = "127.0.0.2"
58+
t.Setenv("NORNICDB_BOLT_ADDRESS", "127.0.0.2")
59+
60+
cmd := &cobra.Command{Use: "test"}
61+
cmd.Flags().String("address", "127.0.0.1", "")
62+
63+
resolved := resolveBindAddress(cmd, cfg, "127.0.0.1", false)
64+
if resolved != "127.0.0.2" {
65+
t.Fatalf("expected Bolt address fallback, got %q", resolved)
66+
}
67+
})
68+
69+
t.Run("defaults_to_loopback_when_empty", func(t *testing.T) {
70+
cmd := &cobra.Command{Use: "test"}
71+
cmd.Flags().String("address", "", "")
72+
73+
resolved := resolveBindAddress(cmd, nil, "", false)
74+
if resolved != "127.0.0.1" {
75+
t.Fatalf("expected loopback default, got %q", resolved)
76+
}
77+
})
78+
}

go.mod

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ require (
1212
github.com/hashicorp/go-kms-wrapping/v2 v2.0.20
1313
github.com/hashicorp/go-kms-wrapping/wrappers/awskms/v2 v2.0.11
1414
github.com/hashicorp/go-kms-wrapping/wrappers/azurekeyvault/v2 v2.0.14
15-
github.com/hybridgroup/yzma v1.11.1
15+
github.com/hybridgroup/yzma v1.12.0
1616
github.com/neo4j/neo4j-go-driver/v5 v5.28.4
1717
github.com/qdrant/go-client v1.17.1
1818
github.com/spf13/cobra v1.10.2
@@ -55,7 +55,7 @@ require (
5555
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
5656
github.com/dgraph-io/ristretto/v2 v2.2.0 // indirect
5757
github.com/dustin/go-humanize v1.0.1 // indirect
58-
github.com/fatih/color v1.14.1 // indirect
58+
github.com/fatih/color v1.18.0 // indirect
5959
github.com/felixge/httpsnoop v1.0.4 // indirect
6060
github.com/go-logr/logr v1.4.3 // indirect
6161
github.com/go-logr/stdr v1.2.2 // indirect
@@ -69,7 +69,7 @@ require (
6969
github.com/gorilla/websocket v1.5.3 // indirect
7070
github.com/hashicorp/errwrap v1.1.0 // indirect
7171
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
72-
github.com/hashicorp/go-hclog v1.5.0 // indirect
72+
github.com/hashicorp/go-hclog v1.6.3 // indirect
7373
github.com/hashicorp/go-multierror v1.1.1 // indirect
7474
github.com/hashicorp/go-secure-stdlib/awsutil v0.1.6 // indirect
7575
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.9 // indirect
@@ -98,6 +98,7 @@ require (
9898
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect
9999
go.opentelemetry.io/otel v1.43.0 // indirect
100100
go.opentelemetry.io/otel/metric v1.43.0 // indirect
101+
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
101102
go.opentelemetry.io/otel/trace v1.43.0 // indirect
102103
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect
103104
golang.org/x/oauth2 v0.36.0 // indirect

go.sum

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,8 @@ github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg
9595
github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA=
9696
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
9797
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
98-
github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w=
99-
github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg=
98+
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
99+
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
100100
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
101101
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
102102
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -135,8 +135,8 @@ github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brv
135135
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
136136
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
137137
github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
138-
github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c=
139-
github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
138+
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
139+
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
140140
github.com/hashicorp/go-kms-wrapping/v2 v2.0.20 h1:afreZ1WJd0WI7v4NsMZ1aL7V/T59sxPuKmQDgGUja20=
141141
github.com/hashicorp/go-kms-wrapping/v2 v2.0.20/go.mod h1:NeK2Ul15t1zutp/dZzt28XQrGZHosbxE/QLNNfaWObM=
142142
github.com/hashicorp/go-kms-wrapping/wrappers/awskms/v2 v2.0.11 h1:J9zGa9SlcOHT3SQTj0Vv3shHo0anWbs58weURGCgChI=
@@ -157,8 +157,8 @@ github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/C
157157
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
158158
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
159159
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
160-
github.com/hybridgroup/yzma v1.11.1 h1:HvpPAFzg6lAWTFgxC8R/wjkRbTW2hd2dEM0F41t/gq8=
161-
github.com/hybridgroup/yzma v1.11.1/go.mod h1:zrzMgv/KVQz23+s6l16b+vJ+9uJVBdWtGcGkwRTMeiQ=
160+
github.com/hybridgroup/yzma v1.12.0 h1:kRHZdzEQCuQaBCUx7AVWYqVUvTHw8qB6gp+9ORcryLg=
161+
github.com/hybridgroup/yzma v1.12.0/go.mod h1:zrzMgv/KVQz23+s6l16b+vJ+9uJVBdWtGcGkwRTMeiQ=
162162
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
163163
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
164164
github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
@@ -254,8 +254,8 @@ go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWv
254254
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
255255
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
256256
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
257-
go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=
258-
go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=
257+
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
258+
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
259259
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
260260
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
261261
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=

pkg/bolt/server.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,7 @@ func (r *BoltAuthResult) HasPermission(perm string) bool {
472472
// config = bolt.DefaultConfig()
473473
// config.Port = 7688 // Use different port
474474
type Config struct {
475+
Host string
475476
Port int
476477
MaxConnections int
477478
ReadBufferSize int
@@ -500,6 +501,7 @@ type Config struct {
500501
// server := bolt.New(config, executor)
501502
func DefaultConfig() *Config {
502503
return &Config{
504+
Host: "127.0.0.1",
503505
Port: 7687,
504506
MaxConnections: 100,
505507
ReadBufferSize: 8192,
@@ -772,14 +774,26 @@ func (s *Server) SetResolvedAccessResolver(resolver func(roles []string, dbName
772774
//
773775
// The server will print its listening address when started successfully.
774776
func (s *Server) ListenAndServe() error {
775-
addr := fmt.Sprintf(":%d", s.config.Port)
777+
host := strings.TrimSpace(s.config.Host)
778+
if host == "" {
779+
host = "127.0.0.1"
780+
}
781+
addr := net.JoinHostPort(host, strconv.Itoa(s.config.Port))
776782
listener, err := net.Listen("tcp", addr)
777783
if err != nil {
778784
return fmt.Errorf("failed to listen on %s: %w", addr, err)
779785
}
780786
s.listener = listener
781787

782-
fmt.Printf("Bolt server listening on bolt://localhost:%d\n", s.config.Port)
788+
announceHost := host
789+
if host == "0.0.0.0" || host == "::" || host == "" {
790+
announceHost = "localhost"
791+
}
792+
actualPort := s.config.Port
793+
if tcpAddr, ok := listener.Addr().(*net.TCPAddr); ok && tcpAddr.Port > 0 {
794+
actualPort = tcpAddr.Port
795+
}
796+
fmt.Printf("Bolt server listening on bolt://%s:%d\n", announceHost, actualPort)
783797

784798
return s.serve()
785799
}

pkg/bolt/server_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ func (m *mockDBManager) DefaultDatabaseName() string {
6363
func TestDefaultConfig(t *testing.T) {
6464
config := DefaultConfig()
6565

66+
if config.Host != "127.0.0.1" {
67+
t.Errorf("expected host 127.0.0.1, got %q", config.Host)
68+
}
6669
if config.Port != 7687 {
6770
t.Errorf("expected port 7687, got %d", config.Port)
6871
}
@@ -562,6 +565,36 @@ func TestListenAndServe(t *testing.T) {
562565
}
563566
})
564567

568+
t.Run("binds_configured_host", func(t *testing.T) {
569+
config := &Config{Host: "127.0.0.1", Port: 0, MaxConnections: 10}
570+
server := New(config, &mockExecutor{})
571+
572+
done := make(chan error, 1)
573+
go func() {
574+
done <- server.ListenAndServe()
575+
}()
576+
577+
time.Sleep(50 * time.Millisecond)
578+
579+
tcpAddr, ok := server.listener.Addr().(*net.TCPAddr)
580+
if !ok {
581+
t.Fatalf("expected TCP listener address, got %T", server.listener.Addr())
582+
}
583+
if !tcpAddr.IP.IsLoopback() {
584+
t.Fatalf("expected loopback bind address, got %v", tcpAddr.IP)
585+
}
586+
587+
if err := server.Close(); err != nil {
588+
t.Fatalf("Close() error = %v", err)
589+
}
590+
591+
select {
592+
case <-done:
593+
case <-time.After(500 * time.Millisecond):
594+
t.Fatal("server did not shut down")
595+
}
596+
})
597+
565598
t.Run("listen_error", func(t *testing.T) {
566599
// Try to listen on an invalid port
567600
config := &Config{Port: -1}

ui/package-lock.json

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ui/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@
2424
"@vitejs/plugin-react": "^6.0.1",
2525
"autoprefixer": "^10.5.0",
2626
"baseline-browser-mapping": "^2.10.19",
27-
"postcss": "^8.5.9",
27+
"postcss": "^8.5.10",
2828
"tailwindcss": "^4.2.2",
29-
"typescript": "^6.0.2"
29+
"typescript": "^6.0.3"
3030
}
3131
}

0 commit comments

Comments
 (0)